ctxman 3.0.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.
Files changed (190) hide show
  1. package/.ctxman/custom-profiles.example.json +20 -0
  2. package/.ctxman/llm-profiles.json +131 -0
  3. package/.ctxman/logs/ctxman-2026-07-02.log +156 -0
  4. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  5. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  6. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  7. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  8. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  9. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  10. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  11. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  12. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  13. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  14. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  15. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  16. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  17. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  18. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  19. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  20. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  21. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  22. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  23. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  24. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  25. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  26. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  27. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  28. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  29. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  30. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  31. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  32. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  33. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  34. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  35. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  36. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  37. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  38. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  39. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  40. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  41. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  42. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  43. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  44. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  45. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  46. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  47. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  48. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  49. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  50. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  51. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  52. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  53. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  54. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  55. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  56. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  57. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  58. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  59. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  60. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  61. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  62. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  63. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  64. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  65. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  66. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
  67. package/.ctxman/wizard-profiles/code-review/.contextignore +41 -0
  68. package/.ctxman/wizard-profiles/code-review/.contextinclude +30 -0
  69. package/.ctxman/wizard-profiles/code-review/.methodignore +26 -0
  70. package/.ctxman/wizard-profiles/code-review/.methodinclude +47 -0
  71. package/.ctxman/wizard-profiles/code-review/profile.json +26 -0
  72. package/.ctxman/wizard-profiles/documentation/.contextignore +21 -0
  73. package/.ctxman/wizard-profiles/documentation/.contextinclude +24 -0
  74. package/.ctxman/wizard-profiles/documentation/.methodignore +19 -0
  75. package/.ctxman/wizard-profiles/documentation/.methodinclude +44 -0
  76. package/.ctxman/wizard-profiles/documentation/profile.json +27 -0
  77. package/.ctxman/wizard-profiles/full/.contextignore +23 -0
  78. package/.ctxman/wizard-profiles/full/.contextinclude +38 -0
  79. package/.ctxman/wizard-profiles/full/.methodignore +5 -0
  80. package/.ctxman/wizard-profiles/full/.methodinclude +5 -0
  81. package/.ctxman/wizard-profiles/full/profile.json +28 -0
  82. package/.ctxman/wizard-profiles/llm-explain/.contextignore +25 -0
  83. package/.ctxman/wizard-profiles/llm-explain/.contextinclude +33 -0
  84. package/.ctxman/wizard-profiles/llm-explain/.methodignore +19 -0
  85. package/.ctxman/wizard-profiles/llm-explain/.methodinclude +48 -0
  86. package/.ctxman/wizard-profiles/llm-explain/profile.json +27 -0
  87. package/.ctxman/wizard-profiles/minimal/.contextignore +27 -0
  88. package/.ctxman/wizard-profiles/minimal/.contextinclude +14 -0
  89. package/.ctxman/wizard-profiles/minimal/.methodignore +22 -0
  90. package/.ctxman/wizard-profiles/minimal/.methodinclude +18 -0
  91. package/.ctxman/wizard-profiles/minimal/profile.json +26 -0
  92. package/.ctxman/wizard-profiles/security-audit/.contextignore +36 -0
  93. package/.ctxman/wizard-profiles/security-audit/.contextinclude +37 -0
  94. package/.ctxman/wizard-profiles/security-audit/.methodignore +29 -0
  95. package/.ctxman/wizard-profiles/security-audit/.methodinclude +60 -0
  96. package/.ctxman/wizard-profiles/security-audit/profile.json +34 -0
  97. package/README.md +1167 -0
  98. package/bin/cli.js +650 -0
  99. package/bin/cm-ask.js +95 -0
  100. package/bin/cm-gitingest.js +134 -0
  101. package/bin/cm-update.js +175 -0
  102. package/bin/mcp-server.js +31 -0
  103. package/ctxman.js +236 -0
  104. package/examples/README.md +298 -0
  105. package/examples/custom-llm-profiles.example.json +19 -0
  106. package/examples/wizard-profiles/code-review/.contextignore +41 -0
  107. package/examples/wizard-profiles/code-review/.contextinclude +30 -0
  108. package/examples/wizard-profiles/code-review/.methodignore +26 -0
  109. package/examples/wizard-profiles/code-review/.methodinclude +47 -0
  110. package/examples/wizard-profiles/code-review/profile.json +26 -0
  111. package/examples/wizard-profiles/documentation/.contextignore +21 -0
  112. package/examples/wizard-profiles/documentation/.contextinclude +24 -0
  113. package/examples/wizard-profiles/documentation/.methodignore +19 -0
  114. package/examples/wizard-profiles/documentation/.methodinclude +44 -0
  115. package/examples/wizard-profiles/documentation/profile.json +27 -0
  116. package/examples/wizard-profiles/full/.contextignore +23 -0
  117. package/examples/wizard-profiles/full/.contextinclude +38 -0
  118. package/examples/wizard-profiles/full/.methodignore +5 -0
  119. package/examples/wizard-profiles/full/.methodinclude +5 -0
  120. package/examples/wizard-profiles/full/profile.json +28 -0
  121. package/examples/wizard-profiles/llm-explain/.contextignore +25 -0
  122. package/examples/wizard-profiles/llm-explain/.contextinclude +33 -0
  123. package/examples/wizard-profiles/llm-explain/.methodignore +19 -0
  124. package/examples/wizard-profiles/llm-explain/.methodinclude +48 -0
  125. package/examples/wizard-profiles/llm-explain/profile.json +27 -0
  126. package/examples/wizard-profiles/minimal/.contextignore +27 -0
  127. package/examples/wizard-profiles/minimal/.contextinclude +14 -0
  128. package/examples/wizard-profiles/minimal/.methodignore +22 -0
  129. package/examples/wizard-profiles/minimal/.methodinclude +18 -0
  130. package/examples/wizard-profiles/minimal/profile.json +26 -0
  131. package/examples/wizard-profiles/security-audit/.contextignore +36 -0
  132. package/examples/wizard-profiles/security-audit/.contextinclude +37 -0
  133. package/examples/wizard-profiles/security-audit/.methodignore +29 -0
  134. package/examples/wizard-profiles/security-audit/.methodinclude +60 -0
  135. package/examples/wizard-profiles/security-audit/profile.json +34 -0
  136. package/index.js +68 -0
  137. package/lib/agents/pr-failure-agent.js +201 -0
  138. package/lib/analyzers/go-method-analyzer.js +126 -0
  139. package/lib/analyzers/method-analyzer.js +457 -0
  140. package/lib/analyzers/token-calculator.js +577 -0
  141. package/lib/api/mcp/prompts.js +319 -0
  142. package/lib/api/mcp/resources.js +269 -0
  143. package/lib/api/mcp/server.js +339 -0
  144. package/lib/api/mcp/transports/HttpSseTransport.js +63 -0
  145. package/lib/api/rest/server.js +405 -0
  146. package/lib/cache/CacheManager.js +388 -0
  147. package/lib/core/Analyzer.js +180 -0
  148. package/lib/core/ContextBuilder.js +325 -0
  149. package/lib/core/Reporter.js +270 -0
  150. package/lib/core/Scanner.js +129 -0
  151. package/lib/formatters/format-registry.js +419 -0
  152. package/lib/formatters/gitingest-formatter.js +746 -0
  153. package/lib/formatters/toon-formatter-v1.3.js +731 -0
  154. package/lib/formatters/toon-formatter.js +291 -0
  155. package/lib/integrations/git/BlameTracker.js +142 -0
  156. package/lib/integrations/git/DiffAnalyzer.js +260 -0
  157. package/lib/integrations/git/GitClient.js +336 -0
  158. package/lib/parsers/gitignore-parser.js +127 -0
  159. package/lib/parsers/method-filter-parser.js +50 -0
  160. package/lib/plugins/ExporterPlugin.js +102 -0
  161. package/lib/plugins/LanguagePlugin.js +100 -0
  162. package/lib/plugins/PluginManager.js +302 -0
  163. package/lib/rag/DataSourcePlugin.js +31 -0
  164. package/lib/rag/EmbeddingProvider.js +75 -0
  165. package/lib/rag/Indexer.js +83 -0
  166. package/lib/rag/VectorStoreFactory.js +24 -0
  167. package/lib/rag/sources/FileDataSource.js +70 -0
  168. package/lib/rag/stores/BaseVectorStore.js +43 -0
  169. package/lib/rag/stores/ChromaDBStore.js +85 -0
  170. package/lib/rag/stores/LanceDBStore.js +109 -0
  171. package/lib/rag/stores/PineconeStore.js +74 -0
  172. package/lib/ui/dashboard.js +109 -0
  173. package/lib/ui/index.js +15 -0
  174. package/lib/ui/progress-bar.js +110 -0
  175. package/lib/ui/select-input.js +51 -0
  176. package/lib/ui/wizard.js +359 -0
  177. package/lib/utils/clipboard-utils.js +66 -0
  178. package/lib/utils/config-utils.js +94 -0
  179. package/lib/utils/error-handler.js +145 -0
  180. package/lib/utils/file-utils.js +89 -0
  181. package/lib/utils/format-converter.js +325 -0
  182. package/lib/utils/git-utils.js +337 -0
  183. package/lib/utils/llm-detector.js +409 -0
  184. package/lib/utils/logger.js +278 -0
  185. package/lib/utils/token-utils.js +200 -0
  186. package/lib/utils/tokenizer-adapter.js +454 -0
  187. package/lib/utils/updater.js +565 -0
  188. package/lib/watch/FileWatcher.js +208 -0
  189. package/lib/watch/IncrementalAnalyzer.js +206 -0
  190. package/package.json +118 -0
@@ -0,0 +1,731 @@
1
+ /**
2
+ * TOON Format Encoder/Decoder - Spec v1.3 Compliant
3
+ *
4
+ * Full implementation of TOON (Tabular Object Oriented Notation)
5
+ * Following official spec: https://github.com/toon-format/spec
6
+ *
7
+ * Spec Version: 1.3 (2025-10-31)
8
+ * Features:
9
+ * - Array length markers [N]
10
+ * - Delimiter options: comma, tab, pipe
11
+ * - Length marker prefix: # (optional)
12
+ * - List format for non-uniform arrays
13
+ * - Smart quoting rules
14
+ * - Type normalizations
15
+ * - Full decoder support
16
+ */
17
+
18
+ class ToonFormatterV13 {
19
+ constructor(options = {}) {
20
+ this.indent = options.indent || 2;
21
+ this.delimiter = options.delimiter || ',';
22
+ this.lengthMarker = options.lengthMarker || false; // false or '#'
23
+ this.indentChar = ' '.repeat(this.indent);
24
+ }
25
+
26
+ /**
27
+ * Encode data to TOON format (Spec v1.3)
28
+ */
29
+ encode(data, options = {}) {
30
+ // Merge options
31
+ const opts = { ...this, ...options };
32
+ this.indent = opts.indent;
33
+ this.delimiter = opts.delimiter || ',';
34
+ this.lengthMarker = opts.lengthMarker || false;
35
+ this.indentChar = ' '.repeat(this.indent);
36
+
37
+ return this.encodeValue(data, 0).trimEnd();
38
+ }
39
+
40
+ /**
41
+ * Encode any value
42
+ */
43
+ encodeValue(value, depth = 0) {
44
+ const normalized = this.normalizeValue(value);
45
+
46
+ if (normalized === null) return 'null';
47
+ if (typeof normalized === 'boolean') return String(normalized);
48
+ if (typeof normalized === 'number') return this.encodeNumber(normalized);
49
+ if (typeof normalized === 'string') return this.encodeString(normalized);
50
+ if (Array.isArray(normalized)) return this.encodeArray(normalized, depth);
51
+ if (typeof normalized === 'object') return this.encodeObject(normalized, depth);
52
+
53
+ return 'null';
54
+ }
55
+
56
+ /**
57
+ * Normalize non-JSON values (Spec: Type Conversions)
58
+ */
59
+ normalizeValue(value) {
60
+ // undefined → null
61
+ if (value === undefined) return null;
62
+
63
+ // function → null
64
+ if (typeof value === 'function') return null;
65
+
66
+ // symbol → null
67
+ if (typeof value === 'symbol') return null;
68
+
69
+ // NaN, Infinity → null
70
+ if (typeof value === 'number' && !isFinite(value)) return null;
71
+
72
+ // BigInt → number or string
73
+ if (typeof value === 'bigint') {
74
+ if (value >= Number.MIN_SAFE_INTEGER && value <= Number.MAX_SAFE_INTEGER) {
75
+ return Number(value);
76
+ }
77
+ return String(value); // Will be quoted
78
+ }
79
+
80
+ // Date → ISO string
81
+ if (value instanceof Date) {
82
+ return value.toISOString();
83
+ }
84
+
85
+ return value;
86
+ }
87
+
88
+ /**
89
+ * Encode number (no scientific notation)
90
+ */
91
+ encodeNumber(num) {
92
+ // -0 → 0
93
+ if (Object.is(num, -0)) return '0';
94
+
95
+ // No scientific notation
96
+ return num.toLocaleString('en-US', {
97
+ useGrouping: false,
98
+ maximumFractionDigits: 20
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Encode string with smart quoting (Spec: Quoting Rules)
104
+ */
105
+ encodeString(str) {
106
+ const escaped = str
107
+ .replace(/\\/g, '\\\\')
108
+ .replace(/"/g, '\\"')
109
+ .replace(/\n/g, '\\n')
110
+ .replace(/\r/g, '\\r')
111
+ .replace(/\t/g, '\\t');
112
+
113
+ // Always quote if any of these conditions:
114
+ if (
115
+ str === '' || // Empty string
116
+ str !== str.trim() || // Leading/trailing spaces
117
+ str.startsWith('- ') || // Looks like list item
118
+ /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(str) || // Looks like number
119
+ /^(true|false|null)$/.test(str) || // Looks like boolean/null
120
+ /[\[\]{}:]/.test(str) || // Contains structural chars
121
+ /^(\[.*\]|\{.*\})$/.test(str) || // Looks structural
122
+ str.includes(this.delimiter) || // Contains active delimiter
123
+ str.includes(':') || // Contains colon
124
+ str.includes('"') || // Contains quote
125
+ str.includes('\\') // Contains backslash
126
+ ) {
127
+ return `"${escaped}"`;
128
+ }
129
+
130
+ return escaped;
131
+ }
132
+
133
+ /**
134
+ * Encode object
135
+ */
136
+ encodeObject(obj, depth = 0) {
137
+ const keys = Object.keys(obj);
138
+
139
+ if (keys.length === 0) {
140
+ return ''; // Empty object
141
+ }
142
+
143
+ const prefix = this.indentChar.repeat(depth);
144
+ const lines = [];
145
+
146
+ for (const key of keys) {
147
+ const quotedKey = this.needsQuotingAsKey(key) ? `"${key}"` : key;
148
+ const value = obj[key];
149
+
150
+ const encoded = this.encodeValue(value, depth + 1);
151
+
152
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
153
+ // Nested object
154
+ lines.push(`${prefix}${quotedKey}:`);
155
+ if (encoded) {
156
+ lines.push(encoded);
157
+ }
158
+ } else {
159
+ lines.push(`${prefix}${quotedKey}: ${encoded}`);
160
+ }
161
+ }
162
+
163
+ return lines.join('\n');
164
+ }
165
+
166
+ /**
167
+ * Check if key needs quoting
168
+ */
169
+ needsQuotingAsKey(key) {
170
+ // Keys must match: /^[a-zA-Z_][a-zA-Z0-9_.]*$/
171
+ return !/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(key);
172
+ }
173
+
174
+ /**
175
+ * Encode array with length markers (Spec v1.3)
176
+ */
177
+ encodeArray(arr, depth = 0) {
178
+ const prefix = this.indentChar.repeat(depth);
179
+ const length = arr.length;
180
+
181
+ if (length === 0) {
182
+ const lengthPrefix = this.lengthMarker ? `#0` : `0`;
183
+ return `[${lengthPrefix}]:`;
184
+ }
185
+
186
+ // Check if tabular format (uniform objects with primitive values)
187
+ if (this.isTabularArray(arr)) {
188
+ return this.encodeTabularArray(arr, depth);
189
+ }
190
+
191
+ // Check if primitive array (inline)
192
+ if (this.isPrimitiveArray(arr)) {
193
+ return this.encodePrimitiveArray(arr);
194
+ }
195
+
196
+ // Non-uniform or complex: use list format
197
+ return this.encodeListArray(arr, depth);
198
+ }
199
+
200
+ /**
201
+ * Check if array is suitable for tabular format
202
+ */
203
+ isTabularArray(arr) {
204
+ if (arr.length === 0) return false;
205
+
206
+ // All must be plain objects
207
+ if (!arr.every(item => this.isPlainObject(item))) {
208
+ return false;
209
+ }
210
+
211
+ // Get keys from first object
212
+ const firstKeys = Object.keys(arr[0]).sort();
213
+
214
+ // All objects must have same keys
215
+ if (!arr.every(item => {
216
+ const keys = Object.keys(item).sort();
217
+ return keys.length === firstKeys.length &&
218
+ keys.every((key, i) => key === firstKeys[i]);
219
+ })) {
220
+ return false;
221
+ }
222
+
223
+ // All values must be primitives (not nested objects/arrays)
224
+ return arr.every(item =>
225
+ Object.values(item).every(val =>
226
+ val === null ||
227
+ typeof val === 'string' ||
228
+ typeof val === 'number' ||
229
+ typeof val === 'boolean'
230
+ )
231
+ );
232
+ }
233
+
234
+ /**
235
+ * Check if all array elements are primitives
236
+ */
237
+ isPrimitiveArray(arr) {
238
+ return arr.every(item =>
239
+ item === null ||
240
+ typeof item === 'string' ||
241
+ typeof item === 'number' ||
242
+ typeof item === 'boolean'
243
+ );
244
+ }
245
+
246
+ /**
247
+ * Check if value is plain object
248
+ */
249
+ isPlainObject(val) {
250
+ return val !== null &&
251
+ typeof val === 'object' &&
252
+ !Array.isArray(val) &&
253
+ Object.getPrototypeOf(val) === Object.prototype;
254
+ }
255
+
256
+ /**
257
+ * Encode tabular array (Spec: Arrays of Objects)
258
+ */
259
+ encodeTabularArray(arr, depth = 0) {
260
+ const prefix = this.indentChar.repeat(depth);
261
+ const keys = Object.keys(arr[0]).sort();
262
+ const length = arr.length;
263
+
264
+ // Build length marker
265
+ const delimiterSuffix = this.delimiter === ',' ? '' : this.delimiter;
266
+ const lengthPrefix = this.lengthMarker ? `#${length}` : `${length}`;
267
+
268
+ // Header: items[N]{field1,field2}:
269
+ const fieldList = keys.join(this.delimiter);
270
+ const header = `[${lengthPrefix}${delimiterSuffix}]{${fieldList}}:`;
271
+
272
+ const lines = [prefix + header];
273
+
274
+ // Data rows
275
+ for (const item of arr) {
276
+ const values = keys.map(key => {
277
+ const val = this.normalizeValue(item[key]);
278
+ if (val === null) return 'null';
279
+ if (typeof val === 'boolean') return String(val);
280
+ if (typeof val === 'number') return this.encodeNumber(val);
281
+ if (typeof val === 'string') return this.encodeString(val);
282
+ return 'null';
283
+ });
284
+
285
+ lines.push(prefix + this.indentChar + values.join(this.delimiter));
286
+ }
287
+
288
+ return lines.join('\n');
289
+ }
290
+
291
+ /**
292
+ * Encode primitive array (Spec: Primitive Arrays)
293
+ */
294
+ encodePrimitiveArray(arr) {
295
+ const length = arr.length;
296
+ const delimiterSuffix = this.delimiter === ',' ? '' : this.delimiter;
297
+ const lengthPrefix = this.lengthMarker ? `#${length}` : `${length}`;
298
+
299
+ const values = arr.map(item => {
300
+ const val = this.normalizeValue(item);
301
+ if (val === null) return 'null';
302
+ if (typeof val === 'boolean') return String(val);
303
+ if (typeof val === 'number') return this.encodeNumber(val);
304
+ if (typeof val === 'string') return this.encodeString(val);
305
+ return 'null';
306
+ });
307
+
308
+ return `[${lengthPrefix}${delimiterSuffix}]: ${values.join(this.delimiter)}`;
309
+ }
310
+
311
+ /**
312
+ * Encode list array (Spec: Mixed and Non-Uniform Arrays)
313
+ */
314
+ encodeListArray(arr, depth = 0) {
315
+ const prefix = this.indentChar.repeat(depth);
316
+ const length = arr.length;
317
+ const delimiterSuffix = this.delimiter === ',' ? '' : this.delimiter;
318
+ const lengthPrefix = this.lengthMarker ? `#${length}` : `${length}`;
319
+
320
+ const lines = [`${prefix}[${lengthPrefix}${delimiterSuffix}]:`];
321
+
322
+ for (const item of arr) {
323
+ const normalized = this.normalizeValue(item);
324
+
325
+ if (this.isPlainObject(normalized)) {
326
+ // Object in list: first field on hyphen line
327
+ const keys = Object.keys(normalized);
328
+ if (keys.length > 0) {
329
+ const firstKey = keys[0];
330
+ const quotedKey = this.needsQuotingAsKey(firstKey) ? `"${firstKey}"` : firstKey;
331
+ const firstValue = this.encodeValue(normalized[firstKey], depth + 1);
332
+
333
+ lines.push(`${prefix}${this.indentChar}- ${quotedKey}: ${firstValue}`);
334
+
335
+ // Remaining fields at same indentation
336
+ for (let i = 1; i < keys.length; i++) {
337
+ const key = keys[i];
338
+ const quotedK = this.needsQuotingAsKey(key) ? `"${key}"` : key;
339
+ const val = this.encodeValue(normalized[key], depth + 1);
340
+ lines.push(`${prefix}${this.indentChar} ${quotedK}: ${val}`);
341
+ }
342
+ }
343
+ } else if (Array.isArray(normalized)) {
344
+ // Array in list
345
+ const encoded = this.encodeArray(normalized, depth + 1);
346
+ lines.push(`${prefix}${this.indentChar}- ${encoded}`);
347
+ } else {
348
+ // Primitive in list
349
+ const encoded = this.encodeValue(normalized, depth + 1);
350
+ lines.push(`${prefix}${this.indentChar}- ${encoded}`);
351
+ }
352
+ }
353
+
354
+ return lines.join('\n');
355
+ }
356
+
357
+ /**
358
+ * Decode TOON string to JavaScript value (Spec v1.3)
359
+ */
360
+ decode(toonString, options = {}) {
361
+ const strict = options.strict !== false;
362
+ const lines = toonString.split('\n');
363
+
364
+ return this.parseValue(lines, 0, 0).value;
365
+ }
366
+
367
+ /**
368
+ * Parse value from lines starting at index
369
+ */
370
+ parseValue(lines, startIndex, expectedDepth) {
371
+ const line = lines[startIndex];
372
+
373
+ if (!line) {
374
+ return { value: null, nextIndex: startIndex + 1 };
375
+ }
376
+
377
+ const trimmed = line.trim();
378
+
379
+ // Empty line
380
+ if (trimmed === '') {
381
+ return { value: null, nextIndex: startIndex + 1 };
382
+ }
383
+
384
+ // Check for key-value pair
385
+ if (trimmed.includes(':')) {
386
+ const colonIndex = trimmed.indexOf(':');
387
+ const key = trimmed.substring(0, colonIndex).trim();
388
+ const valueStr = trimmed.substring(colonIndex + 1).trim();
389
+
390
+ // Check if it's an array header
391
+ if (key.includes('[') && key.includes(']')) {
392
+ return this.parseArray(lines, startIndex, expectedDepth);
393
+ }
394
+
395
+ // Check if value is on same line
396
+ if (valueStr) {
397
+ return {
398
+ value: { [this.unquoteKey(key)]: this.parsePrimitive(valueStr) },
399
+ nextIndex: startIndex + 1
400
+ };
401
+ }
402
+
403
+ // Value is on next lines (nested object)
404
+ const result = this.parseObject(lines, startIndex, expectedDepth);
405
+ return result;
406
+ }
407
+
408
+ // List item
409
+ if (trimmed.startsWith('- ')) {
410
+ return this.parseListItem(lines, startIndex, expectedDepth);
411
+ }
412
+
413
+ // Primitive value
414
+ return {
415
+ value: this.parsePrimitive(trimmed),
416
+ nextIndex: startIndex + 1
417
+ };
418
+ }
419
+
420
+ /**
421
+ * Parse array from TOON format
422
+ */
423
+ parseArray(lines, startIndex, depth) {
424
+ const line = lines[startIndex];
425
+ const match = line.match(/\[([#]?\d+)([,\t|])?\]\{([^}]+)\}:/);
426
+
427
+ if (match) {
428
+ // Tabular array
429
+ const lengthStr = match[1].replace('#', '');
430
+ const expectedLength = parseInt(lengthStr);
431
+ const delimiter = match[2] || ',';
432
+ const fields = match[3].split(delimiter);
433
+
434
+ const items = [];
435
+ let index = startIndex + 1;
436
+
437
+ for (let i = 0; i < expectedLength && index < lines.length; i++) {
438
+ const dataLine = lines[index];
439
+ const values = this.splitByDelimiter(dataLine.trim(), delimiter);
440
+
441
+ const obj = {};
442
+ fields.forEach((field, idx) => {
443
+ obj[field] = this.parsePrimitive(values[idx] || 'null');
444
+ });
445
+
446
+ items.push(obj);
447
+ index++;
448
+ }
449
+
450
+ return { value: items, nextIndex: index };
451
+ }
452
+
453
+ // Primitive or list array
454
+ const primitiveMatch = line.match(/\[([#]?\d+)([,\t|])?\]: (.+)/);
455
+ if (primitiveMatch) {
456
+ // Primitive array
457
+ const delimiter = primitiveMatch[2] || ',';
458
+ const valueStr = primitiveMatch[3];
459
+ const values = this.splitByDelimiter(valueStr, delimiter);
460
+
461
+ return {
462
+ value: values.map(v => this.parsePrimitive(v)),
463
+ nextIndex: startIndex + 1
464
+ };
465
+ }
466
+
467
+ // List array
468
+ return this.parseListArray(lines, startIndex, depth);
469
+ }
470
+
471
+ /**
472
+ * Parse list-format array
473
+ */
474
+ parseListArray(lines, startIndex, depth) {
475
+ const items = [];
476
+ let index = startIndex + 1;
477
+
478
+ while (index < lines.length) {
479
+ const line = lines[index];
480
+ const currentDepth = this.getIndentDepth(line);
481
+
482
+ if (currentDepth <= depth) break;
483
+
484
+ if (line.trim().startsWith('- ')) {
485
+ const result = this.parseListItem(lines, index, depth + 1);
486
+ items.push(result.value);
487
+ index = result.nextIndex;
488
+ } else {
489
+ break;
490
+ }
491
+ }
492
+
493
+ return { value: items, nextIndex: index };
494
+ }
495
+
496
+ /**
497
+ * Parse list item
498
+ */
499
+ parseListItem(lines, startIndex, depth) {
500
+ const line = lines[startIndex];
501
+ const content = line.trim().substring(2); // Remove "- "
502
+
503
+ if (content.includes(':')) {
504
+ // Object
505
+ const result = this.parseObject(lines, startIndex, depth);
506
+ return result;
507
+ }
508
+
509
+ // Primitive
510
+ return {
511
+ value: this.parsePrimitive(content),
512
+ nextIndex: startIndex + 1
513
+ };
514
+ }
515
+
516
+ /**
517
+ * Parse object from lines
518
+ */
519
+ parseObject(lines, startIndex, depth) {
520
+ const obj = {};
521
+ let index = startIndex;
522
+
523
+ while (index < lines.length) {
524
+ const line = lines[index];
525
+ const trimmed = line.trim();
526
+ const currentDepth = this.getIndentDepth(line);
527
+
528
+ if (currentDepth < depth && index !== startIndex) break;
529
+ if (!trimmed || !trimmed.includes(':')) {
530
+ index++;
531
+ continue;
532
+ }
533
+
534
+ const colonIndex = trimmed.indexOf(':');
535
+ const key = trimmed.substring(0, colonIndex).trim();
536
+ const valueStr = trimmed.substring(colonIndex + 1).trim();
537
+
538
+ const unquotedKey = this.unquoteKey(key);
539
+
540
+ if (valueStr) {
541
+ // Value on same line
542
+ obj[unquotedKey] = this.parsePrimitive(valueStr);
543
+ index++;
544
+ } else {
545
+ // Value on next lines
546
+ const result = this.parseValue(lines, index + 1, depth + 1);
547
+ obj[unquotedKey] = result.value;
548
+ index = result.nextIndex;
549
+ }
550
+ }
551
+
552
+ return { value: obj, nextIndex: index };
553
+ }
554
+
555
+ /**
556
+ * Parse primitive value
557
+ */
558
+ parsePrimitive(str) {
559
+ const trimmed = str.trim();
560
+
561
+ if (trimmed === 'null') return null;
562
+ if (trimmed === 'true') return true;
563
+ if (trimmed === 'false') return false;
564
+
565
+ // Number
566
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
567
+ return parseFloat(trimmed);
568
+ }
569
+
570
+ // Quoted string
571
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
572
+ return this.unescapeString(trimmed.slice(1, -1));
573
+ }
574
+
575
+ // Unquoted string
576
+ return trimmed;
577
+ }
578
+
579
+ /**
580
+ * Unescape string
581
+ */
582
+ unescapeString(str) {
583
+ return str
584
+ .replace(/\\n/g, '\n')
585
+ .replace(/\\r/g, '\r')
586
+ .replace(/\\t/g, '\t')
587
+ .replace(/\\"/g, '"')
588
+ .replace(/\\\\/g, '\\');
589
+ }
590
+
591
+ /**
592
+ * Unquote key
593
+ */
594
+ unquoteKey(key) {
595
+ if (key.startsWith('"') && key.endsWith('"')) {
596
+ return this.unescapeString(key.slice(1, -1));
597
+ }
598
+ return key;
599
+ }
600
+
601
+ /**
602
+ * Get indentation depth
603
+ */
604
+ getIndentDepth(line) {
605
+ const match = line.match(/^( *)/);
606
+ return match ? Math.floor(match[1].length / this.indent) : 0;
607
+ }
608
+
609
+ /**
610
+ * Split by delimiter (respecting quotes)
611
+ */
612
+ splitByDelimiter(str, delimiter) {
613
+ const parts = [];
614
+ let current = '';
615
+ let inQuotes = false;
616
+
617
+ for (let i = 0; i < str.length; i++) {
618
+ const char = str[i];
619
+ const nextChar = str[i + 1];
620
+
621
+ if (char === '\\' && inQuotes) {
622
+ current += char + (nextChar || '');
623
+ i++; // Skip next char
624
+ continue;
625
+ }
626
+
627
+ if (char === '"') {
628
+ inQuotes = !inQuotes;
629
+ current += char;
630
+ continue;
631
+ }
632
+
633
+ if (char === delimiter && !inQuotes) {
634
+ parts.push(current.trim());
635
+ current = '';
636
+ continue;
637
+ }
638
+
639
+ current += char;
640
+ }
641
+
642
+ if (current) {
643
+ parts.push(current.trim());
644
+ }
645
+
646
+ return parts;
647
+ }
648
+
649
+ /**
650
+ * Validate TOON string (enhanced)
651
+ */
652
+ validate(toonString) {
653
+ const errors = [];
654
+
655
+ // Check balanced braces/brackets
656
+ let braceDepth = 0;
657
+ let bracketDepth = 0;
658
+
659
+ for (const char of toonString) {
660
+ if (char === '{') braceDepth++;
661
+ if (char === '}') braceDepth--;
662
+ if (char === '[') bracketDepth++;
663
+ if (char === ']') bracketDepth--;
664
+
665
+ if (braceDepth < 0) errors.push('Unbalanced braces: extra closing brace');
666
+ if (bracketDepth < 0) errors.push('Unbalanced brackets: extra closing bracket');
667
+ }
668
+
669
+ if (braceDepth > 0) errors.push('Unbalanced braces: missing closing braces');
670
+ if (bracketDepth > 0) errors.push('Unbalanced brackets: missing closing brackets');
671
+
672
+ return {
673
+ valid: errors.length === 0,
674
+ errors
675
+ };
676
+ }
677
+
678
+ /**
679
+ * Estimate tokens (GPT-4 compatible)
680
+ */
681
+ estimateTokens(toonString) {
682
+ // Simple estimation: ~4 chars per token (GPT-4 average)
683
+ return Math.ceil(toonString.length / 4);
684
+ }
685
+
686
+ /**
687
+ * Optimize TOON output
688
+ */
689
+ optimize(toonString) {
690
+ const lines = toonString.split('\n');
691
+ const optimized = lines.map(line => line.trimEnd()).join('\n');
692
+ return optimized.replace(/\n{3,}/g, '\n\n');
693
+ }
694
+
695
+ /**
696
+ * Minify TOON output
697
+ */
698
+ minify(toonString) {
699
+ return toonString
700
+ .split('\n')
701
+ .map(line => line.trim())
702
+ .filter(line => line.length > 0)
703
+ .join('\n');
704
+ }
705
+
706
+ /**
707
+ * Compare with JSON (token savings)
708
+ */
709
+ compareWithJSON(data) {
710
+ const toon = this.encode(data);
711
+ const json = JSON.stringify(data, null, 2);
712
+
713
+ const toonSize = toon.length;
714
+ const jsonSize = json.length;
715
+ const savings = jsonSize - toonSize;
716
+ const savingsPercentage = ((savings / jsonSize) * 100).toFixed(1);
717
+
718
+ return {
719
+ toon,
720
+ json,
721
+ toonSize,
722
+ jsonSize,
723
+ savings,
724
+ savingsPercentage: parseFloat(savingsPercentage),
725
+ toonTokens: this.estimateTokens(toon),
726
+ jsonTokens: this.estimateTokens(json)
727
+ };
728
+ }
729
+ }
730
+
731
+ export default ToonFormatterV13;