@hachej/boring-workspace 0.1.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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/dist/CodeEditor-DQqOn4xz.js +266 -0
  4. package/dist/CommandPalette-aM61U-b0.js +5229 -0
  5. package/dist/FileTree-DRq_bfue.js +245 -0
  6. package/dist/MarkdownEditor-DjiHxnRv.js +349 -0
  7. package/dist/WorkspaceLoadingState-By0dZoPD.js +568 -0
  8. package/dist/agent-tool-NvxKfist.d.ts +28 -0
  9. package/dist/app-front.d.ts +485 -0
  10. package/dist/app-front.js +452 -0
  11. package/dist/app-server.d.ts +53 -0
  12. package/dist/app-server.js +769 -0
  13. package/dist/bootstrapServer-BRUqUpVW.d.ts +66 -0
  14. package/dist/boring-workspace.css +1 -0
  15. package/dist/charts.d.ts +114 -0
  16. package/dist/charts.js +143 -0
  17. package/dist/events.d.ts +178 -0
  18. package/dist/events.js +88 -0
  19. package/dist/explorer-DtLUnuah.d.ts +129 -0
  20. package/dist/panel-DnvDNQac.js +6 -0
  21. package/dist/server.d.ts +84 -0
  22. package/dist/server.js +811 -0
  23. package/dist/shared.d.ts +113 -0
  24. package/dist/shared.js +11 -0
  25. package/dist/testing-e2e.d.ts +68 -0
  26. package/dist/testing-e2e.js +45 -0
  27. package/dist/testing.d.ts +464 -0
  28. package/dist/testing.js +10984 -0
  29. package/dist/utils-B6yFEsav.js +8 -0
  30. package/dist/workspace.css +5780 -0
  31. package/dist/workspace.d.ts +2119 -0
  32. package/dist/workspace.js +1884 -0
  33. package/docs/INTERFACES.md +58 -0
  34. package/docs/PLUGIN_STRUCTURE.md +162 -0
  35. package/docs/README.md +19 -0
  36. package/docs/bridge.md +135 -0
  37. package/docs/panels.md +102 -0
  38. package/docs/plans/GENERIC_EXPLORER_PLUGIN_PLAN.md +455 -0
  39. package/docs/plans/MACRO_PLUGIN_GENERIC_HELPERS_AUDIT.md +962 -0
  40. package/docs/plans/PLUGIN_OUTPUTS_ISOLATION_PLAN.md +301 -0
  41. package/docs/plans/README.md +9 -0
  42. package/docs/plans/UI_BRIDGE_OWNERSHIP_REFACTOR.md +303 -0
  43. package/docs/plans/archive/CODE_OWNERSHIP_CLEANUP_PLAN.md +387 -0
  44. package/docs/plans/archive/COMMAND_PALETTE_REGISTRY.md +814 -0
  45. package/docs/plans/archive/DECLARATIVE_LAYOUT_MIGRATION.md +277 -0
  46. package/docs/plans/archive/PLUGIN_MODEL.md +3674 -0
  47. package/docs/plans/archive/SRC_FOLDER_REORG_PLAN.md +307 -0
  48. package/docs/plans/archive/UNIFIED_EVENT_BUS.md +647 -0
  49. package/docs/plans/archive/WORKSPACE_V2_PLAN.md +2489 -0
  50. package/docs/plugins.md +158 -0
  51. package/package.json +164 -0
@@ -0,0 +1,962 @@
1
+ # Macro Plugin Generic Helpers Audit
2
+
3
+ Last updated: 2026-05-01
4
+
5
+ Status: draft audit / extraction plan. No implementation yet.
6
+
7
+ ## Goal
8
+
9
+ Inspect `apps/boring-macro-v2/src/plugins/macro` in detail and identify which
10
+ pieces are truly macro-domain logic versus reusable workspace/plugin mechanics.
11
+ The aim is to reduce boilerplate in macro and future apps without moving macro
12
+ meaning into `@boring/workspace`.
13
+
14
+ This plan complements `GENERIC_EXPLORER_PLUGIN_PLAN.md`: macro is a concrete
15
+ consumer that should pressure-test generic explorer, surface resolver, open
16
+ surface, event, and REST adapter helpers.
17
+
18
+ ## Current Macro Plugin Shape
19
+
20
+ ```txt
21
+ apps/boring-macro-v2/src/plugins/macro/
22
+ index.tsx # client plugin composition + chat suggestions
23
+ constants.ts # macro ids, panel ids, surface kinds
24
+ panels.tsx # definePanel wrappers for chart/deck
25
+ surfaceResolver.ts # macro surface routing rules
26
+ catalogs.ts # data catalog output options
27
+ data/
28
+ macroSeriesAdapter.ts # REST -> ExplorerAdapter
29
+ macroSeriesData.ts # series fetch/cache for panes
30
+ macroSeriesTypes.ts # series payload types
31
+ macroSeriesUi.ts # frequency labels, colors, openSeriesPane
32
+ panels/
33
+ ChartCanvasPane.tsx # chart UI
34
+ DeckPane.tsx # markdown deck UI + embeds
35
+ routes/
36
+ StandaloneDeckRoute.tsx # app route component
37
+ server/
38
+ index.ts # server plugin, prompt, provisioning, routes
39
+ routes/macro.ts # Fastify API routes
40
+ services/* # ClickHouse/FRED domain services
41
+ tools/macroTools.ts # agent tools
42
+ sdk/, transforms/, workspace-template/
43
+ ```
44
+
45
+ ## Ownership Rule
46
+
47
+ Move generic mechanics only when at least two plugins/apps can use them.
48
+ Keep macro semantics in macro.
49
+
50
+ Macro keeps:
51
+
52
+ - economic series concepts
53
+ - FRED/ClickHouse schema details
54
+ - chart/deck panels
55
+ - macro surface kinds and panel ids
56
+ - macro system prompt content
57
+ - macro provisioning templates / Python SDK / builtins
58
+ - macro event names and payload meanings
59
+
60
+ Workspace/generic helpers may own:
61
+
62
+ - simple plugin composition from child plugins
63
+ - generic REST-to-`ExplorerAdapter` plumbing
64
+ - generic static rows adapter plumbing
65
+ - generic `openSurface` command helper
66
+ - generic surface resolver builders
67
+ - generic typed event namespace helpers
68
+ - generic path display normalization helpers, where safe
69
+
70
+ Avoid a large enhancer/mixin framework. Prefer normal plugins composing other
71
+ normal plugins through one small `composePlugins()` helper.
72
+
73
+ ## Preferred Generic Plugin Composition Model
74
+
75
+ The simplest general model is: plugins compose plugins.
76
+
77
+ Instead of a broad enhancer framework such as `withPanels()`,
78
+ `withCatalogs()`, `withDataCatalog()`, etc., add one small helper that flattens
79
+ child plugin contributions into a parent plugin:
80
+
81
+ ```ts
82
+ export function composePlugins(options: {
83
+ id: string;
84
+ label?: string;
85
+ plugins: Plugin[];
86
+ outputs?: PluginOutput[];
87
+ panels?: PanelConfig[];
88
+ catalogs?: CatalogConfig[];
89
+ commands?: CommandConfig[];
90
+ adoptOutputs?: boolean;
91
+ }): Plugin;
92
+ ```
93
+
94
+ Mental model:
95
+
96
+ ```ts
97
+ const macroPlugin = composePlugins({
98
+ id: MACRO_PLUGIN_ID,
99
+ label: "Macro",
100
+ plugins: [macroPanelsPlugin, macroSurfacesPlugin, macroSeriesCatalogPlugin],
101
+ });
102
+ ```
103
+
104
+ This keeps every feature behind the same plugin interface. Data catalog remains
105
+ a plugin; surfaces can be a tiny plugin; app domains can publish smaller plugin
106
+ fragments and compose them into one app plugin.
107
+
108
+ ### Composition semantics
109
+
110
+ Default recommendation: **parent adopts child outputs**.
111
+
112
+ When `adoptOutputs !== false`, composed outputs should register as owned by the
113
+ parent plugin id at bootstrap time. This matches app-domain expectations: the
114
+ macro catalog is part of the Macro plugin even if it was created by a reusable
115
+ data catalog plugin factory.
116
+
117
+ When `adoptOutputs === false`, child plugin ids are preserved. This is useful
118
+ for independent third-party plugin bundles where inspector/debug output should
119
+ show original child ownership.
120
+
121
+ `composePlugins()` should:
122
+
123
+ - flatten `outputs`, legacy `panels`, `catalogs`, `commands`, bindings, and
124
+ other contribution arrays from child plugins
125
+ - preserve deterministic order: child plugins in array order, then explicit
126
+ parent outputs
127
+ - detect duplicate contribution ids early where possible
128
+ - not invent a new lifecycle model
129
+ - not deeply clone components/functions
130
+ - leave server plugin composition as a separate concern unless the same simple
131
+ shape naturally applies there
132
+
133
+ This general composition helper can replace many ad hoc `appendXOutputs()`
134
+ patterns over time while keeping existing helpers as compatibility wrappers.
135
+
136
+ ## File-by-file Audit
137
+
138
+ ### `constants.ts`
139
+
140
+ Current:
141
+
142
+ ```ts
143
+ export const MACRO_PLUGIN_ID = "boring-macro";
144
+ export const MACRO_CHART_PANEL_ID = "chart-canvas";
145
+ export const MACRO_DECK_PANEL_ID = "deck";
146
+ export const MACRO_SERIES_SURFACE_RESOLVER_ID = "boring-macro-series";
147
+ export const MACRO_DECK_SURFACE_RESOLVER_ID = "boring-macro-deck-path";
148
+ export const MACRO_OPEN_SERIES_SURFACE_KIND = "macro.open-series";
149
+ ```
150
+
151
+ Decision: keep in macro.
152
+
153
+ Potential generic improvement: add optional helper conventions for deriving ids:
154
+
155
+ ```ts
156
+ createPluginIds("boring-macro", {
157
+ panels: ["chart-canvas", "deck"],
158
+ surfaces: ["open-series"],
159
+ resolvers: ["series", "deck-path"],
160
+ });
161
+ ```
162
+
163
+ Do **not** implement unless id drift becomes common. Current constants are clear
164
+ and low boilerplate.
165
+
166
+ ### `panels.tsx`
167
+
168
+ Current boilerplate:
169
+
170
+ ```ts
171
+ export const chartCanvasPanel = definePanel({
172
+ id: MACRO_CHART_PANEL_ID,
173
+ title: "Chart",
174
+ component: ChartCanvasPane,
175
+ placement: "center",
176
+ source: "app",
177
+ });
178
+ ```
179
+
180
+ Decision: mostly keep. `definePanel()` already handles generic panel shape.
181
+
182
+ Possible helper only if repeated heavily:
183
+
184
+ ```ts
185
+ createCenterPanel({ id, title, component, source: "app" });
186
+ ```
187
+
188
+ Not worth extracting now. Panel declarations are readable and explicit.
189
+
190
+ ### `index.tsx`
191
+
192
+ Current responsibilities:
193
+
194
+ - exports `MacroStandaloneDeckRoute`
195
+ - declares `macroChatSuggestions`
196
+ - declares shell options
197
+ - composes plugin outputs:
198
+ - chart panel
199
+ - deck panel
200
+ - macro surface resolvers
201
+ - data catalog outputs via `appendDataCatalogOutputs()`
202
+
203
+ Good current pattern:
204
+
205
+ ```ts
206
+ return appendDataCatalogOutputs(
207
+ plugin,
208
+ createMacroSeriesDataCatalogOptions(onSeriesSelect),
209
+ );
210
+ ```
211
+
212
+ Decision: keep macro composition here until `composePlugins()` exists, then
213
+ prefer composing smaller plugin fragments:
214
+
215
+ ```ts
216
+ const macroPanelsPlugin = definePlugin({
217
+ id: "macro-panels",
218
+ outputs: [
219
+ { type: "panel", panel: chartCanvasPanel },
220
+ { type: "panel", panel: deckPanel },
221
+ ],
222
+ });
223
+
224
+ const macroSurfacesPlugin = definePlugin({
225
+ id: "macro-surfaces",
226
+ outputs: macroSurfaceOutputs,
227
+ });
228
+
229
+ const macroSeriesCatalogPlugin = createDataCatalogPlugin({
230
+ ...createMacroSeriesDataCatalogOptions(onSeriesSelect),
231
+ pluginId: "macro-series-catalog",
232
+ });
233
+
234
+ return composePlugins({
235
+ id: MACRO_PLUGIN_ID,
236
+ label: "Macro",
237
+ plugins: [macroPanelsPlugin, macroSurfacesPlugin, macroSeriesCatalogPlugin],
238
+ });
239
+ ```
240
+
241
+ This is simpler than adding many specialized enhancer functions. Existing
242
+ `appendDataCatalogOutputs()` can remain as compatibility sugar implemented on
243
+ top of `composePlugins()` later.
244
+
245
+ Possible generic improvements:
246
+
247
+ 1. **Chat suggestion type export**
248
+
249
+ Macro defines a local `MacroChatSuggestion` that probably mirrors a workspace
250
+ chat suggestion shape. If `WorkspaceProvider` already owns a compatible
251
+ public type, macro should import it. If not, expose one:
252
+
253
+ ```ts
254
+ export interface ChatSuggestion {
255
+ label: string;
256
+ hint?: string;
257
+ icon?: ComponentType<{ className?: string }>;
258
+ prompt?: string;
259
+ }
260
+ ```
261
+
262
+ This avoids every app re-declaring the same suggestion type.
263
+
264
+ 2. **Shell options type**
265
+
266
+ `macroShellOptions` is app-shell config, not plugin-domain behavior. If more
267
+ apps duplicate this object shape, expose a `WorkspaceShellOptions` type from
268
+ workspace/app composition docs. Do not move macro values.
269
+
270
+ ### `catalogs.ts`
271
+
272
+ Current responsibilities:
273
+
274
+ - owns macro facets: frequency/source
275
+ - creates macro `CreateDataCatalogOutputsOptions`
276
+ - wires adapter, labels, groupBy, drag payload, select callback
277
+
278
+ Decision: keep macro-specific options here, but generic helpers can reduce
279
+ boilerplate.
280
+
281
+ Generic candidates:
282
+
283
+ 1. **Data catalog preset helper**
284
+
285
+ Current repeated shape:
286
+
287
+ ```ts
288
+ {
289
+ id,
290
+ label: "Data",
291
+ adapter,
292
+ facets,
293
+ groupBy,
294
+ onSelect,
295
+ leftTabId,
296
+ leftTabTitle: "Data",
297
+ catalogId,
298
+ catalogLabel,
299
+ includeVisualizationPanel: false,
300
+ emptyState,
301
+ searchPlaceholder,
302
+ getDragPayload,
303
+ }
304
+ ```
305
+
306
+ Could become:
307
+
308
+ ```ts
309
+ createDataCatalogPreset({
310
+ id: "macro-series",
311
+ catalogLabel: "Macro Series",
312
+ adapter: macroAdapter,
313
+ facets: MACRO_FACETS,
314
+ groupBy: "frequency",
315
+ onSelect,
316
+ emptyState: "No series match",
317
+ searchPlaceholder: "Search...",
318
+ dragMimeType: "text/series-id",
319
+ visualization: false,
320
+ });
321
+ ```
322
+
323
+ Owner: `dataCatalogPlugin`, not macro.
324
+
325
+ 2. **Drag payload helper**
326
+
327
+ ```ts
328
+ getDragPayload: createTextDragPayload("text/series-id", (row) => row.id);
329
+ ```
330
+
331
+ Owner: future `explorerPlugin` or `DataExplorer/adapters`.
332
+
333
+ 3. **Facet config helper**
334
+
335
+ `MACRO_FACETS` is domain-specific, but a helper can encode order + formatter:
336
+
337
+ ```ts
338
+ facet("frequency", "Frequency", { order, labels: FREQ_LABELS });
339
+ ```
340
+
341
+ Only extract if multiple apps repeat this pattern. Low priority.
342
+
343
+ ### `data/macroSeriesAdapter.ts`
344
+
345
+ Current responsibilities:
346
+
347
+ - maps macro API rows to `ExplorerRow`
348
+ - builds query strings
349
+ - fetches `/api/macro/catalog` and `/api/macro/facets`
350
+ - maps explorer search args to query params
351
+ - maps facets response
352
+
353
+ Decision: extract generic REST adapter plumbing; keep macro row mapping and URLs
354
+ in macro.
355
+
356
+ Generic helper proposal:
357
+
358
+ ```ts
359
+ export function createRestExplorerAdapter<ApiRow, FacetsResponse>(options: {
360
+ searchUrl: string | ((args: ExplorerSearchArgs) => string);
361
+ facetsUrl?: string | ((args: ExplorerFacetsArgs) => string);
362
+ mapRow: (row: ApiRow) => ExplorerRow;
363
+ mapSearchArgs?: (
364
+ args: ExplorerSearchArgs,
365
+ ) => Record<string, string | number | string[] | undefined>;
366
+ mapFacetArgs?: (
367
+ args: ExplorerFacetsArgs,
368
+ ) => Record<string, string | number | string[] | undefined>;
369
+ mapSearchResponse?: (json: unknown) => {
370
+ items: ApiRow[];
371
+ total: number;
372
+ hasMore: boolean;
373
+ };
374
+ mapFacetsResponse?: (json: FacetsResponse) => Facets;
375
+ fetch?: typeof globalThis.fetch;
376
+ }): ExplorerAdapter;
377
+ ```
378
+
379
+ Macro after extraction:
380
+
381
+ ```ts
382
+ export function createMacroSeriesAdapter(): ExplorerAdapter {
383
+ return createRestExplorerAdapter<CatalogItem, FacetsResponse>({
384
+ searchUrl: "/api/macro/catalog",
385
+ facetsUrl: "/api/macro/facets",
386
+ mapRow: toMacroSeriesRow,
387
+ mapSearchArgs: (args) => ({
388
+ q: args.query || undefined,
389
+ offset: args.offset,
390
+ limit: args.limit,
391
+ group: args.group?.value,
392
+ frequency: args.filters.frequency,
393
+ source: args.filters.source,
394
+ }),
395
+ mapFacetArgs: (args) => ({
396
+ frequency: args.filters.frequency,
397
+ source: args.filters.source,
398
+ }),
399
+ });
400
+ }
401
+ ```
402
+
403
+ Also extract generic query helper:
404
+
405
+ ```ts
406
+ toQueryString(params: Record<string, string | number | string[] | undefined>): string
407
+ ```
408
+
409
+ Owner: `explorerPlugin/adapters.ts` once created. Temporary owner can be
410
+ `front/components/DataExplorer/adapters.ts`.
411
+
412
+ Risk: don't overfit response shape. Keep mapper hooks explicit.
413
+
414
+ ### `data/macroSeriesUi.ts`
415
+
416
+ Current responsibilities:
417
+
418
+ - `FREQ_LABELS`: macro/domain display labels
419
+ - `SERIES_COLORS`: chart palette
420
+ - `formatSeriesValue`: numeric display helper
421
+ - `openSeriesPane`: posts openSurface command for macro series
422
+
423
+ Decision:
424
+
425
+ - keep labels/colors in macro
426
+ - consider moving `formatSeriesValue` only if many apps need generic numeric
427
+ compact formatting; otherwise keep macro
428
+ - extract generic `openSurface` command builders
429
+
430
+ Generic open helpers:
431
+
432
+ ```ts
433
+ export function openSurface(options: {
434
+ kind: string;
435
+ target: string;
436
+ meta?: Record<string, unknown>;
437
+ }): void;
438
+
439
+ export function createOpenSurfaceHandler<Input>(options: {
440
+ kind: string;
441
+ getTarget: (input: Input) => string | undefined;
442
+ getMeta?: (input: Input) => Record<string, unknown> | undefined;
443
+ }): (input: Input) => void;
444
+
445
+ export function createOpenSurfaceRowHandler(options: {
446
+ kind: string;
447
+ catalogId?: string;
448
+ getTarget?: (row: ExplorerRow) => string | undefined;
449
+ getMeta?: (row: ExplorerRow) => Record<string, unknown> | undefined;
450
+ }): (row: ExplorerRow) => void;
451
+ ```
452
+
453
+ Macro after extraction:
454
+
455
+ ```ts
456
+ export const openSeriesPane = createOpenSurfaceHandler<string>({
457
+ kind: MACRO_OPEN_SERIES_SURFACE_KIND,
458
+ getTarget: (seriesId) => seriesId.trim() || undefined,
459
+ getMeta: (_seriesId, opts) => ... // if supporting options, use a two-arg helper
460
+ })
461
+ ```
462
+
463
+ Because `openSeriesPane(seriesId, opts)` currently takes two args, either keep a
464
+ small macro wrapper or make the generic helper support tuple input. Prefer small
465
+ wrapper for readability.
466
+
467
+ ### `surfaceResolver.ts`
468
+
469
+ Current responsibilities:
470
+
471
+ - generic target trimming / title fallback / panel resolution boilerplate
472
+ - generic path normalization / basename
473
+ - macro-specific series routing
474
+ - macro-specific deck markdown path matching
475
+
476
+ Decision: best immediate extraction candidate after explorer adapter.
477
+
478
+ Generic helper 1: target surface resolver
479
+
480
+ ```ts
481
+ export function createTargetSurfaceResolver<
482
+ Target extends string = string,
483
+ >(options: {
484
+ id: string;
485
+ source?: PanelConfig["source"];
486
+ kind: string;
487
+ component: string;
488
+ score?: number;
489
+ normalizeTarget?: (target: string) => Target | undefined;
490
+ getPanelId?: (target: Target, request: SurfaceOpenRequest) => string;
491
+ getTitle?: (
492
+ target: Target,
493
+ request: SurfaceOpenRequest,
494
+ ) => string | undefined;
495
+ getParams?: (
496
+ target: Target,
497
+ request: SurfaceOpenRequest,
498
+ ) => Record<string, unknown>;
499
+ }): SurfaceResolverConfig;
500
+ ```
501
+
502
+ Generic helper 2: path surface resolver
503
+
504
+ ```ts
505
+ export function createPathSurfaceResolver(options: {
506
+ id: string;
507
+ source?: PanelConfig["source"];
508
+ kind?: string; // default WORKSPACE_OPEN_PATH_SURFACE_KIND
509
+ component: string;
510
+ score?: number;
511
+ matches: (path: string, request: SurfaceOpenRequest) => boolean;
512
+ getPanelId?: (path: string, request: SurfaceOpenRequest) => string;
513
+ getTitle?: (path: string, request: SurfaceOpenRequest) => string;
514
+ getParams?: (
515
+ path: string,
516
+ request: SurfaceOpenRequest,
517
+ ) => Record<string, unknown>;
518
+ }): SurfaceResolverConfig;
519
+ ```
520
+
521
+ Generic helper 3: safe display path helpers
522
+
523
+ ```ts
524
+ normalizeSurfacePath(path: string): string
525
+ basename(path: string): string
526
+ ```
527
+
528
+ Important: these are **not** security validators. Path validation remains the
529
+ adapter/server/filesystem plugin's job.
530
+
531
+ Macro after extraction:
532
+
533
+ ```ts
534
+ export const macroSurfaceOutputs = surfaceResolverOutputs([
535
+ createTargetSurfaceResolver({
536
+ id: MACRO_SERIES_SURFACE_RESOLVER_ID,
537
+ source: "app",
538
+ kind: MACRO_OPEN_SERIES_SURFACE_KIND,
539
+ component: MACRO_CHART_PANEL_ID,
540
+ normalizeTarget: trimNonEmpty,
541
+ getPanelId: (seriesId) => `chart:${seriesId}`,
542
+ getTitle: (seriesId, request) =>
543
+ readStringMeta(request.meta, "title") ?? seriesId,
544
+ getParams: (seriesId) => ({ seriesId }),
545
+ }),
546
+ createPathSurfaceResolver({
547
+ id: MACRO_DECK_SURFACE_RESOLVER_ID,
548
+ source: "app",
549
+ component: MACRO_DECK_PANEL_ID,
550
+ score: 10,
551
+ matches: isDeckMarkdownPath,
552
+ getPanelId: (path) => `file:${path}`,
553
+ getTitle: basename,
554
+ getParams: (path) => ({ path }),
555
+ }),
556
+ ]);
557
+ ```
558
+
559
+ Could also add:
560
+
561
+ ```ts
562
+ surfaceResolverOutputs(resolvers): PluginOutput[]
563
+ ```
564
+
565
+ Owner: `front/registry/surfaceResolverHelpers.ts` or future
566
+ `plugins/explorerPlugin/surface.ts`. Since these helpers are not explorer-only,
567
+ prefer `front/registry` or a new `front/surface` module.
568
+
569
+ ### `data/macroSeriesData.ts`
570
+
571
+ Current responsibilities:
572
+
573
+ - fetch series payload by id
574
+ - in-memory cache
575
+ - in-flight de-dup
576
+ - reset cache
577
+
578
+ Decision: possible generic cache helper, but not priority.
579
+
580
+ Generic helper:
581
+
582
+ ```ts
583
+ createResourceCache<Key, Value>({
584
+ load: (key) => Promise<Value>,
585
+ keyToString?: (key) => string,
586
+ })
587
+ ```
588
+
589
+ Macro after extraction:
590
+
591
+ ```ts
592
+ const seriesCache = createResourceCache({
593
+ load: async (seriesId: string) => { ...fetch macro series... },
594
+ })
595
+ export const fetchMacroSeries = seriesCache.fetch
596
+ export const clearMacroSeriesCache = seriesCache.clear
597
+ ```
598
+
599
+ Risk: introducing a cache abstraction may hide app-specific invalidation rules.
600
+ Leave until another plugin duplicates it.
601
+
602
+ ### `data/macroSeriesTypes.ts`
603
+
604
+ Decision: keep in macro. Domain payload.
605
+
606
+ ### `panels/ChartCanvasPane.tsx` and `panels/DeckPane.tsx`
607
+
608
+ Decision: keep in macro. Domain UI.
609
+
610
+ Potential generic extraction only if repeated:
611
+
612
+ - empty/loading/error panel state components
613
+ - chart color palette? no; macro-specific enough
614
+ - markdown deck embed mechanics? likely domain/app-specific until a separate
615
+ deck plugin exists
616
+
617
+ ### `server/index.ts`
618
+
619
+ Current responsibilities:
620
+
621
+ - server plugin object
622
+ - routes registration
623
+ - provisioning declarations
624
+ - macro system prompt
625
+ - macro tools
626
+
627
+ Decision: keep macro domain, but generic helpers can reduce boilerplate.
628
+
629
+ Generic candidates:
630
+
631
+ 1. **Server plugin factory helper**
632
+
633
+ ```ts
634
+ createServerPlugin({
635
+ id,
636
+ label,
637
+ routes,
638
+ agentTools,
639
+ provisioning,
640
+ systemPrompt,
641
+ });
642
+ ```
643
+
644
+ Only useful if many server plugins repeat the exact shape. Low priority.
645
+
646
+ 2. **System prompt builder for surface contracts**
647
+
648
+ Macro prompt manually documents:
649
+
650
+ ```txt
651
+ call exec_ui with kind "openSurface" and params { kind, target, meta }
652
+ ```
653
+
654
+ Data catalog server helper already generates similar prompt text. Extract a
655
+ shared prompt helper:
656
+
657
+ ```ts
658
+ createOpenSurfacePrompt({
659
+ surfaceKind: MACRO_OPEN_SERIES_SURFACE_KIND,
660
+ targetDescription: "series_id",
661
+ metaExample: "{ title }",
662
+ });
663
+ ```
664
+
665
+ Owner: workspace server UI-control or dataCatalogPlugin server utilities.
666
+
667
+ 3. **Provisioning type**
668
+
669
+ Macro declares local `MacroProvisioningContribution`. If workspace server plugin
670
+ already has a canonical provisioning type, import it. If not, add one. This
671
+ prevents local drift in app server plugins.
672
+
673
+ ### `server/tools/macroTools.ts`
674
+
675
+ Current responsibilities:
676
+
677
+ - ClickHouse read-only SQL guard
678
+ - tool result formatting
679
+ - macro search/data/derived tools
680
+ - SQL hinting
681
+
682
+ Decision: mostly keep in macro.
683
+
684
+ Generic candidates:
685
+
686
+ 1. `textResult()` / `errorResult()` helpers already exist in other workspace
687
+ server code patterns. Export shared helpers from a server utilities module:
688
+
689
+ ```ts
690
+ textToolResult(text);
691
+ errorToolResult(text);
692
+ ```
693
+
694
+ 2. read-only SQL guard might be reusable for DB plugins, but ClickHouse hints
695
+ are macro-specific. If extracted, keep generic guard separate:
696
+
697
+ ```ts
698
+ assertReadonlySql(sql, { allowed: ["SELECT", "WITH", ...] })
699
+ ```
700
+
701
+ Low priority unless another DB plugin appears.
702
+
703
+ ### `server/routes/macro.ts`
704
+
705
+ Current responsibilities:
706
+
707
+ - query param parsing/clamping
708
+ - auth dev bypass
709
+ - macro catalog/facets/series/deck/refresh routes
710
+ - filesystem deck route operations
711
+
712
+ Generic candidates:
713
+
714
+ 1. Query helpers:
715
+
716
+ ```ts
717
+ parseCommaSep();
718
+ clampInt();
719
+ optionalInt();
720
+ ```
721
+
722
+ Could move to workspace server route utils if repeated. Low priority.
723
+
724
+ 2. Catalog/facets REST contract:
725
+
726
+ If `createRestExplorerAdapter` becomes a workspace helper, document the expected
727
+ server response shape `{ items, total, hasMore }`. Do not move macro routes.
728
+
729
+ 3. Dev localhost bypass is app/server policy. Do not move unless core/cloud adds
730
+ a canonical dev-auth helper.
731
+
732
+ ## Event Handling Audit
733
+
734
+ Macro currently uses workspace event bus indirectly:
735
+
736
+ - `openSeriesPane()` posts `workspace:ui.command` through `postUiCommand()`.
737
+ - tests subscribe to `events.on("workspace:ui.command", ...)`.
738
+ - filesystem/agent events are owned by workspace/filesystem plugin, not macro.
739
+
740
+ No macro-specific event namespace exists yet.
741
+
742
+ Generic improvements:
743
+
744
+ 1. **Avoid raw event-name strings in tests**
745
+
746
+ Test currently uses:
747
+
748
+ ```ts
749
+ const UI_COMMAND_EVENT = "workspace:ui.command";
750
+ ```
751
+
752
+ Prefer exporting/importing the canonical event key if available:
753
+
754
+ ```ts
755
+ import { workspaceEvents } from "@boring/workspace/events";
756
+ ```
757
+
758
+ If not exported, expose it. Raw event strings in app tests drift easily.
759
+
760
+ 2. **Plugin event namespace helper**
761
+
762
+ When macro adds domain events, use:
763
+
764
+ ```ts
765
+ export const macroEvents = createPluginEventNamespace(MACRO_PLUGIN_ID, {
766
+ seriesOpened: "series.opened",
767
+ transformCompleted: "transform.completed",
768
+ deckSaved: "deck.saved",
769
+ });
770
+ ```
771
+
772
+ This helper should:
773
+
774
+ - prefix event names with plugin id
775
+ - preserve literal types
776
+ - avoid collisions
777
+ - avoid importing app/domain events into workspace core
778
+
779
+ 3. **Typed plugin event map augmentation**
780
+
781
+ If workspace supports augmentable event maps, macro can own event payload types:
782
+
783
+ ```ts
784
+ declare module "@boring/workspace/events" {
785
+ interface WorkspacePluginEventMap {
786
+ "boring-macro:series.opened": { seriesId: string; title?: string };
787
+ }
788
+ }
789
+ ```
790
+
791
+ Only implement when macro actually emits domain events.
792
+
793
+ ## Surface Handling Audit
794
+
795
+ Surface handling is the biggest near-term boilerplate win.
796
+
797
+ Current macro has two resolver patterns:
798
+
799
+ 1. target resolver: surface kind + string target -> panel
800
+ 2. path resolver: workspace path surface + path predicate -> panel
801
+
802
+ These patterns will recur in Feret:
803
+
804
+ - ingredient id -> ingredient detail panel
805
+ - formulation id -> formulation panel
806
+ - source file path -> extraction review panel
807
+ - project path -> custom markdown/source preview panel
808
+
809
+ Generic resolver helpers should be introduced before Feret repeats the macro
810
+ boilerplate.
811
+
812
+ Recommended owner:
813
+
814
+ ```txt
815
+ packages/workspace/src/front/registry/surfaceResolverHelpers.ts
816
+ ```
817
+
818
+ Exports from package root:
819
+
820
+ ```ts
821
+ createTargetSurfaceResolver;
822
+ createPathSurfaceResolver;
823
+ surfaceResolverOutputs;
824
+ normalizeSurfacePath;
825
+ surfaceBasename;
826
+ readStringMeta;
827
+ ```
828
+
829
+ Risk: this lives under `front/registry` but helpers may be useful in app plugins.
830
+ That is acceptable because app plugins already import `definePanel` from front
831
+ registry types via package root. Do not put these in `shared/` if they depend on
832
+ React/panel component types. If kept type-only and browser-safe, `shared/types`
833
+ may be okay later.
834
+
835
+ ## Data Catalog / Explorer Integration Audit
836
+
837
+ Macro already correctly composes data catalog outputs:
838
+
839
+ ```ts
840
+ appendDataCatalogOutputs(
841
+ plugin,
842
+ createMacroSeriesDataCatalogOptions(onSeriesSelect),
843
+ );
844
+ ```
845
+
846
+ Better integration opportunities:
847
+
848
+ 1. `composePlugins()` so macro can compose a real `createDataCatalogPlugin()`
849
+ child plugin rather than mutating/appending outputs onto a base plugin.
850
+ 2. `createDataCatalogPreset()` only if option boilerplate remains noisy after
851
+ plugin composition.
852
+ 3. `createRestExplorerAdapter()` for `/catalog` + `/facets` endpoints.
853
+ 4. `createOpenSurfaceRowHandler()` for catalog row activation.
854
+ 5. Future `explorerPlugin` owns explorer primitives and adapter helpers;
855
+ `dataCatalogPlugin` composes them.
856
+
857
+ Do not make macro depend directly on a future `explorerPlugin` if
858
+ `dataCatalogPlugin` can provide the right data-catalog specialization. Macro
859
+ should depend on the highest-level appropriate abstraction:
860
+
861
+ ```txt
862
+ macro series catalog -> dataCatalogPlugin plugin/factory
863
+ macro custom project/tree explorer -> explorerPlugin plugin/factory
864
+ macro chart/deck panels -> macro-owned panels
865
+ ```
866
+
867
+ ## Proposed Extraction Order
868
+
869
+ ### Step 1 — `composePlugins()`
870
+
871
+ Why first: it answers the general customization question without a complex
872
+ mixin/enhancer framework. Plugins can build on other plugins through the same
873
+ interface they already expose.
874
+
875
+ Add `composePlugins()` in the shared plugin model and migrate macro composition
876
+ only if it makes the file simpler. Keep `appendDataCatalogOutputs()` as a
877
+ compatibility helper.
878
+
879
+ Tests:
880
+
881
+ - composition flattens child outputs in order
882
+ - duplicate ids are caught or warned consistently
883
+ - adopted ownership defaults to parent plugin id
884
+ - `adoptOutputs: false` preserves child plugin ids
885
+ - macro plugin output order remains stable
886
+
887
+ ### Step 2 — Surface resolver helpers
888
+
889
+ Why first: high signal, low risk, clearly reduces macro/Feret boilerplate.
890
+
891
+ Add helpers and migrate macro `surfaceResolver.ts` to declare only rules.
892
+
893
+ Tests:
894
+
895
+ - macro resolver tests unchanged
896
+ - helper tests for kind mismatch, blank target, title fallback, path matching
897
+
898
+ ### Step 3 — Open surface helpers
899
+
900
+ Add `openSurface()`, `createOpenSurfaceHandler()`, and
901
+ `createOpenSurfaceRowHandler()`.
902
+
903
+ Migrate `openSeriesPane()` to use helper internally, keeping public macro API
904
+ unchanged.
905
+
906
+ Tests:
907
+
908
+ - macro `openSeriesPane` test unchanged
909
+ - helper tests for trimming/empty target/meta
910
+
911
+ ### Step 4 — REST explorer adapter helper
912
+
913
+ Add generic `createRestExplorerAdapter()` and `toQueryString()`.
914
+
915
+ Migrate `macroSeriesAdapter.ts` to keep only macro `toRow()` and arg mapping.
916
+
917
+ Tests:
918
+
919
+ - macro catalog adapter tests if present
920
+ - new helper tests for array query params, abort signal forwarding, non-OK
921
+ responses, response mapping
922
+
923
+ ### Step 5 — Data catalog preset helper
924
+
925
+ Add `createDataCatalogPreset()` only if macro and playground both simplify after
926
+ `composePlugins()` exists. Plugin composition may make this unnecessary.
927
+
928
+ Migrate macro and playground catalog options only if the helper removes real
929
+ boilerplate.
930
+
931
+ Tests:
932
+
933
+ - existing data catalog plugin tests
934
+ - public API test for helper export if exported
935
+
936
+ ### Step 6 — Event namespace helper
937
+
938
+ Add only when macro or Feret introduces first real domain event. Do not create
939
+ unused event abstraction.
940
+
941
+ ## Do Not Extract Yet
942
+
943
+ - Chart/deck panels
944
+ - FRED frequency labels
945
+ - series colors
946
+ - ClickHouse services
947
+ - SQL tool hinting
948
+ - macro transform SDK/provisioning contents
949
+ - local shell option values
950
+ - deck path predicate (`deck/*.md` at root) beyond generic path resolver helper
951
+
952
+ ## Acceptance Criteria
953
+
954
+ - Macro plugin file count does not grow just to satisfy abstraction.
955
+ - Macro `surfaceResolver.ts` becomes declarative: constants + predicates + helper
956
+ calls, no repeated resolver boilerplate.
957
+ - Macro catalog adapter keeps macro row mapping but delegates REST/query plumbing.
958
+ - Macro tests remain mostly unchanged, proving helpers preserve behavior.
959
+ - Feret can reuse the same helpers for ingredient/formulation/source-file
960
+ surfaces without depending on macro.
961
+ - Workspace does not learn FRED, ClickHouse, chart, deck, or macro series
962
+ semantics.