@bendyline/squisq 1.5.2 → 2.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 (263) hide show
  1. package/README.md +63 -10
  2. package/dist/{Doc-BBVGq1_9.d.ts → Doc-ylqWKDc_.d.ts} +248 -37
  3. package/dist/{ImageEditDoc-FuyGtt6o.d.ts → ImageEditDoc-D828IXJ1.d.ts} +1 -1
  4. package/dist/annotationCoercion-Dr_DY9Hx.d.ts +86 -0
  5. package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
  6. package/dist/chunk-3E5F2XMR.js.map +1 -0
  7. package/dist/chunk-4X3JQXNM.js +35 -0
  8. package/dist/chunk-4X3JQXNM.js.map +1 -0
  9. package/dist/{chunk-P3K7NLHF.js → chunk-5DKBFXPY.js} +31 -121
  10. package/dist/chunk-5DKBFXPY.js.map +1 -0
  11. package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
  12. package/dist/chunk-5J4HKRJF.js.map +1 -0
  13. package/dist/chunk-5UYI3RJB.js +2503 -0
  14. package/dist/chunk-5UYI3RJB.js.map +1 -0
  15. package/dist/{chunk-BEQODJWV.js → chunk-66QO7XFB.js} +11 -291
  16. package/dist/chunk-66QO7XFB.js.map +1 -0
  17. package/dist/{chunk-KKNUBQ6Y.js → chunk-6PJRB2CF.js} +156 -91
  18. package/dist/chunk-6PJRB2CF.js.map +1 -0
  19. package/dist/{chunk-HWVFJAAH.js → chunk-AMIR72JP.js} +28 -10
  20. package/dist/chunk-AMIR72JP.js.map +1 -0
  21. package/dist/chunk-FVJUUTEM.js +74 -0
  22. package/dist/chunk-FVJUUTEM.js.map +1 -0
  23. package/dist/{chunk-5TYS5V3G.js → chunk-GF3BVNLU.js} +4127 -3538
  24. package/dist/chunk-GF3BVNLU.js.map +1 -0
  25. package/dist/{chunk-Q3ROPT5H.js → chunk-GZY66SDN.js} +1 -1
  26. package/dist/{chunk-Q3ROPT5H.js.map → chunk-GZY66SDN.js.map} +1 -1
  27. package/dist/chunk-JOQYPRDZ.js +127 -0
  28. package/dist/chunk-JOQYPRDZ.js.map +1 -0
  29. package/dist/{chunk-A4O7GIWE.js → chunk-LYNJEOAC.js} +10 -73
  30. package/dist/chunk-LYNJEOAC.js.map +1 -0
  31. package/dist/{chunk-LDQ2HJIX.js → chunk-PXLVXTLW.js} +252 -54
  32. package/dist/chunk-PXLVXTLW.js.map +1 -0
  33. package/dist/{chunk-TQWLI6S2.js → chunk-Q7LROV2H.js} +4 -2
  34. package/dist/{chunk-TQWLI6S2.js.map → chunk-Q7LROV2H.js.map} +1 -1
  35. package/dist/{chunk-T5UK6YOB.js → chunk-QTSJOY54.js} +60 -17
  36. package/dist/chunk-QTSJOY54.js.map +1 -0
  37. package/dist/chunk-QVANWDPH.js +2358 -0
  38. package/dist/chunk-QVANWDPH.js.map +1 -0
  39. package/dist/{chunk-GPLTCSXO.js → chunk-R2GC6RLD.js} +7 -7
  40. package/dist/chunk-R2GC6RLD.js.map +1 -0
  41. package/dist/chunk-R2YZTCLO.js +292 -0
  42. package/dist/chunk-R2YZTCLO.js.map +1 -0
  43. package/dist/chunk-S7A5D7ZJ.js +174 -0
  44. package/dist/chunk-S7A5D7ZJ.js.map +1 -0
  45. package/dist/chunk-T3TI7GSZ.js +884 -0
  46. package/dist/chunk-T3TI7GSZ.js.map +1 -0
  47. package/dist/{chunk-LTN6I7QW.js → chunk-TUWMK62B.js} +55 -4
  48. package/dist/chunk-TUWMK62B.js.map +1 -0
  49. package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
  50. package/dist/chunk-VR7UPJ7C.js.map +1 -0
  51. package/dist/doc/index.d.ts +2334 -5
  52. package/dist/doc/index.js +127 -33
  53. package/dist/generate/index.d.ts +7 -2
  54. package/dist/generate/index.js +1 -1
  55. package/dist/imageEdit/index.d.ts +9 -6
  56. package/dist/imageEdit/index.js +3 -3
  57. package/dist/index.d.ts +12 -10
  58. package/dist/index.js +246 -80
  59. package/dist/jsonForm/index.d.ts +7 -3
  60. package/dist/jsonForm/index.js +9 -4
  61. package/dist/markdown/index.d.ts +6 -70
  62. package/dist/markdown/index.js +13 -9
  63. package/dist/narration/index.d.ts +618 -0
  64. package/dist/narration/index.js +82 -0
  65. package/dist/recommend/index.d.ts +15 -1
  66. package/dist/recommend/index.js +2 -2
  67. package/dist/schemas/index.d.ts +28 -6
  68. package/dist/schemas/index.js +15 -13
  69. package/dist/spatial/index.d.ts +0 -1
  70. package/dist/spatial/index.js +1 -1
  71. package/dist/storage/index.js +1 -1
  72. package/dist/{themeLibrary-CehcJhzz.d.ts → themeLibrary-RjVgLpUi.d.ts} +10 -11
  73. package/dist/timing/index.js +5 -3
  74. package/dist/transform/index.d.ts +35 -24
  75. package/dist/transform/index.js +9 -10
  76. package/dist/{types-DlAZ7MW4.d.ts → types-BwMPl2CB.d.ts} +1 -1
  77. package/dist/{types-D98li1gV.d.ts → types-C4kiRn_k.d.ts} +3 -1
  78. package/dist/versions/index.d.ts +2 -1
  79. package/dist/versions/index.js +2 -2
  80. package/package.json +6 -6
  81. package/src/__tests__/applyNarrationTiming.test.ts +190 -0
  82. package/src/__tests__/applyRenderStyle.test.ts +3 -3
  83. package/src/__tests__/asciiDiagramBattle.test.ts +246 -0
  84. package/src/__tests__/asciiDiagramDetect.test.ts +162 -0
  85. package/src/__tests__/asciiDiagramMapping.test.ts +38 -0
  86. package/src/__tests__/asciiDiagramParse.test.ts +233 -0
  87. package/src/__tests__/asciiDiagramPipeline.test.ts +273 -0
  88. package/src/__tests__/asciiDiagramRender.test.ts +162 -0
  89. package/src/__tests__/asciiDiagramRepair.test.ts +84 -0
  90. package/src/__tests__/asciiTimelineDetect.test.ts +157 -0
  91. package/src/__tests__/asciiTimelineParse.test.ts +205 -0
  92. package/src/__tests__/asciiTimelinePipeline.test.ts +349 -0
  93. package/src/__tests__/asciiTimelineRender.test.ts +376 -0
  94. package/src/__tests__/attrTokens.test.ts +5 -0
  95. package/src/__tests__/contentContainer.test.ts +43 -0
  96. package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -15
  97. package/src/__tests__/dataTable.test.ts +25 -15
  98. package/src/__tests__/diagramBlock.test.ts +3 -3
  99. package/src/__tests__/diagramText.test.ts +26 -0
  100. package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
  101. package/src/__tests__/drawingBlock.test.ts +3 -3
  102. package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
  103. package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
  104. package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
  105. package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
  106. package/src/__tests__/getLayers.test.ts +32 -18
  107. package/src/__tests__/getLayersFallback.test.ts +8 -8
  108. package/src/__tests__/imageEditVersions.test.ts +2 -0
  109. package/src/__tests__/jsonForm.pointer.test.ts +47 -1
  110. package/src/__tests__/layoutBlock.test.ts +3 -3
  111. package/src/__tests__/majorApiTypes.test.ts +18 -0
  112. package/src/__tests__/markdown.test.ts +41 -0
  113. package/src/__tests__/matchFontFamily.test.ts +114 -0
  114. package/src/__tests__/materializeBlockLayers.test.ts +135 -0
  115. package/src/__tests__/materializeTestUtils.ts +17 -0
  116. package/src/__tests__/narrationAlign.test.ts +150 -0
  117. package/src/__tests__/narrationFeatures.test.ts +71 -0
  118. package/src/__tests__/narrationNuclei.test.ts +45 -0
  119. package/src/__tests__/narrationPacing.test.ts +189 -0
  120. package/src/__tests__/narrationScript.test.ts +149 -0
  121. package/src/__tests__/narrationSessionRepro.test.ts +62 -0
  122. package/src/__tests__/narrationSidecar.test.ts +88 -0
  123. package/src/__tests__/narrationSyllables.test.ts +65 -0
  124. package/src/__tests__/narrationTestSignals.ts +167 -0
  125. package/src/__tests__/narrationVad.test.ts +66 -0
  126. package/src/__tests__/resolveDocTheme.test.ts +16 -2
  127. package/src/__tests__/rootExports.test.ts +18 -0
  128. package/src/__tests__/scopedContentContainer.test.ts +20 -0
  129. package/src/__tests__/shapeGeometry.test.ts +12 -0
  130. package/src/__tests__/templateAnnotationParse.test.ts +10 -0
  131. package/src/__tests__/templateInputs.test.ts +69 -0
  132. package/src/__tests__/templateMetadata.test.ts +2 -1
  133. package/src/__tests__/templates.test.ts +143 -17
  134. package/src/__tests__/themeCompile.test.ts +49 -22
  135. package/src/__tests__/transformProvenance.test.ts +160 -0
  136. package/src/__tests__/transformV2.test.ts +40 -14
  137. package/src/__tests__/treeviewBattle.test.ts +76 -0
  138. package/src/__tests__/treeviewDetect.test.ts +168 -0
  139. package/src/__tests__/treeviewParse.test.ts +118 -0
  140. package/src/__tests__/treeviewPipeline.test.ts +183 -0
  141. package/src/__tests__/treeviewRender.test.ts +91 -0
  142. package/src/__tests__/versions.test.ts +44 -0
  143. package/src/doc/applyNarrationTiming.ts +244 -0
  144. package/src/doc/asciiDiagram/chars.ts +210 -0
  145. package/src/doc/asciiDiagram/detect.ts +175 -0
  146. package/src/doc/asciiDiagram/index.ts +38 -0
  147. package/src/doc/asciiDiagram/mapping.ts +225 -0
  148. package/src/doc/asciiDiagram/parse.ts +1095 -0
  149. package/src/doc/asciiDiagram/render.ts +963 -0
  150. package/src/doc/asciiDiagram/repair.ts +400 -0
  151. package/src/doc/asciiDiagram/types.ts +65 -0
  152. package/src/doc/asciiTimeline/detect.ts +138 -0
  153. package/src/doc/asciiTimeline/index.ts +26 -0
  154. package/src/doc/asciiTimeline/mapping.ts +43 -0
  155. package/src/doc/asciiTimeline/parse.ts +870 -0
  156. package/src/doc/asciiTimeline/render.ts +427 -0
  157. package/src/doc/asciiTimeline/types.ts +73 -0
  158. package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
  159. package/src/doc/audioMapping.ts +60 -46
  160. package/src/doc/customTemplatesFrontmatter.ts +0 -41
  161. package/src/doc/docToMarkdown.ts +70 -2
  162. package/src/doc/index.ts +76 -4
  163. package/src/doc/markdownToDoc.ts +135 -1
  164. package/src/doc/materializeBlockLayers.ts +304 -0
  165. package/src/doc/mediaAnnotations.ts +20 -0
  166. package/src/doc/resolveDocTheme.ts +8 -7
  167. package/src/doc/templateInputs.ts +185 -2
  168. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
  169. package/src/doc/templates/diagramBlock.ts +237 -38
  170. package/src/doc/templates/fallbackBlock.ts +1 -1
  171. package/src/doc/templates/index.ts +134 -279
  172. package/src/doc/templates/inputDescriptors.ts +20 -1
  173. package/src/doc/templates/metadata.ts +10 -0
  174. package/src/doc/templates/persistentLayers.ts +12 -126
  175. package/src/doc/templates/registry.ts +88 -0
  176. package/src/doc/templates/templateNames.ts +22 -0
  177. package/src/doc/templates/timelineBlock.ts +613 -0
  178. package/src/doc/templates/treeBlock.ts +131 -0
  179. package/src/doc/treeview/chars.ts +72 -0
  180. package/src/doc/treeview/detect.ts +145 -0
  181. package/src/doc/treeview/index.ts +30 -0
  182. package/src/doc/treeview/mapping.ts +95 -0
  183. package/src/doc/treeview/parse.ts +201 -0
  184. package/src/doc/treeview/render.ts +41 -0
  185. package/src/doc/treeview/types.ts +43 -0
  186. package/src/doc/utils/applyRenderStyle.ts +2 -9
  187. package/src/doc/utils/diagramText.ts +78 -0
  188. package/src/doc/utils/shapeGeometry.ts +67 -5
  189. package/src/doc/validate.ts +1 -1
  190. package/src/generate/templateMapper.ts +7 -0
  191. package/src/imageEdit/export.ts +6 -5
  192. package/src/imageEdit/versionPaths.ts +13 -26
  193. package/src/imageEdit/versions.ts +21 -100
  194. package/src/index.ts +1 -0
  195. package/src/internal/immutable.ts +58 -0
  196. package/src/jsonForm/index.ts +9 -1
  197. package/src/jsonForm/pointer.ts +68 -23
  198. package/src/markdown/attrTokens.ts +7 -5
  199. package/src/markdown/index.ts +1 -0
  200. package/src/markdown/stringify.ts +4 -9
  201. package/src/markdown/utils.ts +36 -14
  202. package/src/narration/align.ts +514 -0
  203. package/src/narration/features.ts +231 -0
  204. package/src/narration/index.ts +86 -0
  205. package/src/narration/nuclei.ts +118 -0
  206. package/src/narration/pacing.ts +211 -0
  207. package/src/narration/script.ts +239 -0
  208. package/src/narration/session.ts +100 -0
  209. package/src/narration/sidecar.ts +187 -0
  210. package/src/narration/syllables.ts +85 -0
  211. package/src/narration/trace.ts +50 -0
  212. package/src/narration/types.ts +299 -0
  213. package/src/narration/vad.ts +131 -0
  214. package/src/recommend/numberHighlight.ts +25 -0
  215. package/src/recommend/templates.ts +108 -12
  216. package/src/schemas/BlockTemplates.ts +156 -7
  217. package/src/schemas/Doc.ts +72 -9
  218. package/src/schemas/Media.ts +12 -2
  219. package/src/schemas/Theme.ts +45 -25
  220. package/src/schemas/Viewport.ts +2 -2
  221. package/src/schemas/fontStacks.ts +79 -0
  222. package/src/schemas/themeCompile.ts +15 -0
  223. package/src/schemas/themeConstants.ts +2 -0
  224. package/src/schemas/themeLibrary.ts +25 -19
  225. package/src/schemas/themeValidator.ts +1 -1
  226. package/src/spatial/Geohash.ts +0 -3
  227. package/src/storage/ContentContainer.ts +17 -9
  228. package/src/storage/ScopedContentContainer.ts +45 -15
  229. package/src/transform/applyTransform.ts +5 -5
  230. package/src/transform/index.ts +3 -1
  231. package/src/transform/registry.ts +169 -49
  232. package/src/transform/templateSelector.ts +21 -2
  233. package/src/transform/timingAllocator.ts +152 -21
  234. package/src/transform/types.ts +14 -1
  235. package/src/versions/gitignore.ts +34 -0
  236. package/src/versions/index.ts +2 -0
  237. package/src/versions/operations.ts +18 -104
  238. package/src/versions/paths.ts +13 -28
  239. package/src/versions/snapshotStore.ts +202 -0
  240. package/src/versions/types.ts +3 -1
  241. package/dist/chunk-5TYS5V3G.js.map +0 -1
  242. package/dist/chunk-A4O7GIWE.js.map +0 -1
  243. package/dist/chunk-BEQODJWV.js.map +0 -1
  244. package/dist/chunk-DLXZMS5K.js.map +0 -1
  245. package/dist/chunk-GPLTCSXO.js.map +0 -1
  246. package/dist/chunk-HWVFJAAH.js.map +0 -1
  247. package/dist/chunk-IMSCRLLV.js.map +0 -1
  248. package/dist/chunk-JELIRVDP.js +0 -24
  249. package/dist/chunk-JELIRVDP.js.map +0 -1
  250. package/dist/chunk-KKNUBQ6Y.js.map +0 -1
  251. package/dist/chunk-LDQ2HJIX.js.map +0 -1
  252. package/dist/chunk-LTN6I7QW.js.map +0 -1
  253. package/dist/chunk-P3K7NLHF.js.map +0 -1
  254. package/dist/chunk-T5UK6YOB.js.map +0 -1
  255. package/dist/chunk-UY7KGQ5R.js +0 -192
  256. package/dist/chunk-UY7KGQ5R.js.map +0 -1
  257. package/dist/chunk-VLJJHDAC.js +0 -218
  258. package/dist/chunk-VLJJHDAC.js.map +0 -1
  259. package/dist/chunk-VWUFZ6ZG.js.map +0 -1
  260. package/dist/story/index.d.ts +0 -1801
  261. package/dist/story/index.js +0 -279
  262. package/src/doc/getLayers.ts +0 -195
  263. /package/dist/{story → narration}/index.js.map +0 -0
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ repairAsciiDiagram,
4
+ isRepairableDiagram,
5
+ detectAsciiDiagram,
6
+ parseAsciiDiagram,
7
+ } from '../doc/asciiDiagram/index.js';
8
+ import { POSITIVE_FIXTURES } from './fixtures/asciiDiagrams.js';
9
+
10
+ /**
11
+ * Broken box art: package-name labels overflow their boxes and collide with
12
+ * neighbours, so the columns desync row-to-row. Conservative detection
13
+ * declines it (`garbled-labels`) — the opt-in repair pass reconstructs it.
14
+ * (A shorter second label line keeps a real `│` on one interior row, which is
15
+ * how real "half-broken" art still gives the tracer border evidence.)
16
+ */
17
+ const BROKEN = [
18
+ '┌──────────┐ ┌──────────┐',
19
+ '│ @scope/alpha-svc │ │ @scope/beta-svc │',
20
+ '│ one │ │ two │',
21
+ '└────┬─────┘ └────┬─────┘',
22
+ ' │ │',
23
+ ' ▼ ▼',
24
+ '┌────────────────────────────┐',
25
+ '│ @scope/gamma-store │',
26
+ '│ persists │',
27
+ '└────────────────────────────┘',
28
+ ].join('\n');
29
+
30
+ describe('isRepairableDiagram', () => {
31
+ it('is true for broken box art that conservative detection declines', () => {
32
+ expect(detectAsciiDiagram(BROKEN).isDiagram).toBe(false);
33
+ expect(isRepairableDiagram(BROKEN)).toBe(true);
34
+ });
35
+
36
+ it('is false for a clean diagram (the canvas already handles it)', () => {
37
+ expect(detectAsciiDiagram(POSITIVE_FIXTURES.TWO_BOX_VERTICAL).isDiagram).toBe(true);
38
+ expect(isRepairableDiagram(POSITIVE_FIXTURES.TWO_BOX_VERTICAL)).toBe(false);
39
+ });
40
+
41
+ it('is false for non-box art (prose, file trees)', () => {
42
+ expect(isRepairableDiagram('just some\nplain prose\nwith no boxes')).toBe(false);
43
+ expect(isRepairableDiagram('src/\n├── index.ts\n└── utils/')).toBe(false);
44
+ });
45
+ });
46
+
47
+ describe('repairAsciiDiagram', () => {
48
+ it('recovers clean labels from overflowing/colliding boxes (band-segment matching)', () => {
49
+ const r = repairAsciiDiagram(BROKEN);
50
+ expect(r).not.toBeNull();
51
+ const firstLines = r!.diagram.nodes.map((n) => n.label.split('\n')[0]);
52
+ expect(firstLines).toEqual(['@scope/alpha-svc', '@scope/beta-svc', '@scope/gamma-store']);
53
+ // Hyphens/slashes in labels survive (they're not box-drawing chars).
54
+ expect(firstLines.every((l) => l.includes('/') && l.includes('-'))).toBe(true);
55
+ });
56
+
57
+ it('recovers the fan-in edges', () => {
58
+ const r = repairAsciiDiagram(BROKEN)!;
59
+ const edgeSet = new Set(r.diagram.edges.map((e) => `${e.source}->${e.target}`));
60
+ expect(edgeSet.has('scope-alpha-svc->scope-gamma-store')).toBe(true);
61
+ expect(edgeSet.has('scope-beta-svc->scope-gamma-store')).toBe(true);
62
+ // No phantom edge between the two horizontally-adjacent source boxes.
63
+ expect(edgeSet.has('scope-alpha-svc->scope-beta-svc')).toBe(false);
64
+ });
65
+
66
+ it('produces art that re-parses cleanly as a diagram (repair is a real fixpoint entry)', () => {
67
+ const r = repairAsciiDiagram(BROKEN)!;
68
+ const redetect = detectAsciiDiagram(r.art);
69
+ expect(redetect.isDiagram).toBe(true);
70
+ // Same box count, and labels are no longer garbled.
71
+ const reparsed = parseAsciiDiagram(r.art);
72
+ expect(reparsed.nodes.length).toBe(r.diagram.nodes.length);
73
+ expect(redetect.reasons.join(' ')).not.toMatch(/garbled/);
74
+ });
75
+
76
+ it('returns null when fewer than two boxes can be recovered', () => {
77
+ expect(repairAsciiDiagram('just prose\nno boxes here')).toBeNull();
78
+ expect(repairAsciiDiagram('┌───┐\n│ A │\n└───┘')).toBeNull();
79
+ });
80
+
81
+ it('marks the diagram as repaired', () => {
82
+ expect(repairAsciiDiagram(BROKEN)!.diagram.warnings).toContain('repaired');
83
+ });
84
+ });
@@ -0,0 +1,157 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ detectAsciiTimeline,
4
+ isAsciiTimelineFence,
5
+ isEligibleAsciiTimelineFenceLang,
6
+ isExplicitTimelineLang,
7
+ renderAsciiTimeline,
8
+ type AsciiTimeline,
9
+ } from '../doc/asciiTimeline/index.js';
10
+ import { detectAsciiDiagram } from '../doc/asciiDiagram/index.js';
11
+ import { detectTree } from '../doc/treeview/index.js';
12
+ import type { MarkdownCodeBlock } from '../markdown/types.js';
13
+ import {
14
+ MULTI_TRACK_BRANCH_TIMELINE,
15
+ SINGLE_POINT_ASCII_TIMELINE,
16
+ TICK_INTERPOLATION_TIMELINE,
17
+ WRAPPED_CLIENT_KERNEL_TIMELINE,
18
+ } from './fixtures/asciiTimelines.js';
19
+
20
+ describe('detectAsciiTimeline', () => {
21
+ it('recognizes the exact tick/interpolation example as an untagged timeline', () => {
22
+ const detection = detectAsciiTimeline(TICK_INTERPOLATION_TIMELINE);
23
+ expect(detection.isTimeline).toBe(true);
24
+ expect(detection.timeline?.tracks).toHaveLength(2);
25
+ expect(detection.reasons).toContain('strong-markers');
26
+ });
27
+
28
+ it('recognizes multiple tracks and an explicit branch declaration', () => {
29
+ const detection = detectAsciiTimeline(MULTI_TRACK_BRANCH_TIMELINE);
30
+ expect(detection.isTimeline).toBe(true);
31
+ expect(detection.timeline?.tracks).toHaveLength(2);
32
+ expect(detection.timeline?.links).toEqual([
33
+ { source: 't29', target: 'f29', label: 'interpolation path' },
34
+ ]);
35
+ });
36
+
37
+ it('conservatively recognizes a multi-lane flow with a wrapped return edge', () => {
38
+ const detection = detectAsciiTimeline(WRAPPED_CLIENT_KERNEL_TIMELINE);
39
+ expect(detection.isTimeline).toBe(true);
40
+ expect(detection.reasons).toContain('wrapped-flow');
41
+ expect(detection.timeline?.tracks).toHaveLength(2);
42
+ expect(detectAsciiDiagram(WRAPPED_CLIENT_KERNEL_TIMELINE).isDiagram).toBe(false);
43
+
44
+ const withoutReturn = WRAPPED_CLIENT_KERNEL_TIMELINE.replace(/┐/gu, '│').replace(/┘/gu, '│');
45
+ expect(detectAsciiTimeline(withoutReturn).isTimeline).toBe(false);
46
+ });
47
+
48
+ it('only accepts a sparse ASCII track when author intent is explicit', () => {
49
+ expect(detectAsciiTimeline(SINGLE_POINT_ASCII_TIMELINE).isTimeline).toBe(false);
50
+ const explicit = detectAsciiTimeline(SINGLE_POINT_ASCII_TIMELINE, { explicit: true });
51
+ expect(explicit.isTimeline).toBe(true);
52
+ expect(explicit.timeline?.tracks[0].events).toMatchObject([
53
+ { id: 'alpha', label: 'Alpha', marker: 'filled' },
54
+ ]);
55
+ });
56
+
57
+ it('accepts a cadence-only track through an explicit timeline fence', () => {
58
+ const cadence = 'client frames (120 Hz): f f f f f';
59
+ expect(detectAsciiTimeline(cadence).isTimeline).toBe(false);
60
+ expect(detectAsciiTimeline(cadence, { explicit: true }).isTimeline).toBe(true);
61
+ const fence: MarkdownCodeBlock = { type: 'code', lang: 'timeline', value: cadence };
62
+ expect(isAsciiTimelineFence(fence)).toBe(true);
63
+ });
64
+
65
+ it('keeps the exact sample mutually exclusive with tree and box-diagram codecs', () => {
66
+ expect(detectTree(TICK_INTERPOLATION_TIMELINE).isTree).toBe(false);
67
+ expect(detectAsciiDiagram(TICK_INTERPOLATION_TIMELINE).isDiagram).toBe(false);
68
+ });
69
+
70
+ it('does not steal a tree item whose label happens to contain a marker rail', () => {
71
+ const tree = ['root/', '├── ●────────● benchmark.txt', '└── notes.md'].join('\n');
72
+ expect(detectAsciiTimeline(tree).isTimeline).toBe(false);
73
+ expect(detectTree(tree).isTree).toBe(true);
74
+ });
75
+
76
+ it('does not steal marker rails inside deeply nested tree items', () => {
77
+ const tree = [
78
+ 'root/',
79
+ '│ └── child/',
80
+ '│ └── grandchild/',
81
+ '│ └── ●────────● benchmark.txt',
82
+ ].join('\n');
83
+ expect(detectAsciiTimeline(tree).isTimeline).toBe(false);
84
+ expect(detectTree(tree).isTree).toBe(true);
85
+ });
86
+
87
+ it('leaves a single closed box as authored code instead of treating its top as an axis', () => {
88
+ const box = ['┌──●────●──┐', '│ content │', '└──────────┘'].join('\n');
89
+ expect(detectAsciiTimeline(box).isTimeline).toBe(false);
90
+ });
91
+
92
+ it('allows a lone top elbow to act as an above-axis callout', () => {
93
+ const callout = [' ┌─ warmup', '────●────●────►'].join('\n');
94
+ expect(detectAsciiTimeline(callout).isTimeline).toBe(true);
95
+ });
96
+
97
+ it('uses an inert-language allowlist and a durable explicit tag', () => {
98
+ for (const lang of [undefined, null, '', 'text', 'txt', 'plain', 'ascii', 'timeline']) {
99
+ expect(isEligibleAsciiTimelineFenceLang(lang)).toBe(true);
100
+ }
101
+ for (const lang of ['js', 'typescript', 'python', 'yaml', 'json', 'bash']) {
102
+ expect(isEligibleAsciiTimelineFenceLang(lang)).toBe(false);
103
+ }
104
+ for (const lang of ['timeline', ' Timeline ', 'TIMELINE']) {
105
+ expect(isExplicitTimelineLang(lang)).toBe(true);
106
+ }
107
+ expect(isExplicitTimelineLang('text')).toBe(false);
108
+ });
109
+
110
+ it('combines the language gate with explicit detection for code fences', () => {
111
+ const fence = (lang?: string): MarkdownCodeBlock => ({
112
+ type: 'code',
113
+ ...(lang !== undefined ? { lang } : {}),
114
+ value: SINGLE_POINT_ASCII_TIMELINE,
115
+ });
116
+ expect(isAsciiTimelineFence(fence())).toBe(false);
117
+ expect(isAsciiTimelineFence(fence('timeline'))).toBe(true);
118
+ expect(isAsciiTimelineFence(fence('js'))).toBe(false);
119
+ });
120
+
121
+ it('rejects over-wide art before running the grid parsers', () => {
122
+ const tooWide = `●${'─'.repeat(401)}●`;
123
+ expect(detectAsciiTimeline(tooWide)).toMatchObject({
124
+ isTimeline: false,
125
+ reasons: [expect.stringMatching(/^too-wide\(/)],
126
+ });
127
+ });
128
+
129
+ it('allows long canonical callout form while retaining the ordinary line cap', () => {
130
+ const timeline: AsciiTimeline = {
131
+ tracks: Array.from({ length: 150 }, (_, index) => ({
132
+ id: `track-${index}`,
133
+ label: `Track ${index}`,
134
+ row: index,
135
+ startColumn: 0,
136
+ endColumn: 10,
137
+ events: [
138
+ { id: `start-${index}`, label: 'Start', column: 0, side: 'above' as const },
139
+ { id: `end-${index}`, label: 'End', column: 10, side: 'below' as const },
140
+ ],
141
+ })),
142
+ links: [],
143
+ width: 11,
144
+ height: 150,
145
+ style: 'unicode',
146
+ warnings: [],
147
+ };
148
+ const canonical = renderAsciiTimeline(timeline);
149
+ expect(canonical.split('\n').length).toBeGreaterThan(400);
150
+ expect(detectAsciiTimeline(canonical, { explicit: true }).isTimeline).toBe(true);
151
+
152
+ const arbitrary = Array.from({ length: 401 }, () => '●────────●').join('\n');
153
+ expect(detectAsciiTimeline(arbitrary, { explicit: true }).reasons[0]).toMatch(
154
+ /^too-many-lines\(/,
155
+ );
156
+ });
157
+ });
@@ -0,0 +1,205 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { asciiTimelineToTemplateData, parseAsciiTimeline } from '../doc/asciiTimeline/index.js';
3
+ import {
4
+ MULTI_TRACK_BRANCH_TIMELINE,
5
+ TICK_INTERPOLATION_TIMELINE,
6
+ WRAPPED_CLIENT_KERNEL_TIMELINE,
7
+ } from './fixtures/asciiTimelines.js';
8
+
9
+ describe('parseAsciiTimeline', () => {
10
+ it('extracts tick labels, delta callouts, and fractional pointer events from the exact sample', () => {
11
+ const timeline = parseAsciiTimeline(TICK_INTERPOLATION_TIMELINE);
12
+ expect(timeline.tracks).toHaveLength(2);
13
+
14
+ const [kernel, client] = timeline.tracks;
15
+ expect(kernel.label).toBe('kernel ticks (30 Hz)');
16
+ expect(kernel.endLabel).toBe('sim time');
17
+ expect(client.label).toBe('client frames (e.g. 120 Hz)');
18
+
19
+ const events = kernel.events;
20
+ const milestones = events.filter((event) => event.marker === 'filled');
21
+ expect(milestones.map((event) => event.label)).toEqual(['T28', 'T29', 'T30', 'T31']);
22
+ expect(milestones.map((event) => event.description)).toEqual([
23
+ 'deltas sent: Δ28',
24
+ 'Δ29',
25
+ 'Δ30',
26
+ 'Δ31',
27
+ ]);
28
+ expect(milestones.map((event) => event.side)).toEqual(['above', 'above', 'above', 'above']);
29
+ expect(milestones.map((event) => event.descriptionSide)).toEqual([
30
+ 'below',
31
+ 'below',
32
+ 'below',
33
+ 'below',
34
+ ]);
35
+
36
+ const cadence = client.events.filter((event) => event.callout === false);
37
+ expect(cadence).toHaveLength(16);
38
+ expect(cadence.every((event) => event.marker === 'hollow')).toBe(true);
39
+ expect(client.events).toEqual(
40
+ expect.arrayContaining([
41
+ expect.objectContaining({
42
+ label: 'estimated kernel clock',
43
+ description: 'estTick ≈ 30.4',
44
+ marker: 'hollow',
45
+ }),
46
+ expect.objectContaining({
47
+ label: 'render position',
48
+ description: expect.stringContaining('interpolate between snapshot T28 and T29'),
49
+ marker: 'hollow',
50
+ }),
51
+ ]),
52
+ );
53
+ });
54
+
55
+ it('parses multiple tracks and resolves explicit branch declarations by stable event id', () => {
56
+ const timeline = parseAsciiTimeline(MULTI_TRACK_BRANCH_TIMELINE);
57
+ expect(timeline.tracks.map((track) => track.label)).toEqual(['Kernel', 'Client']);
58
+ expect(timeline.tracks.map((track) => track.events.map((event) => event.id))).toEqual([
59
+ ['t28', 't29'],
60
+ ['f28', 'f29'],
61
+ ]);
62
+ expect(timeline.links).toEqual([{ source: 't29', target: 'f29', label: 'interpolation path' }]);
63
+ expect(timeline.warnings).toEqual([]);
64
+ });
65
+
66
+ it('maps a wrapped client/kernel flow into tracks, events, and a return branch', () => {
67
+ const timeline = parseAsciiTimeline(WRAPPED_CLIENT_KERNEL_TIMELINE);
68
+ expect(timeline.warnings).toEqual([]);
69
+ expect(timeline.tracks.map((track) => track.label)).toEqual([
70
+ 'CLIENT (main thread)',
71
+ 'KERNEL (Web Worker / Node)',
72
+ ]);
73
+
74
+ const events = timeline.tracks.flatMap((track) => track.events);
75
+ expect(events.map((event) => event.label)).toEqual(
76
+ expect.arrayContaining([
77
+ 'input events',
78
+ 'InputMap',
79
+ 'command',
80
+ 'validate (schema)',
81
+ 'command-rejected event',
82
+ 'queue at tick T',
83
+ 'dirty-tracked per-tick delta',
84
+ 'delta {tick T}',
85
+ 'interpolation buffer',
86
+ 'keyframe',
87
+ 'error surfaced to user/agent',
88
+ ]),
89
+ );
90
+ expect(events.find((event) => event.id === 'interpolation-buffer')?.description).toBe(
91
+ '(render ~1.5 ticks behind, lerp/slerp)',
92
+ );
93
+ expect(timeline.tracks[0].events.map((event) => event.id)).toEqual(
94
+ expect.arrayContaining(['input-events', 'inputmap', 'command', 'delta-tick-t', 'keyframe']),
95
+ );
96
+ expect(timeline.links).toEqual(
97
+ expect.arrayContaining([
98
+ expect.objectContaining({ source: 'command', target: 'validate-schema' }),
99
+ expect.objectContaining({ source: 'dirty-tracked-per-tick-delta', target: 'delta-tick-t' }),
100
+ expect.objectContaining({
101
+ source: 'command-rejected-event',
102
+ target: 'error-surfaced-to-user-agent',
103
+ }),
104
+ ]),
105
+ );
106
+ });
107
+
108
+ it('normalizes every track against one shared horizontal scale', () => {
109
+ const timeline = parseAsciiTimeline(MULTI_TRACK_BRANCH_TIMELINE);
110
+ const { tracks, links } = asciiTimelineToTemplateData(timeline);
111
+ expect(tracks).toHaveLength(2);
112
+ expect(tracks[0].events.map((event) => event.position)).toEqual(
113
+ tracks[1].events.map((event) => event.position),
114
+ );
115
+ for (const event of tracks.flatMap((track) => track.events)) {
116
+ expect(event.position).toBeGreaterThanOrEqual(0);
117
+ expect(event.position).toBeLessThanOrEqual(1);
118
+ }
119
+ expect(links).toEqual(timeline.links);
120
+ });
121
+
122
+ it('is deterministic and never throws on empty or malformed input', () => {
123
+ expect(parseAsciiTimeline('')).toMatchObject({ tracks: [], links: [], warnings: [] });
124
+ expect(() => parseAsciiTimeline('not a timeline\nfunction x() {}')).not.toThrow();
125
+ expect(parseAsciiTimeline(MULTI_TRACK_BRANCH_TIMELINE)).toEqual(
126
+ parseAsciiTimeline(MULTI_TRACK_BRANCH_TIMELINE.replace(/\n/g, '\r\n')),
127
+ );
128
+ });
129
+
130
+ it('keeps prose arrows and double hyphens inside inline event descriptions', () => {
131
+ const timeline = parseAsciiTimeline(
132
+ 'Flow: ● Compare :: A > B; release--candidate {#compare side=above} ───►',
133
+ );
134
+
135
+ expect(timeline.tracks[0].events[0]).toMatchObject({
136
+ id: 'compare',
137
+ label: 'Compare',
138
+ description: 'A > B; release--candidate',
139
+ });
140
+ });
141
+
142
+ it('keeps leading CLI flags, negative values, and rail-like prose in authored labels', () => {
143
+ const timeline = parseAsciiTimeline('Flags: ● --watch ─────● -1.5 ─────● ─phase ─────►');
144
+
145
+ expect(timeline.tracks[0].events.map((event) => event.label)).toEqual([
146
+ '--watch',
147
+ '-1.5',
148
+ '─phase',
149
+ ]);
150
+ });
151
+
152
+ it('keeps direct-source arrow prose, pointer offsets, and end-label punctuation', () => {
153
+ const arrow = parseAsciiTimeline('Flow: ●────● Result :: A -> B');
154
+ expect(arrow.tracks[0].events[1].description).toBe('A -> B');
155
+
156
+ const pointer = parseAsciiTimeline(['●────────●────────►', ' ▲ -1.5'].join('\n'));
157
+ expect(pointer.tracks[0].events.map((event) => event.label)).toContain('-1.5');
158
+
159
+ const endLabel = parseAsciiTimeline('Track: ● A ─────● B ───► --phase');
160
+ expect(endLabel.tracks[0].endLabel).toBe('--phase');
161
+ });
162
+
163
+ it('does not duplicate a suffix-only axis label at the left edge', () => {
164
+ const track = parseAsciiTimeline('────●────●────► sim time').tracks[0];
165
+ expect(track.label).toBeUndefined();
166
+ expect(track.endLabel).toBe('sim time');
167
+ });
168
+
169
+ it('promotes a cadence point when an authored elbow adds a callout', () => {
170
+ const timeline = parseAsciiTimeline(['Client: f f f f', ' └─ dropped frame'].join('\n'));
171
+ expect(timeline.tracks[0].events).toEqual(
172
+ expect.arrayContaining([
173
+ expect.objectContaining({ label: 'dropped frame', callout: true, side: 'below' }),
174
+ ]),
175
+ );
176
+ });
177
+
178
+ it('preserves a leading minus in an elbow callout', () => {
179
+ const timeline = parseAsciiTimeline(['●────────●────────►', '└─-1.5'].join('\n'));
180
+ expect(timeline.tracks[0].events[0].description).toBe('-1.5');
181
+ });
182
+
183
+ it('keeps trailing prose braces when they are not timeline metadata', () => {
184
+ const timeline = parseAsciiTimeline('Flow: ● Deploy {phase} ───►');
185
+ expect(timeline.tracks[0].events[0].label).toBe('Deploy {phase}');
186
+ });
187
+
188
+ it('segments multiple fractional pointers on the same row', () => {
189
+ const timeline = parseAsciiTimeline(
190
+ ['●────────●────────●────────►', ' ▲ A ▲ B'].join('\n'),
191
+ );
192
+ const pointerLabels = timeline.tracks[0].events
193
+ .filter((event) => event.marker === 'hollow')
194
+ .map((event) => event.label);
195
+ expect(pointerLabels).toEqual(['A', 'B']);
196
+ });
197
+
198
+ it('does not turn hyphenated Unicode event labels into phantom ASCII markers', () => {
199
+ const timeline = parseAsciiTimeline('Flow: ● Go-live {#go} ─────● Done {#done} ─────►');
200
+
201
+ expect(timeline.style).toBe('unicode');
202
+ expect(timeline.tracks[0].events.map((event) => event.label)).toEqual(['Go-live', 'Done']);
203
+ expect(timeline.tracks[0].events.map((event) => event.id)).toEqual(['go', 'done']);
204
+ });
205
+ });