@pennyfarthing/shared 7.5.0 → 9.3.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 (82) hide show
  1. package/dist/browser.d.ts +6 -0
  2. package/dist/browser.d.ts.map +1 -0
  3. package/dist/browser.js +8 -0
  4. package/dist/browser.js.map +1 -0
  5. package/dist/generate-skill-docs.d.ts +35 -0
  6. package/dist/generate-skill-docs.d.ts.map +1 -0
  7. package/dist/generate-skill-docs.js +442 -0
  8. package/dist/generate-skill-docs.js.map +1 -0
  9. package/dist/generate-skill-docs.test.d.ts +13 -0
  10. package/dist/generate-skill-docs.test.d.ts.map +1 -0
  11. package/dist/generate-skill-docs.test.js +519 -0
  12. package/dist/generate-skill-docs.test.js.map +1 -0
  13. package/dist/index.d.ts +11 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +13 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/marker/constants.d.ts +39 -0
  18. package/dist/marker/constants.d.ts.map +1 -0
  19. package/dist/marker/constants.js +46 -0
  20. package/dist/marker/constants.js.map +1 -0
  21. package/dist/marker/continue.test.d.ts +12 -0
  22. package/dist/marker/continue.test.d.ts.map +1 -0
  23. package/dist/marker/continue.test.js +76 -0
  24. package/dist/marker/continue.test.js.map +1 -0
  25. package/dist/marker/detect.d.ts +25 -0
  26. package/dist/marker/detect.d.ts.map +1 -0
  27. package/dist/marker/detect.js +53 -0
  28. package/dist/marker/detect.js.map +1 -0
  29. package/dist/marker/detect.test.d.ts +10 -0
  30. package/dist/marker/detect.test.d.ts.map +1 -0
  31. package/dist/marker/detect.test.js +418 -0
  32. package/dist/marker/detect.test.js.map +1 -0
  33. package/dist/marker/index.d.ts +14 -0
  34. package/dist/marker/index.d.ts.map +1 -0
  35. package/dist/marker/index.js +15 -0
  36. package/dist/marker/index.js.map +1 -0
  37. package/dist/marker/strip.d.ts +26 -0
  38. package/dist/marker/strip.d.ts.map +1 -0
  39. package/dist/marker/strip.js +39 -0
  40. package/dist/marker/strip.js.map +1 -0
  41. package/dist/marker/types.d.ts +23 -0
  42. package/dist/marker/types.d.ts.map +1 -0
  43. package/dist/marker/types.js +7 -0
  44. package/dist/marker/types.js.map +1 -0
  45. package/dist/migrate-theme-schema.test.d.ts +8 -0
  46. package/dist/migrate-theme-schema.test.d.ts.map +1 -0
  47. package/dist/migrate-theme-schema.test.js +100 -0
  48. package/dist/migrate-theme-schema.test.js.map +1 -0
  49. package/dist/portrait-resolver.d.ts +32 -0
  50. package/dist/portrait-resolver.d.ts.map +1 -0
  51. package/dist/portrait-resolver.js +225 -0
  52. package/dist/portrait-resolver.js.map +1 -0
  53. package/dist/portrait-resolver.test.d.ts +2 -0
  54. package/dist/portrait-resolver.test.d.ts.map +1 -0
  55. package/dist/portrait-resolver.test.js +156 -0
  56. package/dist/portrait-resolver.test.js.map +1 -0
  57. package/dist/skill-search.d.ts +36 -0
  58. package/dist/skill-search.d.ts.map +1 -0
  59. package/dist/skill-search.js +300 -0
  60. package/dist/skill-search.js.map +1 -0
  61. package/dist/skill-search.sh +41 -0
  62. package/dist/skill-search.test.d.ts +16 -0
  63. package/dist/skill-search.test.d.ts.map +1 -0
  64. package/dist/skill-search.test.js +177 -0
  65. package/dist/skill-search.test.js.map +1 -0
  66. package/dist/skill-suggest.d.ts +76 -0
  67. package/dist/skill-suggest.d.ts.map +1 -0
  68. package/dist/skill-suggest.js +256 -0
  69. package/dist/skill-suggest.js.map +1 -0
  70. package/dist/skill-suggest.test.d.ts +12 -0
  71. package/dist/skill-suggest.test.d.ts.map +1 -0
  72. package/dist/skill-suggest.test.js +257 -0
  73. package/dist/skill-suggest.test.js.map +1 -0
  74. package/dist/theme-loader.d.ts +81 -0
  75. package/dist/theme-loader.d.ts.map +1 -0
  76. package/dist/theme-loader.js +491 -0
  77. package/dist/theme-loader.js.map +1 -0
  78. package/dist/theme-loader.test.d.ts +8 -0
  79. package/dist/theme-loader.test.d.ts.map +1 -0
  80. package/dist/theme-loader.test.js +62 -0
  81. package/dist/theme-loader.test.js.map +1 -0
  82. package/package.json +14 -1
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Tests for Story 9-3: Add Skill Suggestions to Agents
3
+ *
4
+ * These tests verify:
5
+ * - AC1: Session-aware suggestions based on story context
6
+ * - AC2: Keyword-triggered suggestions with scoring
7
+ * - AC3: Non-intrusive presentation (confidence threshold, limits)
8
+ *
9
+ * Run with: npm test -- packages/shared/src/skill-suggest.test.ts
10
+ */
11
+ import { describe, it } from 'node:test';
12
+ import assert from 'node:assert';
13
+ // Import functions to test - these don't exist yet, tests should fail (RED)
14
+ import { suggestSkills, suggestFromSession, suggestFromKeywords, } from './skill-suggest.js';
15
+ describe('Story 9-3: Skill Suggestions', () => {
16
+ describe('AC1: Session-aware suggestions', () => {
17
+ it('should suggest skills based on story acceptance criteria', async () => {
18
+ // AC1: Agents suggest skills when relevant (session-aware)
19
+ const sessionContext = {
20
+ storyId: '9-3',
21
+ acceptanceCriteria: [
22
+ 'Write failing tests for the feature',
23
+ 'Implement TDD workflow',
24
+ ],
25
+ phase: 'testing',
26
+ };
27
+ const suggestions = await suggestFromSession(sessionContext);
28
+ assert.ok(Array.isArray(suggestions), 'Should return array of suggestions');
29
+ assert.ok(suggestions.length > 0, 'Should suggest at least one skill');
30
+ const skillNames = suggestions.map(s => s.name);
31
+ assert.ok(skillNames.includes('testing'), 'Should suggest testing skill for TDD-related ACs');
32
+ });
33
+ it('should suggest skills based on current workflow phase', async () => {
34
+ // AC1: Phase-aware suggestions
35
+ const sessionContext = {
36
+ storyId: '9-3',
37
+ acceptanceCriteria: ['Deploy the feature'],
38
+ phase: 'review',
39
+ };
40
+ const suggestions = await suggestFromSession(sessionContext);
41
+ // Review phase should suggest code-review skill
42
+ const skillNames = suggestions.map(s => s.name);
43
+ assert.ok(skillNames.includes('code-review'), 'Should suggest code-review skill during review phase');
44
+ });
45
+ it('should include relevance reason for each suggestion', async () => {
46
+ // AC1: Suggestions should explain why they're relevant
47
+ const sessionContext = {
48
+ storyId: '9-3',
49
+ acceptanceCriteria: ['Add unit tests'],
50
+ phase: 'testing',
51
+ };
52
+ const suggestions = await suggestFromSession(sessionContext);
53
+ assert.ok(suggestions.length > 0, 'Should have suggestions');
54
+ for (const suggestion of suggestions) {
55
+ assert.ok(suggestion.reason, `Suggestion ${suggestion.name} should have a reason`);
56
+ assert.ok(suggestion.reason.length > 10, `Reason should be descriptive: ${suggestion.reason}`);
57
+ }
58
+ });
59
+ it('should return empty array when no story context available', async () => {
60
+ // AC1: Graceful handling of missing context
61
+ const emptyContext = {
62
+ storyId: '',
63
+ acceptanceCriteria: [],
64
+ phase: 'unknown',
65
+ };
66
+ const suggestions = await suggestFromSession(emptyContext);
67
+ assert.ok(Array.isArray(suggestions), 'Should return array');
68
+ assert.strictEqual(suggestions.length, 0, 'Should return empty array for no context');
69
+ });
70
+ it('should suggest jira skill when story has Jira reference', async () => {
71
+ // AC1: Detect Jira context
72
+ const sessionContext = {
73
+ storyId: '9-3',
74
+ jiraKey: 'MSSCI-11518',
75
+ acceptanceCriteria: ['Update Jira status'],
76
+ phase: 'development',
77
+ };
78
+ const suggestions = await suggestFromSession(sessionContext);
79
+ const skillNames = suggestions.map(s => s.name);
80
+ assert.ok(skillNames.includes('jira'), 'Should suggest jira skill when Jira key is present');
81
+ });
82
+ });
83
+ describe('AC2: Keyword-triggered suggestions', () => {
84
+ it('should extract keywords from user input and match skills', async () => {
85
+ // AC2: Suggestions based on task analysis (keyword extraction)
86
+ const userInput = 'I need to write some tests for this feature';
87
+ const suggestions = await suggestFromKeywords(userInput);
88
+ assert.ok(Array.isArray(suggestions), 'Should return array');
89
+ assert.ok(suggestions.length > 0, 'Should find matching skills');
90
+ const skillNames = suggestions.map(s => s.name);
91
+ assert.ok(skillNames.includes('testing'), 'Should suggest testing skill for "tests" keyword');
92
+ });
93
+ it('should match skills by their registered keywords', async () => {
94
+ // AC2: Registry matching
95
+ const userInput = 'How do I use vitest for unit testing?';
96
+ const suggestions = await suggestFromKeywords(userInput);
97
+ // vitest is a keyword for testing skill
98
+ const testingSuggestion = suggestions.find(s => s.name === 'testing');
99
+ assert.ok(testingSuggestion, 'Should find testing skill via vitest keyword');
100
+ });
101
+ it('should score suggestions by keyword match count', async () => {
102
+ // AC2: Scoring mechanism
103
+ const userInput = 'I need to run tests, write tests, and check test coverage';
104
+ const suggestions = await suggestFromKeywords(userInput);
105
+ assert.ok(suggestions.length > 0, 'Should have suggestions');
106
+ // First suggestion should have highest score
107
+ for (let i = 1; i < suggestions.length; i++) {
108
+ assert.ok(suggestions[i - 1].score >= suggestions[i].score, 'Suggestions should be sorted by score descending');
109
+ }
110
+ });
111
+ it('should include score in each suggestion', async () => {
112
+ // AC2: Each suggestion has a relevance score
113
+ const userInput = 'deploy the application to production';
114
+ const suggestions = await suggestFromKeywords(userInput);
115
+ for (const suggestion of suggestions) {
116
+ assert.ok(typeof suggestion.score === 'number', `Suggestion ${suggestion.name} should have numeric score`);
117
+ assert.ok(suggestion.score >= 0 && suggestion.score <= 1, `Score should be between 0 and 1: ${suggestion.score}`);
118
+ }
119
+ });
120
+ it('should return empty array for input with no skill keywords', async () => {
121
+ // AC2: Graceful handling of no matches
122
+ const userInput = 'hello world this is random text';
123
+ const suggestions = await suggestFromKeywords(userInput);
124
+ assert.ok(Array.isArray(suggestions), 'Should return array');
125
+ assert.strictEqual(suggestions.length, 0, 'Should return empty for no keyword matches');
126
+ });
127
+ it('should handle case-insensitive keyword matching', async () => {
128
+ // AC2: Case insensitivity
129
+ const lowerInput = 'run jest tests';
130
+ const upperInput = 'Run JEST Tests';
131
+ const lowerSuggestions = await suggestFromKeywords(lowerInput);
132
+ const upperSuggestions = await suggestFromKeywords(upperInput);
133
+ assert.deepStrictEqual(lowerSuggestions.map(s => s.name).sort(), upperSuggestions.map(s => s.name).sort(), 'Keyword matching should be case-insensitive');
134
+ });
135
+ it('should match skill tags in addition to keywords', async () => {
136
+ // AC2: Tag matching
137
+ const userInput = 'I need help with quality assurance';
138
+ const suggestions = await suggestFromKeywords(userInput);
139
+ // 'quality' is a tag, should match skills with that tag
140
+ const hasQualitySkill = suggestions.some(s => s.matchedOn?.includes('tag:quality'));
141
+ assert.ok(hasQualitySkill, 'Should match skills by tag');
142
+ });
143
+ });
144
+ describe('AC3: Non-intrusive presentation', () => {
145
+ it('should only return suggestions above confidence threshold', async () => {
146
+ // AC3: Contextual, not every message
147
+ const options = {
148
+ confidenceThreshold: 0.5,
149
+ };
150
+ const userInput = 'maybe something about testing I guess';
151
+ const suggestions = await suggestSkills(userInput, options);
152
+ for (const suggestion of suggestions) {
153
+ assert.ok(suggestion.score >= 0.5, `All suggestions should be above threshold: ${suggestion.name} has ${suggestion.score}`);
154
+ }
155
+ });
156
+ it('should limit number of suggestions returned', async () => {
157
+ // AC3: Non-intrusive - don't overwhelm with suggestions
158
+ const options = {
159
+ maxResults: 3,
160
+ };
161
+ const userInput = 'I need to test, deploy, review, document everything';
162
+ const suggestions = await suggestSkills(userInput, options);
163
+ assert.ok(suggestions.length <= 3, `Should return at most ${options.maxResults} suggestions`);
164
+ });
165
+ it('should exclude already-used skills from suggestions', async () => {
166
+ // AC3: Contextual - don't suggest what they're already using
167
+ const options = {
168
+ excludeSkills: ['testing'],
169
+ };
170
+ const userInput = 'I need to write more tests';
171
+ const suggestions = await suggestSkills(userInput, options);
172
+ const skillNames = suggestions.map(s => s.name);
173
+ assert.ok(!skillNames.includes('testing'), 'Should not suggest excluded skills');
174
+ });
175
+ it('should return empty array when all matches are below threshold', async () => {
176
+ // AC3: Don't suggest low-confidence matches
177
+ const options = {
178
+ confidenceThreshold: 0.99,
179
+ };
180
+ const userInput = 'something vaguely related to code';
181
+ const suggestions = await suggestSkills(userInput, options);
182
+ assert.ok(Array.isArray(suggestions), 'Should return array');
183
+ assert.strictEqual(suggestions.length, 0, 'Should return empty when below threshold');
184
+ });
185
+ it('should combine session context and keywords when both provided', async () => {
186
+ // AC3: Proactive - detect helpful moments
187
+ const sessionContext = {
188
+ storyId: '9-3',
189
+ acceptanceCriteria: ['Add documentation'],
190
+ phase: 'development',
191
+ };
192
+ const options = {
193
+ sessionContext,
194
+ };
195
+ const userInput = 'how do I write good docs?';
196
+ const suggestions = await suggestSkills(userInput, options);
197
+ // Should boost documentation-related skills
198
+ assert.ok(suggestions.length > 0, 'Should have suggestions');
199
+ // First suggestion should be docs-related given both context and input mention it
200
+ const topSuggestion = suggestions[0];
201
+ assert.ok(topSuggestion.name.includes('changelog') ||
202
+ topSuggestion.reason?.toLowerCase().includes('document'), 'Top suggestion should be documentation-related');
203
+ });
204
+ it('should respect default options when none provided', async () => {
205
+ // AC3: Sensible defaults
206
+ const userInput = 'run the tests';
207
+ const suggestions = await suggestSkills(userInput);
208
+ // Should work with defaults
209
+ assert.ok(Array.isArray(suggestions), 'Should return array with default options');
210
+ // Default threshold should filter out very low matches
211
+ for (const suggestion of suggestions) {
212
+ assert.ok(suggestion.score >= 0.3, 'Default threshold should be at least 0.3');
213
+ }
214
+ });
215
+ });
216
+ describe('SkillSuggestion interface', () => {
217
+ it('should include name, description, score, and reason', async () => {
218
+ // Verify the shape of returned suggestions
219
+ const userInput = 'write tests';
220
+ const suggestions = await suggestSkills(userInput);
221
+ assert.ok(suggestions.length > 0, 'Should have suggestions');
222
+ const suggestion = suggestions[0];
223
+ assert.ok('name' in suggestion, 'Should have name');
224
+ assert.ok('description' in suggestion, 'Should have description');
225
+ assert.ok('score' in suggestion, 'Should have score');
226
+ assert.ok('reason' in suggestion, 'Should have reason');
227
+ });
228
+ it('should optionally include matchedOn for debugging', async () => {
229
+ // Shows what triggered the suggestion
230
+ const userInput = 'run jest tests';
231
+ const suggestions = await suggestSkills(userInput);
232
+ const testingSuggestion = suggestions.find(s => s.name === 'testing');
233
+ if (testingSuggestion) {
234
+ assert.ok('matchedOn' in testingSuggestion, 'Should include matchedOn field');
235
+ assert.ok(Array.isArray(testingSuggestion.matchedOn), 'matchedOn should be array');
236
+ }
237
+ });
238
+ });
239
+ describe('Error handling', () => {
240
+ it('should handle invalid input gracefully', async () => {
241
+ // Empty input
242
+ const suggestions = await suggestSkills('');
243
+ assert.ok(Array.isArray(suggestions), 'Should return array for empty input');
244
+ assert.strictEqual(suggestions.length, 0, 'Should return empty for empty input');
245
+ });
246
+ it('should handle missing registry gracefully', async () => {
247
+ // If registry not found, should return empty (not crash)
248
+ const options = {
249
+ registryPath: '/nonexistent/path/registry.yaml',
250
+ };
251
+ const suggestions = await suggestSkills('test something', options);
252
+ // Should gracefully return empty, not throw
253
+ assert.ok(Array.isArray(suggestions), 'Should return array even with missing registry');
254
+ });
255
+ });
256
+ });
257
+ //# sourceMappingURL=skill-suggest.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-suggest.test.js","sourceRoot":"","sources":["../src/skill-suggest.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Theme Loader - Unified theme discovery, loading, and metadata for Pennyfarthing
3
+ *
4
+ * This is the ONE canonical implementation of theme loading. All other packages
5
+ * (core CLI, cyclist, Python scripts, bash scripts) should delegate here.
6
+ *
7
+ * Discovery algorithm (checked in order, deduped by theme ID):
8
+ * 1. Core themes: resolvePennyfarthingDist()/personas/themes/
9
+ * 2. Theme packages: node_modules/@pennyfarthing/themes-* /themes/
10
+ * 3. Project custom: {projectRoot}/.claude/pennyfarthing/themes/
11
+ * 4. User custom: ~/.claude/pennyfarthing/themes/
12
+ */
13
+ export interface ThemeAgent {
14
+ character: string;
15
+ style: string;
16
+ role: string;
17
+ trait: string;
18
+ catchphrases: string[];
19
+ helper?: string;
20
+ }
21
+ export interface Theme {
22
+ name: string;
23
+ description: string;
24
+ agents: Record<string, ThemeAgent>;
25
+ }
26
+ export interface ThemePackageInfo {
27
+ packageName: string;
28
+ themesDir: string;
29
+ portraitsDir: string;
30
+ }
31
+ export interface ThemeMetadata {
32
+ id: string;
33
+ name: string;
34
+ description: string;
35
+ source: string;
36
+ tier: 'S' | 'A' | 'B' | 'U';
37
+ category: string;
38
+ agentCount: number;
39
+ }
40
+ export declare const CATEGORY_MAP: Record<string, string>;
41
+ /**
42
+ * Derive category from theme ID and source text.
43
+ * Uses CATEGORY_MAP for known themes, falls back to pattern matching.
44
+ */
45
+ export declare function deriveCategory(themeId: string, source: string): string;
46
+ /**
47
+ * Discover installed @pennyfarthing/themes-* packages.
48
+ * Walks up from projectRoot looking for node_modules/@pennyfarthing/,
49
+ * then checks each themes-* directory for a valid theme pack.
50
+ */
51
+ export declare function discoverThemePackages(projectRoot?: string): ThemePackageInfo[];
52
+ /**
53
+ * Discover all directories containing theme YAML files.
54
+ * Returns dirs in priority order (core, theme packages, project custom, user custom).
55
+ */
56
+ export declare function discoverAllThemeDirs(projectRoot?: string): string[];
57
+ /**
58
+ * Resolve the file path for a specific theme across all sources.
59
+ * Returns the first match found in priority order, or null.
60
+ */
61
+ export declare function resolveThemePath(themeId: string, projectRoot?: string): string | null;
62
+ /**
63
+ * Load metadata for all discoverable themes.
64
+ * Deduplicates by theme ID (first source wins).
65
+ */
66
+ export declare function loadAllThemeMetadata(projectRoot?: string): ThemeMetadata[];
67
+ /**
68
+ * Load a theme configuration by name.
69
+ * Searches all theme sources in priority order.
70
+ */
71
+ export declare function loadTheme(themeName: string): Theme | null;
72
+ /**
73
+ * List all available theme IDs across all sources.
74
+ * Deduplicates — first source wins.
75
+ */
76
+ export declare function listThemes(projectRoot?: string): string[];
77
+ /**
78
+ * Get agent persona from a theme.
79
+ */
80
+ export declare function getAgentPersona(themeName: string, agentName: string): ThemeAgent | null;
81
+ //# sourceMappingURL=theme-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-loader.d.ts","sourceRoot":"","sources":["../src/theme-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAYH,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACpC;AAMD,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AA8BD,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAgK/C,CAAC;AAEF;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CA0CtE;AAMD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAoF9E;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAmCnE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASrF;AAMD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,aAAa,EAAE,CAyC1E;AAMD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CA8BzD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAsBzD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAMvF"}