ctxman 3.0.0 → 3.1.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 (224) hide show
  1. package/.ctxman/llm-profiles.json +1067 -39
  2. package/.ctxman/profiles/backend-team.json +13 -0
  3. package/.ctxman/profiles/default.json +12 -0
  4. package/.ctxman/profiles/devops-team.json +21 -0
  5. package/.ctxman/profiles/frontend-team.json +20 -0
  6. package/.ctxman/templates/bug-fix.json +17 -0
  7. package/.ctxman/templates/code-review.json +9 -0
  8. package/.ctxman/templates/documentation.json +9 -0
  9. package/.ctxman/templates/feature.json +9 -0
  10. package/.ctxman/templates/full-context.json +9 -0
  11. package/.ctxman/templates/refactoring.json +18 -0
  12. package/.ctxman/templates/testing.json +9 -0
  13. package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
  14. package/LICENSE +21 -0
  15. package/README.md +381 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -67
  18. package/bin/cm-audit.js +121 -0
  19. package/bin/cm-count.js +151 -0
  20. package/bin/cm-gitingest.js +128 -115
  21. package/bin/cm-map.js +242 -0
  22. package/bin/cm-mcp.js +293 -0
  23. package/bin/cm-pack.js +405 -0
  24. package/bin/cm-policy.js +170 -0
  25. package/bin/cm-pr.js +248 -0
  26. package/bin/cm-search.js +178 -0
  27. package/bin/cm-update.js +128 -128
  28. package/bin/cm-why.js +197 -0
  29. package/bin/mcp-server.js +8 -24
  30. package/ctxman.js +237 -183
  31. package/examples/README.md +19 -4
  32. package/examples/github-actions/ctxman-check.yml +49 -0
  33. package/examples/wizard-profiles/security-audit/profile.json +7 -1
  34. package/index.d.ts +1422 -0
  35. package/index.js +55 -31
  36. package/lib/agents/pr-failure-agent.js +48 -45
  37. package/lib/analyzers/ai-suggester.js +451 -0
  38. package/lib/analyzers/dependency-scanner.js +505 -0
  39. package/lib/analyzers/duplicate-detector.js +264 -0
  40. package/lib/analyzers/go-method-analyzer.js +151 -110
  41. package/lib/analyzers/import-tracker.js +431 -0
  42. package/lib/analyzers/method-analyzer.js +1055 -434
  43. package/lib/analyzers/performance-dashboard.js +687 -0
  44. package/lib/analyzers/token-calculator.js +895 -504
  45. package/lib/analyzers/unused-exports-detector.js +336 -0
  46. package/lib/api/mcp/install.js +424 -0
  47. package/lib/api/mcp/prompts.js +281 -281
  48. package/lib/api/mcp/resources.js +300 -241
  49. package/lib/api/mcp/sandbox.js +89 -0
  50. package/lib/api/mcp/server.js +772 -312
  51. package/lib/api/mcp/start.js +125 -0
  52. package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
  53. package/lib/api/rest/server.js +105 -28
  54. package/lib/cache/CacheManager.js +9 -17
  55. package/lib/core/Analyzer.js +16 -10
  56. package/lib/core/ContextBuilder.js +19 -28
  57. package/lib/core/Reporter.js +15 -14
  58. package/lib/core/Scanner.js +81 -12
  59. package/lib/count/index.js +100 -0
  60. package/lib/formatters/format-registry.js +394 -372
  61. package/lib/formatters/gitingest-formatter.js +656 -621
  62. package/lib/formatters/toon-formatter-v1.3.js +637 -632
  63. package/lib/formatters/toon-formatter.js +239 -240
  64. package/lib/integrations/git/BlameTracker.js +4 -4
  65. package/lib/integrations/git/DiffAnalyzer.js +38 -31
  66. package/lib/integrations/git/GitClient.js +71 -53
  67. package/lib/lsp/server.js +311 -0
  68. package/lib/map/graph.js +299 -0
  69. package/lib/map/imports.js +233 -0
  70. package/lib/map/index.js +33 -0
  71. package/lib/map/rank.js +253 -0
  72. package/lib/map/render.js +346 -0
  73. package/lib/map/resolver.js +574 -0
  74. package/lib/pack/budget.js +111 -0
  75. package/lib/pack/code-lexer.js +360 -0
  76. package/lib/pack/compress.js +414 -0
  77. package/lib/pack/index.js +354 -0
  78. package/lib/pack/renderers/common.js +134 -0
  79. package/lib/pack/renderers/index.js +24 -0
  80. package/lib/pack/renderers/json.js +47 -0
  81. package/lib/pack/renderers/markdown.js +54 -0
  82. package/lib/pack/renderers/plain.js +38 -0
  83. package/lib/pack/renderers/xml.js +85 -0
  84. package/lib/pack/select.js +177 -0
  85. package/lib/pack/split.js +182 -0
  86. package/lib/pack/tree.js +35 -0
  87. package/lib/parsers/gitignore-parser.js +116 -107
  88. package/lib/parsers/method-filter-parser.js +50 -33
  89. package/lib/plugins/ExporterPlugin.js +3 -3
  90. package/lib/plugins/LanguagePlugin.js +3 -3
  91. package/lib/plugins/PluginManager.js +7 -13
  92. package/lib/policy/audit.js +312 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +53 -0
  96. package/lib/policy/policy.js +489 -0
  97. package/lib/pr/diff-parser.js +267 -0
  98. package/lib/pr/method-mapper.js +148 -0
  99. package/lib/pr/pr-context.js +829 -0
  100. package/lib/pr/pr-git.js +149 -0
  101. package/lib/pr/pr-renderer.js +644 -0
  102. package/lib/pr/related-tests.js +296 -0
  103. package/lib/rag/DataSourcePlugin.js +5 -5
  104. package/lib/rag/EmbeddingProvider.js +19 -8
  105. package/lib/rag/Indexer.js +14 -5
  106. package/lib/rag/VectorStoreFactory.js +3 -3
  107. package/lib/rag/ask.js +122 -0
  108. package/lib/rag/sources/FileDataSource.js +12 -9
  109. package/lib/rag/stores/BaseVectorStore.js +5 -5
  110. package/lib/rag/stores/ChromaDBStore.js +10 -10
  111. package/lib/rag/stores/LanceDBStore.js +29 -19
  112. package/lib/rag/stores/PineconeStore.js +11 -9
  113. package/lib/search/bm25.js +433 -0
  114. package/lib/search/code-units.js +201 -0
  115. package/lib/search/symbols.js +189 -0
  116. package/lib/ui/dashboard.js +120 -89
  117. package/lib/ui/index.js +1 -6
  118. package/lib/ui/init-wizard-ui.js +550 -0
  119. package/lib/ui/progress-bar.js +94 -88
  120. package/lib/ui/select-input.js +40 -32
  121. package/lib/ui/template-selector.js +99 -0
  122. package/lib/ui/wizard.js +374 -329
  123. package/lib/utils/claude-token-counter.js +151 -0
  124. package/lib/utils/cli-output.js +17 -0
  125. package/lib/utils/clipboard-utils.js +49 -50
  126. package/lib/utils/config-utils.js +81 -66
  127. package/lib/utils/context-versioning.js +532 -0
  128. package/lib/utils/error-handler.js +112 -112
  129. package/lib/utils/file-utils.js +153 -69
  130. package/lib/utils/format-converter.js +356 -283
  131. package/lib/utils/git-utils.js +366 -294
  132. package/lib/utils/llm-cost-estimator.js +281 -0
  133. package/lib/utils/llm-detector.js +242 -78
  134. package/lib/utils/logger.js +246 -237
  135. package/lib/utils/messages.js +355 -0
  136. package/lib/utils/multi-repo-manager.js +364 -0
  137. package/lib/utils/profile-manager.js +380 -0
  138. package/lib/utils/safe-read.js +61 -0
  139. package/lib/utils/secret-redactor.js +61 -0
  140. package/lib/utils/snapshot-manager.js +679 -0
  141. package/lib/utils/template-manager.js +378 -0
  142. package/lib/utils/token-utils.js +201 -170
  143. package/lib/utils/tokenizer-adapter.js +421 -377
  144. package/lib/utils/updater.js +539 -506
  145. package/lib/watch/FileWatcher.js +12 -8
  146. package/lib/watch/IncrementalAnalyzer.js +7 -7
  147. package/lib/why/coupling.js +81 -0
  148. package/lib/why/errors.js +23 -0
  149. package/lib/why/git.js +169 -0
  150. package/lib/why/history.js +108 -0
  151. package/lib/why/index.js +720 -0
  152. package/lib/why/ownership.js +79 -0
  153. package/lib/why/paths.js +207 -0
  154. package/lib/why/references.js +259 -0
  155. package/lib/why/render.js +722 -0
  156. package/lib/why/search.js +358 -0
  157. package/lib/why/target.js +389 -0
  158. package/lib/wizards/config-generator.js +257 -0
  159. package/lib/wizards/project-detector.js +572 -0
  160. package/package.json +44 -10
  161. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  224. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
@@ -10,316 +10,389 @@
10
10
  * - JSON ↔ Markdown
11
11
  */
12
12
 
13
+ import { decode as decodeToon } from '@toon-format/toon';
13
14
  import FormatRegistry from '../formatters/format-registry.js';
14
15
  import ToonFormatter from '../formatters/toon-formatter.js';
15
16
  import fs from 'fs';
16
17
  import path from 'path';
17
18
 
18
19
  class FormatConverter {
19
- constructor() {
20
- this.registry = new FormatRegistry();
21
- this.toonFormatter = new ToonFormatter();
20
+ constructor() {
21
+ this.registry = new FormatRegistry();
22
+ this.toonFormatter = new ToonFormatter();
23
+ }
24
+
25
+ /**
26
+ * Convert from one format to another
27
+ * @param {string} input - Input data as string
28
+ * @param {string} fromFormat - Source format
29
+ * @param {string} toFormat - Target format
30
+ * @returns {Object} Conversion result with output and metadata
31
+ */
32
+ convert(input, fromFormat, toFormat) {
33
+ // Step 1: Parse input to JavaScript object
34
+ const data = this.parse(input, fromFormat);
35
+
36
+ // Step 2: Encode to target format
37
+ const output = this.encode(data, toFormat);
38
+
39
+ // Step 3: Calculate metadata
40
+ const inputSize = input.length;
41
+ const outputSize = output.length;
42
+ const savings = inputSize - outputSize;
43
+ const savingsPercentage = inputSize > 0 ? ((savings / inputSize) * 100).toFixed(1) : '0.0';
44
+
45
+ return {
46
+ output,
47
+ metadata: {
48
+ inputSize,
49
+ outputSize,
50
+ savings,
51
+ savingsPercentage: parseFloat(savingsPercentage),
52
+ fromFormat,
53
+ toFormat,
54
+ },
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Parse string input to JavaScript object
60
+ */
61
+ parse(input, format) {
62
+ switch (format.toLowerCase()) {
63
+ case 'json':
64
+ case 'json-compact':
65
+ return JSON.parse(input);
66
+
67
+ case 'yaml':
68
+ return this.parseYAML(input);
69
+
70
+ case 'toon':
71
+ return decodeToon(input);
72
+
73
+ case 'xml':
74
+ throw new Error('XML parsing not yet implemented. Use JSON as intermediate format.');
75
+
76
+ case 'csv':
77
+ return this.parseCSV(input);
78
+
79
+ case 'markdown':
80
+ throw new Error('Markdown parsing not yet implemented. Use JSON as intermediate format.');
81
+
82
+ default:
83
+ throw new Error(`Unknown format for parsing: ${format}`);
22
84
  }
23
-
24
- /**
25
- * Convert from one format to another
26
- * @param {string} input - Input data as string
27
- * @param {string} fromFormat - Source format
28
- * @param {string} toFormat - Target format
29
- * @returns {Object} Conversion result with output and metadata
30
- */
31
- convert(input, fromFormat, toFormat) {
32
- // Step 1: Parse input to JavaScript object
33
- const data = this.parse(input, fromFormat);
34
-
35
- // Step 2: Encode to target format
36
- const output = this.encode(data, toFormat);
37
-
38
- // Step 3: Calculate metadata
39
- const inputSize = input.length;
40
- const outputSize = output.length;
41
- const savings = inputSize - outputSize;
42
- const savingsPercentage = inputSize > 0 ? ((savings / inputSize) * 100).toFixed(1) : '0.0';
43
-
44
- return {
45
- output,
46
- metadata: {
47
- inputSize,
48
- outputSize,
49
- savings,
50
- savingsPercentage: parseFloat(savingsPercentage),
51
- fromFormat,
52
- toFormat
53
- }
54
- };
85
+ }
86
+
87
+ /**
88
+ * Encode JavaScript object to format
89
+ */
90
+ encode(data, format) {
91
+ try {
92
+ return this.registry.encode(format, data);
93
+ } catch (error) {
94
+ throw new Error(`Failed to encode to ${format}: ${error.message}`);
55
95
  }
56
-
57
- /**
58
- * Parse string input to JavaScript object
59
- */
60
- parse(input, format) {
61
- switch (format.toLowerCase()) {
62
- case 'json':
63
- case 'json-compact':
64
- return JSON.parse(input);
65
-
66
- case 'yaml':
67
- return this.parseYAML(input);
68
-
69
- case 'toon':
70
- throw new Error('TOON parsing not yet implemented. Use JSON as intermediate format.');
71
-
72
- case 'xml':
73
- throw new Error('XML parsing not yet implemented. Use JSON as intermediate format.');
74
-
75
- case 'csv':
76
- return this.parseCSV(input);
77
-
78
- case 'markdown':
79
- throw new Error('Markdown parsing not yet implemented. Use JSON as intermediate format.');
80
-
81
- default:
82
- throw new Error(`Unknown format for parsing: ${format}`);
83
- }
96
+ }
97
+
98
+ /**
99
+ * Simple YAML parser for block-style mappings and sequences (what encodeYAML emits)
100
+ * Note: This is a basic implementation. Flow collections other than {} and [],
101
+ * compact "- key: value" items, anchors and block scalars are not supported.
102
+ */
103
+ parseYAML(yamlString) {
104
+ const lines = yamlString
105
+ .split('\n')
106
+ .filter((line) => line.trim() && !line.trim().startsWith('#'))
107
+ .map((line) => ({ indent: line.length - line.trimStart().length, text: line.trim() }));
108
+
109
+ if (lines.length === 0) return {};
110
+
111
+ const state = { pos: 0 };
112
+ const first = lines[0];
113
+ return this.isYAMLSequenceItem(first.text)
114
+ ? this.parseYAMLSequence(lines, state, first.indent)
115
+ : this.parseYAMLMapping(lines, state, first.indent);
116
+ }
117
+
118
+ /**
119
+ * Parse the mapping whose keys sit at `indent`, starting at state.pos
120
+ */
121
+ parseYAMLMapping(lines, state, indent) {
122
+ const result = {};
123
+
124
+ while (state.pos < lines.length) {
125
+ const line = lines[state.pos];
126
+ if (line.indent < indent) break;
127
+
128
+ state.pos++;
129
+ // Lines that are not a key at this level are skipped, as before
130
+ const isKeyLine = line.indent === indent && !this.isYAMLSequenceItem(line.text);
131
+ const entry = isKeyLine ? this.splitYAMLKey(line.text) : null;
132
+ if (!entry) continue;
133
+
134
+ if (entry.value !== '') {
135
+ result[entry.key] = this.parseYAMLValue(entry.value);
136
+ continue;
137
+ }
138
+
139
+ // An empty value introduces a nested block: deeper lines, or a sequence at the same indent
140
+ const next = lines[state.pos];
141
+ if (next && next.indent > indent) {
142
+ result[entry.key] = this.isYAMLSequenceItem(next.text)
143
+ ? this.parseYAMLSequence(lines, state, next.indent)
144
+ : this.parseYAMLMapping(lines, state, next.indent);
145
+ } else if (next && next.indent === indent && this.isYAMLSequenceItem(next.text)) {
146
+ result[entry.key] = this.parseYAMLSequence(lines, state, indent);
147
+ } else {
148
+ result[entry.key] = null;
149
+ }
84
150
  }
85
151
 
86
- /**
87
- * Encode JavaScript object to format
88
- */
89
- encode(data, format) {
90
- try {
91
- return this.registry.encode(format, data);
92
- } catch (error) {
93
- throw new Error(`Failed to encode to ${format}: ${error.message}`);
94
- }
152
+ return result;
153
+ }
154
+
155
+ /**
156
+ * Parse the sequence whose "-" markers sit at `indent`, starting at state.pos
157
+ */
158
+ parseYAMLSequence(lines, state, indent) {
159
+ const result = [];
160
+
161
+ while (state.pos < lines.length) {
162
+ const line = lines[state.pos];
163
+ if (line.indent < indent) break;
164
+ if (line.indent > indent) {
165
+ state.pos++;
166
+ continue;
167
+ }
168
+ if (!this.isYAMLSequenceItem(line.text)) break;
169
+
170
+ state.pos++;
171
+ const value = line.text.substring(1).trim();
172
+ if (value !== '') {
173
+ result.push(this.parseYAMLValue(value));
174
+ continue;
175
+ }
176
+
177
+ // "-" alone: the item is the block on the following, deeper lines
178
+ const next = lines[state.pos];
179
+ if (next && next.indent > indent) {
180
+ result.push(
181
+ this.isYAMLSequenceItem(next.text)
182
+ ? this.parseYAMLSequence(lines, state, next.indent)
183
+ : this.parseYAMLMapping(lines, state, next.indent)
184
+ );
185
+ } else {
186
+ result.push(null);
187
+ }
95
188
  }
96
189
 
97
- /**
98
- * Simple YAML parser
99
- * Note: This is a basic implementation. For complex YAML, use a proper library.
100
- */
101
- parseYAML(yamlString) {
102
- const lines = yamlString.split('\n').filter(line => line.trim() && !line.trim().startsWith('#'));
103
- const result = {};
104
- let currentKey = null;
105
- let currentArray = null;
106
-
107
- for (const line of lines) {
108
- const trimmed = line.trim();
109
-
110
- // Array item
111
- if (trimmed.startsWith('- ')) {
112
- const value = trimmed.substring(2).trim();
113
-
114
- // Initialize array if needed
115
- if (currentKey && (!result[currentKey] || !Array.isArray(result[currentKey]))) {
116
- result[currentKey] = [];
117
- currentArray = result[currentKey];
118
- }
119
-
120
- if (currentArray) {
121
- currentArray.push(this.parseYAMLValue(value));
122
- }
123
- continue;
124
- }
125
-
126
- // Key-value pair
127
- const colonIndex = trimmed.indexOf(':');
128
- if (colonIndex !== -1) {
129
- const key = trimmed.substring(0, colonIndex).trim();
130
- const value = trimmed.substring(colonIndex + 1).trim();
131
-
132
- if (value === '') {
133
- // Object or array follows
134
- currentKey = key;
135
- result[key] = {}; // Default to object, will be overwritten if array items found
136
- currentArray = null; // Reset current array
137
- } else {
138
- result[key] = this.parseYAMLValue(value);
139
- currentKey = null;
140
- currentArray = null;
141
- }
142
- }
143
- }
144
-
145
- return result;
190
+ return result;
191
+ }
192
+
193
+ isYAMLSequenceItem(text) {
194
+ return text === '-' || text.startsWith('- ');
195
+ }
196
+
197
+ /**
198
+ * Split "key: value" into its parts; the key may be double-quoted
199
+ * @returns {{key: string, value: string}|null} null when the line has no key
200
+ */
201
+ splitYAMLKey(text) {
202
+ // The value is trimmed below instead of matched as `\s+(.*)`, whose two quantifiers
203
+ // overlap on spaces (polynomial backtracking)
204
+ const quoted = text.match(/^("(?:[^"\\]|\\.)*")\s*:(.*)$/s);
205
+ if (quoted) {
206
+ return { key: JSON.parse(quoted[1]), value: quoted[2].trim() };
146
207
  }
147
208
 
148
- /**
149
- * Parse YAML value (string, number, boolean)
150
- */
151
- parseYAMLValue(value) {
152
- if (value === 'true') return true;
153
- if (value === 'false') return false;
154
- if (value === 'null') return null;
155
- if (/^-?\d+$/.test(value)) return parseInt(value, 10);
156
- if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
157
- if (value.startsWith('"') && value.endsWith('"')) {
158
- return value.substring(1, value.length - 1);
159
- }
160
- return value;
209
+ let colonIndex = text.search(/:(\s|$)/);
210
+ if (colonIndex === -1) colonIndex = text.indexOf(':');
211
+ if (colonIndex === -1) return null;
212
+
213
+ return {
214
+ key: text.substring(0, colonIndex).trim(),
215
+ value: text.substring(colonIndex + 1).trim(),
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Parse YAML value (string, number, boolean)
221
+ */
222
+ parseYAMLValue(value) {
223
+ if (value === 'true') return true;
224
+ if (value === 'false') return false;
225
+ if (value === 'null') return null;
226
+ if (value === '{}') return {};
227
+ if (value === '[]') return [];
228
+ if (/^-?\d+$/.test(value)) return parseInt(value, 10);
229
+ if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(value)) return parseFloat(value);
230
+ if (value.startsWith('"') && value.endsWith('"')) {
231
+ try {
232
+ return JSON.parse(value);
233
+ } catch {
234
+ return value.substring(1, value.length - 1);
235
+ }
161
236
  }
162
-
163
- /**
164
- * Simple CSV parser
165
- */
166
- parseCSV(csvString) {
167
- const lines = csvString.split('\n').filter(line => line.trim());
168
- if (lines.length === 0) return { rows: [] };
169
-
170
- const headers = this.parseCSVLine(lines[0]);
171
- const rows = [];
172
-
173
- for (let i = 1; i < lines.length; i++) {
174
- const values = this.parseCSVLine(lines[i]);
175
- const row = {};
176
- headers.forEach((header, index) => {
177
- row[header] = values[index] || '';
178
- });
179
- rows.push(row);
180
- }
181
-
182
- return { headers, rows };
237
+ return value;
238
+ }
239
+
240
+ /**
241
+ * Simple CSV parser
242
+ */
243
+ parseCSV(csvString) {
244
+ const lines = csvString.split('\n').filter((line) => line.trim());
245
+ if (lines.length === 0) return { rows: [] };
246
+
247
+ const headers = this.parseCSVLine(lines[0]);
248
+ const rows = [];
249
+
250
+ for (let i = 1; i < lines.length; i++) {
251
+ const values = this.parseCSVLine(lines[i]);
252
+ const row = {};
253
+ headers.forEach((header, index) => {
254
+ row[header] = values[index] || '';
255
+ });
256
+ rows.push(row);
183
257
  }
184
258
 
185
- /**
186
- * Parse CSV line (handles quoted values)
187
- */
188
- parseCSVLine(line) {
189
- const values = [];
190
- let current = '';
191
- let inQuotes = false;
192
-
193
- for (let i = 0; i < line.length; i++) {
194
- const char = line[i];
195
-
196
- if (char === '"') {
197
- inQuotes = !inQuotes;
198
- } else if (char === ',' && !inQuotes) {
199
- values.push(current.trim());
200
- current = '';
201
- } else {
202
- current += char;
203
- }
204
- }
259
+ return { headers, rows };
260
+ }
205
261
 
262
+ /**
263
+ * Parse CSV line (handles quoted values)
264
+ */
265
+ parseCSVLine(line) {
266
+ const values = [];
267
+ let current = '';
268
+ let inQuotes = false;
269
+
270
+ for (let i = 0; i < line.length; i++) {
271
+ const char = line[i];
272
+
273
+ if (char === '"') {
274
+ inQuotes = !inQuotes;
275
+ } else if (char === ',' && !inQuotes) {
206
276
  values.push(current.trim());
207
- return values;
277
+ current = '';
278
+ } else {
279
+ current += char;
280
+ }
208
281
  }
209
282
 
210
- /**
211
- * Convert file from one format to another
212
- * @param {string} inputFile - Input file path
213
- * @param {string} outputFile - Output file path
214
- * @param {string} fromFormat - Source format (auto-detect from extension if not provided)
215
- * @param {string} toFormat - Target format (auto-detect from extension if not provided)
216
- */
217
- convertFile(inputFile, outputFile, fromFormat = null, toFormat = null) {
218
- // Auto-detect formats from file extensions
219
- if (!fromFormat) {
220
- const ext = path.extname(inputFile).substring(1);
221
- fromFormat = this.extensionToFormat(ext);
222
- }
223
-
224
- if (!toFormat) {
225
- const ext = path.extname(outputFile).substring(1);
226
- toFormat = this.extensionToFormat(ext);
227
- }
228
-
229
- // Read input file
230
- const input = fs.readFileSync(inputFile, 'utf8');
231
-
232
- // Convert
233
- const result = this.convert(input, fromFormat, toFormat);
234
-
235
- // Write output file
236
- fs.writeFileSync(outputFile, result.output, 'utf8');
237
-
238
- return {
239
- inputFile,
240
- outputFile,
241
- fromFormat,
242
- toFormat,
243
- inputSize: input.length,
244
- outputSize: result.output.length,
245
- savings: input.length - result.output.length,
246
- savingsPercent: result.metadata.savingsPercentage + '%'
247
- };
283
+ values.push(current.trim());
284
+ return values;
285
+ }
286
+
287
+ /**
288
+ * Convert file from one format to another
289
+ * @param {string} inputFile - Input file path
290
+ * @param {string} outputFile - Output file path
291
+ * @param {string} fromFormat - Source format (auto-detect from extension if not provided)
292
+ * @param {string} toFormat - Target format (auto-detect from extension if not provided)
293
+ */
294
+ convertFile(inputFile, outputFile, fromFormat = null, toFormat = null) {
295
+ // Auto-detect formats from file extensions
296
+ if (!fromFormat) {
297
+ const ext = path.extname(inputFile).substring(1);
298
+ fromFormat = this.extensionToFormat(ext);
248
299
  }
249
300
 
250
- /**
251
- * Map file extension to format name
252
- */
253
- extensionToFormat(ext) {
254
- const map = {
255
- 'json': 'json',
256
- 'toon': 'toon',
257
- 'yaml': 'yaml',
258
- 'yml': 'yaml',
259
- 'csv': 'csv',
260
- 'xml': 'xml',
261
- 'md': 'markdown',
262
- 'txt': 'gitingest'
263
- };
264
-
265
- return map[ext.toLowerCase()] || 'json';
301
+ if (!toFormat) {
302
+ const ext = path.extname(outputFile).substring(1);
303
+ toFormat = this.extensionToFormat(ext);
266
304
  }
267
305
 
268
- /**
269
- * Batch convert multiple files
270
- * @param {Array} files - Array of {input, output, from, to}
271
- * @returns {Array} Conversion results
272
- */
273
- batchConvert(files) {
274
- const results = [];
275
-
276
- for (const file of files) {
277
- try {
278
- const result = this.convertFile(
279
- file.input,
280
- file.output,
281
- file.from || null,
282
- file.to || null
283
- );
284
- results.push({ success: true, ...result });
285
- } catch (error) {
286
- results.push({
287
- success: false,
288
- inputFile: file.input,
289
- error: error.message
290
- });
291
- }
292
- }
293
-
294
- return results;
306
+ // Read input file
307
+ const input = fs.readFileSync(inputFile, 'utf8');
308
+
309
+ // Convert
310
+ const result = this.convert(input, fromFormat, toFormat);
311
+
312
+ // Write output file
313
+ fs.writeFileSync(outputFile, result.output, 'utf8');
314
+
315
+ return {
316
+ inputFile,
317
+ outputFile,
318
+ fromFormat,
319
+ toFormat,
320
+ inputSize: input.length,
321
+ outputSize: result.output.length,
322
+ savings: input.length - result.output.length,
323
+ savingsPercent: result.metadata.savingsPercentage + '%',
324
+ };
325
+ }
326
+
327
+ /**
328
+ * Map file extension to format name
329
+ */
330
+ extensionToFormat(ext) {
331
+ const map = {
332
+ json: 'json',
333
+ toon: 'toon',
334
+ yaml: 'yaml',
335
+ yml: 'yaml',
336
+ csv: 'csv',
337
+ xml: 'xml',
338
+ md: 'markdown',
339
+ txt: 'gitingest',
340
+ };
341
+
342
+ return map[ext.toLowerCase()] || 'json';
343
+ }
344
+
345
+ /**
346
+ * Batch convert multiple files
347
+ * @param {Array} files - Array of {input, output, from, to}
348
+ * @returns {Array} Conversion results
349
+ */
350
+ batchConvert(files) {
351
+ const results = [];
352
+
353
+ for (const file of files) {
354
+ try {
355
+ const result = this.convertFile(
356
+ file.input,
357
+ file.output,
358
+ file.from || null,
359
+ file.to || null
360
+ );
361
+ results.push({ success: true, ...result });
362
+ } catch (error) {
363
+ results.push({
364
+ success: false,
365
+ inputFile: file.input,
366
+ error: error.message,
367
+ });
368
+ }
295
369
  }
296
370
 
297
- /**
298
- * Get supported conversions
299
- */
300
- getSupportedConversions() {
301
- return {
302
- fullySupported: [
303
- 'JSON → TOON',
304
- 'JSON → YAML',
305
- 'JSON → CSV',
306
- 'JSON → XML',
307
- 'JSON → Markdown',
308
- 'YAML → JSON (basic)',
309
- 'CSV → JSON'
310
- ],
311
- partialSupport: [
312
- 'TOON → JSON (decoder not implemented)',
313
- 'XML → JSON (parser not implemented)',
314
- 'Markdown → JSON (parser not implemented)'
315
- ],
316
- planned: [
317
- 'TOON ↔ JSON (bidirectional)',
318
- 'XML ↔ JSON (bidirectional)',
319
- 'Advanced YAML parsing'
320
- ]
321
- };
322
- }
371
+ return results;
372
+ }
373
+
374
+ /**
375
+ * Get supported conversions
376
+ */
377
+ getSupportedConversions() {
378
+ return {
379
+ fullySupported: [
380
+ 'JSON → TOON',
381
+ 'JSON → YAML',
382
+ 'JSON → CSV',
383
+ 'JSON → XML',
384
+ 'JSON → Markdown',
385
+ 'TOON → JSON',
386
+ 'YAML → JSON (basic)',
387
+ 'CSV → JSON',
388
+ ],
389
+ partialSupport: [
390
+ 'XML → JSON (parser not implemented)',
391
+ 'Markdown → JSON (parser not implemented)',
392
+ ],
393
+ planned: ['XML ↔ JSON (bidirectional)', 'Advanced YAML parsing'],
394
+ };
395
+ }
323
396
  }
324
397
 
325
398
  export default FormatConverter;