@miliastry/quasar 1.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 (172) hide show
  1. package/LICENSE +119 -0
  2. package/README.md +45 -0
  3. package/dist/Visuals/lyne.css +1027 -0
  4. package/dist/Visuals/osu.css +257 -0
  5. package/dist/index.d.mts +4563 -0
  6. package/dist/index.d.ts +4563 -0
  7. package/dist/index.js +11291 -0
  8. package/dist/index.mjs +11197 -0
  9. package/package.json +51 -0
  10. package/src/Analysis/Contracts/AnalysisReport.ts +26 -0
  11. package/src/Analysis/Contracts/Contribution.ts +76 -0
  12. package/src/Analysis/Contracts/Pass.ts +76 -0
  13. package/src/Analysis/Contracts/PipelineContext.ts +48 -0
  14. package/src/Analysis/Passes/Analysis/GradientAnalyzer.ts +292 -0
  15. package/src/Analysis/Passes/Analysis/MergeableColorAnalyzer.ts +112 -0
  16. package/src/Analysis/Passes/Analysis/RainbowAnalyzer.ts +233 -0
  17. package/src/Analysis/Passes/Analysis/WaveAnalyzer.ts +211 -0
  18. package/src/Analysis/Passes/Analysis/__tests__/GradientAnalyzer.test.ts +135 -0
  19. package/src/Analysis/Passes/Analysis/__tests__/MergeableColorAnalyzer.test.ts +84 -0
  20. package/src/Analysis/Passes/Analysis/__tests__/RainbowAnalyzer.test.ts +99 -0
  21. package/src/Analysis/Passes/Analysis/__tests__/WaveAnalyzer.test.ts +119 -0
  22. package/src/Analysis/Passes/Decision/DefaultDecision.ts +139 -0
  23. package/src/Analysis/Passes/Decision/__tests__/DefaultDecision.test.ts +179 -0
  24. package/src/Analysis/Passes/Transform/CollapseGradientTransform.ts +176 -0
  25. package/src/Analysis/Passes/Transform/MergeColorsTransform.ts +126 -0
  26. package/src/Analysis/Passes/Transform/RainbowCollapseTransform.ts +83 -0
  27. package/src/Analysis/Passes/Transform/WaveCollapseTransform.ts +88 -0
  28. package/src/Analysis/Passes/Utility/CharacterCountAnalyzer.ts +45 -0
  29. package/src/Analysis/Pipeline/Pipeline.ts +133 -0
  30. package/src/Analysis/Pipeline/PipelineBuilder.ts +55 -0
  31. package/src/Analysis/Pipeline/PipelineStage.ts +19 -0
  32. package/src/Analysis/Utils/color-utils.ts +132 -0
  33. package/src/Analysis/__tests__/Integration.test.ts +162 -0
  34. package/src/Analysis/__tests__/Pipeline.test.ts +133 -0
  35. package/src/Analysis/index.ts +52 -0
  36. package/src/BBCode/BBCodeDocumentModel.ts +175 -0
  37. package/src/BBCode/BBCodeToGreenNode.ts +755 -0
  38. package/src/BBCode/Parser.ts +384 -0
  39. package/src/BBCode/index.ts +12 -0
  40. package/src/Collab/positions.ts +91 -0
  41. package/src/Commands/Command.ts +44 -0
  42. package/src/Commands/CommandRegistry.ts +78 -0
  43. package/src/Commands/DeleteNode.ts +20 -0
  44. package/src/Commands/InsertText.ts +21 -0
  45. package/src/Commands/SplitMerge.ts +28 -0
  46. package/src/Commands/WrapInTag.ts +21 -0
  47. package/src/Commands/index.ts +6 -0
  48. package/src/Diff/TreeDiffer.ts +264 -0
  49. package/src/Diff/__tests__/TreeDiffer.test.ts +65 -0
  50. package/src/Diff/index.ts +2 -0
  51. package/src/Events/EventBus.ts +160 -0
  52. package/src/Events/index.ts +2 -0
  53. package/src/Formatter/Formatter.ts +54 -0
  54. package/src/Formatter/index.ts +2 -0
  55. package/src/HTML/HTMLDocumentModel.ts +35 -0
  56. package/src/HTML/HTMLToGreenNode.ts +290 -0
  57. package/src/Incremental/ChangeTracker.ts +105 -0
  58. package/src/Incremental/IncrementalParser.ts +591 -0
  59. package/src/Incremental/__tests__/IncrementalParser.test.ts +164 -0
  60. package/src/Incremental/index.ts +4 -0
  61. package/src/Lexer/BBCodeLexer.ts +382 -0
  62. package/src/Lexer/Lexer.ts +181 -0
  63. package/src/Lexer/index.ts +10 -0
  64. package/src/Linter/Linter.ts +193 -0
  65. package/src/Linter/index.ts +2 -0
  66. package/src/Markdown/MarkdownAST.ts +112 -0
  67. package/src/Markdown/MarkdownDocumentModel.ts +55 -0
  68. package/src/Markdown/MarkdownLexer.ts +203 -0
  69. package/src/Markdown/MarkdownParser.ts +455 -0
  70. package/src/Markdown/MarkdownToGreenNode.ts +153 -0
  71. package/src/Model/DocumentModel.ts +694 -0
  72. package/src/Model/NodeFactory.ts +117 -0
  73. package/src/Model/TagRegistry.ts +495 -0
  74. package/src/Model/index.ts +5 -0
  75. package/src/Plugins/PluginAPI.ts +119 -0
  76. package/src/Plugins/PluginRegistry.ts +132 -0
  77. package/src/Plugins/index.ts +3 -0
  78. package/src/Queries/QueryEngine.ts +152 -0
  79. package/src/Queries/index.ts +1 -0
  80. package/src/RenderPipeline/RenderPipeline.ts +125 -0
  81. package/src/RenderPipeline/RenderTree.ts +134 -0
  82. package/src/RenderPipeline/index.ts +4 -0
  83. package/src/Semantic/SemanticAnalyzer.ts +506 -0
  84. package/src/Semantic/index.ts +2 -0
  85. package/src/Symbols/SymbolTable.ts +124 -0
  86. package/src/Symbols/index.ts +1 -0
  87. package/src/Syntax/GreenNode.ts +324 -0
  88. package/src/Syntax/GreenNodePool.ts +269 -0
  89. package/src/Syntax/NodeMatcher.ts +370 -0
  90. package/src/Syntax/RedNode.ts +569 -0
  91. package/src/Syntax/RedNodeStore.ts +184 -0
  92. package/src/Syntax/TreeBuilder.ts +214 -0
  93. package/src/Syntax/__tests__/GreenNode.test.ts +33 -0
  94. package/src/Syntax/__tests__/RedNode.test.ts +81 -0
  95. package/src/Syntax/__tests__/RedNodeStore.test.ts +104 -0
  96. package/src/Syntax/greenEdit.ts +110 -0
  97. package/src/Syntax/hash.ts +30 -0
  98. package/src/Syntax/index.ts +12 -0
  99. package/src/Syntax/partition.ts +161 -0
  100. package/src/Syntax/preserveNodeIds.ts +201 -0
  101. package/src/Tests/ASTOptimizerIdempotence.test.ts +77 -0
  102. package/src/Tests/BlockPatcher.test.ts +437 -0
  103. package/src/Tests/BlockPatcherWindowed.test.ts +364 -0
  104. package/src/Tests/BoxDrawer.test.ts +217 -0
  105. package/src/Tests/BoxRichTitle.test.ts +105 -0
  106. package/src/Tests/Chars500kBenchmark.test.ts +151 -0
  107. package/src/Tests/Chars500kEdits.test.ts +321 -0
  108. package/src/Tests/CollabPositions.test.ts +146 -0
  109. package/src/Tests/CompilerPathProfiling.test.ts +186 -0
  110. package/src/Tests/DOMMorpher.test.ts +142 -0
  111. package/src/Tests/DomPatchPerf.test.ts +60 -0
  112. package/src/Tests/EffectSegments.snapshot.json +616 -0
  113. package/src/Tests/EffectSegments.test.ts +68 -0
  114. package/src/Tests/FindNodeAtOffset.test.ts +65 -0
  115. package/src/Tests/Fuzzer.test.ts +166 -0
  116. package/src/Tests/GreenNodePool.test.ts +153 -0
  117. package/src/Tests/Lexer.test.ts +238 -0
  118. package/src/Tests/LyneMode.test.ts +187 -0
  119. package/src/Tests/ModelCoherence.test.ts +180 -0
  120. package/src/Tests/Partition.test.ts +238 -0
  121. package/src/Tests/PluginTags.test.ts +150 -0
  122. package/src/Tests/ProblematicSection.test.ts +46 -0
  123. package/src/Tests/ProblematicSectionHTML.test.ts +58 -0
  124. package/src/Tests/RedReuse.test.ts +134 -0
  125. package/src/Tests/ReproDelete20k.test.ts +62 -0
  126. package/src/Tests/SemanticValidators.test.ts +136 -0
  127. package/src/Tests/StableNodeIds.test.ts +210 -0
  128. package/src/Tests/StudioColorBloat.test.ts +25 -0
  129. package/src/Tests/StudioDebugText.test.ts +27 -0
  130. package/src/Tests/StudioTrailingChar.test.ts +25 -0
  131. package/src/Tests/StudioValidText.test.ts +25 -0
  132. package/src/Tests/UrlImgBug.test.ts +23 -0
  133. package/src/Tests/VisualBuilderFidelity.test.ts +105 -0
  134. package/src/Tests/referenceDocument.ts +119 -0
  135. package/src/Transactions/Transaction.ts +176 -0
  136. package/src/Transactions/UndoManager.ts +111 -0
  137. package/src/Transactions/index.ts +3 -0
  138. package/src/Transformers/ASTOptimizer.ts +315 -0
  139. package/src/Transformers/GradientTransformer.ts +143 -0
  140. package/src/Transformers/GrowTransformer.ts +115 -0
  141. package/src/Transformers/RainbowTransformer.ts +121 -0
  142. package/src/Transformers/SineWaveTransformer.ts +130 -0
  143. package/src/Transformers/Transformer.ts +22 -0
  144. package/src/Types/core.ts +270 -0
  145. package/src/Types/diagnostics.ts +156 -0
  146. package/src/Types/index.ts +23 -0
  147. package/src/Types/operations.ts +180 -0
  148. package/src/Types/queries.ts +121 -0
  149. package/src/Types/symbols.ts +69 -0
  150. package/src/Types/tokens.ts +186 -0
  151. package/src/Utils/BBCodeGenerator.ts +126 -0
  152. package/src/Utils/ColorMath.ts +276 -0
  153. package/src/Utils/color.ts +112 -0
  154. package/src/Utils/dom-to-svg.test.ts +86 -0
  155. package/src/Utils/dom-to-svg.ts +615 -0
  156. package/src/Utils/treeTransformers.ts +717 -0
  157. package/src/Visitors/BBBlocksExporter.ts +69 -0
  158. package/src/Visitors/BBCodeExporter.ts +318 -0
  159. package/src/Visitors/BlockPatcher.ts +963 -0
  160. package/src/Visitors/DOMMorpher.ts +134 -0
  161. package/src/Visitors/HTMLRenderer.ts +1077 -0
  162. package/src/Visitors/JSONExporter.ts +66 -0
  163. package/src/Visitors/MarkdownExporter.ts +99 -0
  164. package/src/Visitors/SVGRenderer.ts +35 -0
  165. package/src/Visitors/TiptapExporter.ts +145 -0
  166. package/src/Visitors/Visitor.ts +48 -0
  167. package/src/Visitors/index.ts +9 -0
  168. package/src/Visuals/BoxDrawer.ts +175 -0
  169. package/src/Visuals/index.ts +42 -0
  170. package/src/Visuals/lyne.css +1027 -0
  171. package/src/Visuals/osu.css +257 -0
  172. package/src/index.ts +197 -0
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Quasar Analysis Framework — Pipeline Builder
3
+ *
4
+ * Provides a fluent API for assembling a Pipeline from individually
5
+ * registered passes. Each stage (analysis / decision / transform)
6
+ * has its own registration method so TypeScript catches misuse at
7
+ * compile time.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const pipeline = Pipeline
12
+ * .builder()
13
+ * .analysis(new CharacterCountAnalyzer())
14
+ * .decision(new DefaultDecision())
15
+ * .transform(new CollapseGradientTransform())
16
+ * .build()
17
+ * ```
18
+ */
19
+
20
+ import type { AnalyzerPass, DecisionPass, TransformPass } from '../Contracts/Pass'
21
+ import type { PipelineStage } from './PipelineStage'
22
+ import { Pipeline } from './Pipeline'
23
+
24
+ export class PipelineBuilder {
25
+ private readonly passes: { stage: PipelineStage; pass: AnalyzerPass | DecisionPass | TransformPass }[] = []
26
+
27
+ /** Register one or more AnalyzerPasses (observation stage). */
28
+ analysis(...passes: AnalyzerPass[]): this {
29
+ for (const p of passes) {
30
+ this.passes.push({ stage: 'analysis', pass: p })
31
+ }
32
+ return this
33
+ }
34
+
35
+ /** Register one or more DecisionPasses (planning stage). */
36
+ decision(...passes: DecisionPass[]): this {
37
+ for (const p of passes) {
38
+ this.passes.push({ stage: 'decision', pass: p })
39
+ }
40
+ return this
41
+ }
42
+
43
+ /** Register one or more TransformPasses (mutation stage). */
44
+ transform(...passes: TransformPass[]): this {
45
+ for (const p of passes) {
46
+ this.passes.push({ stage: 'transform', pass: p })
47
+ }
48
+ return this
49
+ }
50
+
51
+ /** Build the Pipeline from the registered passes. */
52
+ build(): Pipeline {
53
+ return new Pipeline([...this.passes])
54
+ }
55
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Quasar Analysis Framework — Pipeline Stage
3
+ *
4
+ * Passes are grouped into stages that execute in order:
5
+ *
6
+ * 1. Analysis — observe the tree, produce Contributions
7
+ * 2. Decision — consume the AnalysisReport, produce a TransformationPlan
8
+ * 3. Transform — apply the plan to produce a new Green Tree
9
+ *
10
+ * @see Pass
11
+ */
12
+
13
+ export const PipelineStage = {
14
+ Analysis: 'analysis',
15
+ Decision: 'decision',
16
+ Transform: 'transform',
17
+ } as const
18
+
19
+ export type PipelineStage = (typeof PipelineStage)[keyof typeof PipelineStage]
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Quasar Analysis Framework — Color Utilities
3
+ *
4
+ * Shared utility functions for color node analysis across all passes.
5
+ * Extracted here to avoid duplication across GradientAnalyzer,
6
+ * RainbowAnalyzer, MergeableColorAnalyzer, etc.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import type { GreenNode } from '../../Syntax/GreenNode'
12
+
13
+ // ── Hex extraction ────────────────────────────────────────────────
14
+
15
+ /**
16
+ * Extract the hex colour from a [color] GreenNode.
17
+ * Supports both "=#FF0000" and "#FF0000" formats.
18
+ * Returns null if the node is not a color node or has invalid hex.
19
+ */
20
+ export function extractHex(node: GreenNode): string | null {
21
+ if (node.kind !== 'color') return null
22
+ const text = node.text || ''
23
+ const eqIdx = text.indexOf('=')
24
+ const hex = eqIdx >= 0 ? text.slice(eqIdx + 1).trim() : text.trim()
25
+ return /^#[0-9A-Fa-f]{6}$/.test(hex) ? hex.toUpperCase() : null
26
+ }
27
+
28
+ /**
29
+ * Extract the size value from a [size] / [font_size] GreenNode.
30
+ * Supports "=150", "=90" formats.
31
+ * Returns null if the node is not a size node.
32
+ */
33
+ export function extractSize(node: GreenNode): number | null {
34
+ if (node.kind !== 'font_size') return null
35
+ const text = node.text || ''
36
+ const eqIdx = text.indexOf('=')
37
+ const val = eqIdx >= 0 ? text.slice(eqIdx + 1).trim() : text.trim()
38
+ const num = parseInt(val, 10)
39
+ return isNaN(num) ? null : num
40
+ }
41
+
42
+ // ── Sigmoid ───────────────────────────────────────────────────────
43
+
44
+ /**
45
+ * Sigmoid function to map a raw score to [0, 1] confidence.
46
+ * Centered at 0: sigmoid(0) = 0.5
47
+ * steepness controls the sharpness of the transition.
48
+ */
49
+ export function sigmoid(x: number, steepness: number = 6): number {
50
+ return 1 / (1 + Math.exp(-x * steepness))
51
+ }
52
+
53
+ // ── Sequence helpers ──────────────────────────────────────────────
54
+
55
+ /**
56
+ * A detected sequence of adjacent similar nodes in the Green Tree.
57
+ */
58
+ export interface NodeSequence {
59
+ /** Extracted values (hex colours, sizes, etc.) in order */
60
+ readonly values: (string | number)[]
61
+ /** Start index in the parent's children array */
62
+ readonly startIdx: number
63
+ /** End index (exclusive) in the parent's children array */
64
+ readonly endIdx: number
65
+ }
66
+
67
+ /**
68
+ * Extract consecutive sequences of [color] or [font_size] nodes
69
+ * from a parent's children array.
70
+ *
71
+ * @param children - Direct children of a container node
72
+ * @param kind - The node kind to extract ('color' or 'font_size')
73
+ * @param extract - Function to extract values from matching nodes
74
+ * @returns Array of sequences found
75
+ */
76
+ export function extractSequences<T extends string | number>(
77
+ children: GreenNode[],
78
+ kind: string,
79
+ extract: (node: GreenNode) => T | null,
80
+ ): Array<{ values: T[]; startIdx: number; endIdx: number }> {
81
+ const sequences: Array<{ values: T[]; startIdx: number; endIdx: number }> = []
82
+ let current: { values: T[]; startIdx: number } | null = null
83
+
84
+ for (let i = 0; i < children.length; i++) {
85
+ const child = children[i]
86
+ const val = child.kind === kind ? extract(child) : null
87
+
88
+ if (val !== null) {
89
+ if (current === null) {
90
+ current = { values: [val], startIdx: i }
91
+ } else {
92
+ current.values.push(val)
93
+ }
94
+ } else if (child.kind !== 'text' || child.text.trim() !== '') {
95
+ // Non-text, non-{kind} node OR non-whitespace text — end sequence
96
+ if (current !== null) {
97
+ sequences.push({ ...current, endIdx: i })
98
+ current = null
99
+ }
100
+ }
101
+ }
102
+
103
+ if (current !== null) {
104
+ sequences.push({ ...current, endIdx: children.length })
105
+ }
106
+
107
+ return sequences
108
+ }
109
+
110
+ /**
111
+ * Check if there are formatting tags between nodes in a sequence.
112
+ */
113
+ export function checkFormattingBreaks(
114
+ children: GreenNode[],
115
+ seq: { startIdx: number; endIdx: number },
116
+ targetKind: string,
117
+ ): boolean {
118
+ for (let i = seq.startIdx; i < seq.endIdx; i++) {
119
+ const child = children[i]
120
+ if (child.kind !== targetKind && child.kind !== 'text') {
121
+ return true
122
+ }
123
+ if (child.kind === targetKind) {
124
+ for (const c of child.children as GreenNode[]) {
125
+ if (c.kind !== 'text' && c.kind !== 'spacing' && c.kind !== 'empty_line') {
126
+ return true
127
+ }
128
+ }
129
+ }
130
+ }
131
+ return false
132
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Quasar Analysis Framework — Integration Tests
3
+ *
4
+ * End-to-end tests that exercise the full pipeline with real passes:
5
+ * GradientAnalyzer → DefaultDecision → CollapseGradientTransform
6
+ * MergeableColorAnalyzer → DefaultDecision → MergeColorsTransform
7
+ */
8
+
9
+ import { describe, it, expect } from 'vitest'
10
+ import { greenNode, greenLeaf } from '../../Syntax/GreenNode'
11
+ import type { GreenNode } from '../../Syntax/GreenNode'
12
+ import { Pipeline } from '../Pipeline/Pipeline'
13
+ import { PipelineBuilder } from '../Pipeline/PipelineBuilder'
14
+ import { GradientAnalyzer } from '../Passes/Analysis/GradientAnalyzer'
15
+ import { MergeableColorAnalyzer } from '../Passes/Analysis/MergeableColorAnalyzer'
16
+ import { DefaultDecision } from '../Passes/Decision/DefaultDecision'
17
+ import { CollapseGradientTransform } from '../Passes/Transform/CollapseGradientTransform'
18
+ import { MergeColorsTransform } from '../Passes/Transform/MergeColorsTransform'
19
+ import type { PipelineContext } from '../Contracts/PipelineContext'
20
+ import { PipelineMode, ExportTarget } from '../Contracts/PipelineContext'
21
+
22
+ // ── Helpers ───────────────────────────────────────────────────────
23
+
24
+ function colorNode(hex: string, text: string): GreenNode {
25
+ // Widths of the real BBCode this stands for: `[color=#RRGGBB]` and `[/color]`.
26
+ const textLeaf = greenLeaf('text', text)
27
+ return greenNode('color', `=${hex}`, [textLeaf], 8 + hex.length, 8)
28
+ }
29
+
30
+ const interactiveContext: PipelineContext = {
31
+ mode: PipelineMode.Interactive,
32
+ target: ExportTarget.Miliastry,
33
+ featureFlags: {},
34
+ metadata: {},
35
+ }
36
+
37
+ const batchOsuContext: PipelineContext = {
38
+ mode: PipelineMode.Batch,
39
+ target: ExportTarget.Osu,
40
+ featureFlags: {},
41
+ metadata: {},
42
+ }
43
+
44
+ // ── Tests ─────────────────────────────────────────────────────────
45
+
46
+ describe('Gradient pipeline (full integration)', () => {
47
+ it('collapses a clear gradient with miliastry target', () => {
48
+ // [color=#FF0000]H[/color][color=#EE1100]e[/color][color=#DD2200]l[/color][color=#CC3300]l[/color][color=#BB4400]o[/color]
49
+ const tree = greenNode('document', '', [
50
+ colorNode('#FF0000', 'H'),
51
+ colorNode('#EE1100', 'e'),
52
+ colorNode('#DD2200', 'l'),
53
+ colorNode('#CC3300', 'l'),
54
+ colorNode('#BB4400', 'o'),
55
+ ])
56
+
57
+ const pipeline = new PipelineBuilder()
58
+ .analysis(new GradientAnalyzer())
59
+ .decision(new DefaultDecision())
60
+ .transform(new CollapseGradientTransform())
61
+ .build()
62
+
63
+ const result = pipeline.run(tree, interactiveContext)
64
+ const treeChildren = result.tree.children as GreenNode[]
65
+
66
+ // After collapsing, we should have a single gradient node
67
+ const gradientNodes = treeChildren.filter(c => c.kind === 'gradient')
68
+ expect(gradientNodes.length).toBeGreaterThanOrEqual(1)
69
+ })
70
+
71
+ it('preserves non-gradient color sequences with osu target', () => {
72
+ // Random colors — low confidence, below osu threshold
73
+ const tree = greenNode('document', '', [
74
+ colorNode('#FF0000', 'A'),
75
+ colorNode('#00FF00', 'B'),
76
+ colorNode('#0000FF', 'C'),
77
+ ])
78
+
79
+ const pipeline = new PipelineBuilder()
80
+ .analysis(new GradientAnalyzer())
81
+ .decision(new DefaultDecision())
82
+ .transform(new CollapseGradientTransform())
83
+ .build()
84
+
85
+ const result = pipeline.run(tree, batchOsuContext)
86
+ const treeChildren = result.tree.children as GreenNode[]
87
+
88
+ // With only 3 random colors, confidence should be below osu threshold
89
+ const gradientNodes = treeChildren.filter(c => c.kind === 'gradient')
90
+ expect(gradientNodes).toHaveLength(0)
91
+ })
92
+
93
+ it('produces an AnalysisReport with contributions', () => {
94
+ const tree = greenNode('document', '', [
95
+ colorNode('#FF0000', 'A'),
96
+ colorNode('#EE1100', 'B'),
97
+ colorNode('#DD2200', 'C'),
98
+ colorNode('#CC3300', 'D'),
99
+ ])
100
+
101
+ const pipeline = new PipelineBuilder()
102
+ .analysis(new GradientAnalyzer())
103
+ .build()
104
+
105
+ const result = pipeline.run(tree, interactiveContext)
106
+
107
+ expect(result.report.contributions.length).toBeGreaterThanOrEqual(1)
108
+ expect(result.report.elapsedMs).toBeGreaterThanOrEqual(0)
109
+ expect(result.report.passCount).toBe(1)
110
+ })
111
+ })
112
+
113
+ describe('Merge colors pipeline (full integration)', () => {
114
+ it('merges identical consecutive color nodes', () => {
115
+ // [color=#FF0000]H[/color][color=#FF0000]e[/color][color=#FF0000]l[/color]
116
+ const tree = greenNode('document', '', [
117
+ colorNode('#FF0000', 'H'),
118
+ colorNode('#FF0000', 'e'),
119
+ colorNode('#FF0000', 'l'),
120
+ ])
121
+
122
+ const pipeline = new PipelineBuilder()
123
+ .analysis(new MergeableColorAnalyzer())
124
+ .decision(new DefaultDecision())
125
+ .transform(new MergeColorsTransform())
126
+ .build()
127
+
128
+ const result = pipeline.run(tree, interactiveContext)
129
+ const treeChildren = result.tree.children as GreenNode[]
130
+
131
+ // Should be 1 colour node (merged) instead of 3
132
+ const colorNodes = treeChildren.filter(c => c.kind === 'color')
133
+ expect(colorNodes).toHaveLength(1)
134
+ })
135
+ })
136
+
137
+ describe('Combined pipeline', () => {
138
+ it('runs multiple analysis passes and produces correct report', () => {
139
+ const tree = greenNode('document', '', [
140
+ colorNode('#FF0000', 'A'),
141
+ colorNode('#EE1100', 'B'),
142
+ colorNode('#DD2200', 'C'),
143
+ colorNode('#FF0000', 'X'), // part of mergeable (different from gradient sequence)
144
+ colorNode('#FF0000', 'Y'),
145
+ ])
146
+
147
+ const pipeline = new PipelineBuilder()
148
+ .analysis(new GradientAnalyzer(), new MergeableColorAnalyzer())
149
+ .decision(new DefaultDecision())
150
+ .build()
151
+
152
+ const result = pipeline.run(tree, interactiveContext)
153
+
154
+ // Should have contributions from both analyzers
155
+ const analysisContributions = result.report.contributions
156
+ const gradientContributions = analysisContributions.filter(c => c.kind === 'semantic' && c.label === 'Gradient')
157
+ const mergeContributions = analysisContributions.filter(c => c.kind === 'optimization' && c.label === 'Merge Colors')
158
+
159
+ expect(gradientContributions.length).toBeGreaterThanOrEqual(1)
160
+ expect(mergeContributions.length).toBeGreaterThanOrEqual(1)
161
+ })
162
+ })
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Quasar Analysis Framework — Pipeline Tests
3
+ *
4
+ * Validates that the core pipeline infrastructure works:
5
+ * - Pass interfaces are correctly typed
6
+ * - PipelineBuilder assembles passes
7
+ * - Pipeline executes them in order
8
+ * - CharacterCountAnalyzer (PoC) produces expected metrics
9
+ */
10
+
11
+ import { describe, it, expect } from 'vitest'
12
+ import { greenNode, greenLeaf } from '../../Syntax/GreenNode'
13
+ import type { GreenNode } from '../../Syntax/GreenNode'
14
+ import { Pipeline } from '../Pipeline/Pipeline'
15
+ import { PipelineBuilder } from '../Pipeline/PipelineBuilder'
16
+ import { CharacterCountAnalyzer } from '../Passes/Utility/CharacterCountAnalyzer'
17
+ import type { PipelineContext } from '../Contracts/PipelineContext'
18
+ import { PipelineMode, ExportTarget } from '../Contracts/PipelineContext'
19
+
20
+ // ── Helpers ───────────────────────────────────────────────────────
21
+
22
+ const mockContext: PipelineContext = {
23
+ mode: PipelineMode.Batch,
24
+ target: ExportTarget.Osu,
25
+ featureFlags: {},
26
+ metadata: {},
27
+ }
28
+
29
+ function buildSimpleTree(): GreenNode {
30
+ // root
31
+ // ├── heading "Hello"
32
+ // └── text "World"
33
+ const headingText = greenLeaf('text', 'Hello')
34
+ const heading = greenNode('heading', '', [headingText])
35
+ const worldText = greenLeaf('text', 'World')
36
+ return greenNode('root', '', [heading, worldText])
37
+ }
38
+
39
+ // ── Tests ─────────────────────────────────────────────────────────
40
+
41
+ describe('Pipeline Infrastructure', () => {
42
+ describe('PipelineBuilder', () => {
43
+ it('builds a pipeline from passes', () => {
44
+ const pipeline = new PipelineBuilder()
45
+ .analysis(new CharacterCountAnalyzer())
46
+ .build()
47
+
48
+ expect(pipeline).toBeInstanceOf(Pipeline)
49
+ })
50
+
51
+ it('accepts multiple analysis passes', () => {
52
+ const pipeline = new PipelineBuilder()
53
+ .analysis(new CharacterCountAnalyzer(), new CharacterCountAnalyzer())
54
+ .build()
55
+
56
+ expect(pipeline).toBeInstanceOf(Pipeline)
57
+ })
58
+
59
+ it('rejects analysis passes in decision/transform slots at type level', () => {
60
+ // TypeScript enforces that builder.analysis() only accepts AnalyzerPass
61
+ // instances. This is a compile-time check that can't be validated with
62
+ // vitest. At runtime we just verify the builder doesn't throw for valid usage.
63
+ const builder = new PipelineBuilder()
64
+ expect(() => builder.analysis(new CharacterCountAnalyzer())).not.toThrow()
65
+ })
66
+ })
67
+
68
+ describe('Pipeline execution', () => {
69
+ it('executes analysis passes and produces a report', () => {
70
+ const pipeline = new PipelineBuilder()
71
+ .analysis(new CharacterCountAnalyzer())
72
+ .build()
73
+
74
+ const tree = buildSimpleTree()
75
+ const result = pipeline.run(tree, mockContext)
76
+
77
+ expect(result.report.contributions).toHaveLength(1)
78
+ expect(result.report.passCount).toBe(1)
79
+ expect(result.report.elapsedMs).toBeGreaterThanOrEqual(0)
80
+ })
81
+
82
+ it('preserves the original tree when no transforms are registered', () => {
83
+ const pipeline = new PipelineBuilder()
84
+ .analysis(new CharacterCountAnalyzer())
85
+ .build()
86
+
87
+ const tree = buildSimpleTree()
88
+ const result = pipeline.run(tree, mockContext)
89
+
90
+ expect(result.tree).toBe(tree) // same reference since no transforms
91
+ })
92
+
93
+ it('reports correct character count', () => {
94
+ const pipeline = new PipelineBuilder()
95
+ .analysis(new CharacterCountAnalyzer())
96
+ .build()
97
+
98
+ const tree = buildSimpleTree()
99
+ const result = pipeline.run(tree, mockContext)
100
+
101
+ const metricsContribution = result.report.contributions[0]
102
+ expect(metricsContribution).toMatchObject({
103
+ kind: 'metrics',
104
+ metrics: { characters: 10 }, // "Hello" (5) + "World" (5)
105
+ })
106
+ })
107
+
108
+ it('handles empty trees gracefully', () => {
109
+ const pipeline = new PipelineBuilder()
110
+ .analysis(new CharacterCountAnalyzer())
111
+ .build()
112
+
113
+ const emptyTree = greenNode('root', '', [])
114
+ const result = pipeline.run(emptyTree, mockContext)
115
+
116
+ const metricsContribution = result.report.contributions[0]
117
+ expect(metricsContribution).toMatchObject({
118
+ kind: 'metrics',
119
+ metrics: { characters: 0 },
120
+ })
121
+ })
122
+ })
123
+
124
+ describe('PipelineBuilder direct usage', () => {
125
+ it('works via new PipelineBuilder()', () => {
126
+ const pipeline = new PipelineBuilder()
127
+ .analysis(new CharacterCountAnalyzer())
128
+ .build()
129
+
130
+ expect(pipeline).toBeInstanceOf(Pipeline)
131
+ })
132
+ })
133
+ })
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Quasar Analysis Framework — Public API
3
+ *
4
+ * A pipeline-based analysis and transformation engine for Green Trees.
5
+ *
6
+ * @module
7
+ * @see Analysis/ARCHITECTURE.md (planned)
8
+ */
9
+
10
+ // ── Contracts ─────────────────────────────────────────────────────
11
+ export type { Pass } from './Contracts/Pass'
12
+ export type { AnalyzerPass, DecisionPass, TransformPass } from './Contracts/Pass'
13
+ export type { TransformationPlan, TransformAction } from './Contracts/Pass'
14
+
15
+ export type { Contribution } from './Contracts/Contribution'
16
+ export type {
17
+ SemanticContribution,
18
+ DiagnosticContribution,
19
+ OptimizationContribution,
20
+ MetricsContribution,
21
+ } from './Contracts/Contribution'
22
+ export { ContributionKind } from './Contracts/Contribution'
23
+
24
+ export type { AnalysisReport } from './Contracts/AnalysisReport'
25
+
26
+ export type { PipelineContext } from './Contracts/PipelineContext'
27
+ export { PipelineMode, ExportTarget } from './Contracts/PipelineContext'
28
+
29
+ // ── Pipeline ──────────────────────────────────────────────────────
30
+ export { Pipeline } from './Pipeline/Pipeline'
31
+ export type { PipelineResult } from './Pipeline/Pipeline'
32
+ export { PipelineBuilder } from './Pipeline/PipelineBuilder'
33
+ export { PipelineStage } from './Pipeline/PipelineStage'
34
+
35
+ // ── Analysis Passes ───────────────────────────────────────────────
36
+ export { CharacterCountAnalyzer } from './Passes/Utility/CharacterCountAnalyzer'
37
+ export { MergeableColorAnalyzer } from './Passes/Analysis/MergeableColorAnalyzer'
38
+ export { GradientAnalyzer } from './Passes/Analysis/GradientAnalyzer'
39
+ export type { GradientModel, GradientDiagnostics, GradientStop } from './Passes/Analysis/GradientAnalyzer'
40
+ export { RainbowAnalyzer } from './Passes/Analysis/RainbowAnalyzer'
41
+ export type { RainbowModel, RainbowDiagnostics } from './Passes/Analysis/RainbowAnalyzer'
42
+ export { WaveAnalyzer } from './Passes/Analysis/WaveAnalyzer'
43
+ export type { WaveModel, WaveDiagnostics } from './Passes/Analysis/WaveAnalyzer'
44
+
45
+ // ── Decision Passes ───────────────────────────────────────────────
46
+ export { DefaultDecision } from './Passes/Decision/DefaultDecision'
47
+
48
+ // ── Transform Passes ──────────────────────────────────────────────
49
+ export { CollapseGradientTransform } from './Passes/Transform/CollapseGradientTransform'
50
+ export { RainbowCollapseTransform } from './Passes/Transform/RainbowCollapseTransform'
51
+ export { WaveCollapseTransform } from './Passes/Transform/WaveCollapseTransform'
52
+ export { MergeColorsTransform } from './Passes/Transform/MergeColorsTransform'