plotleaf 0.4.1 → 0.5.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.
- package/README.md +3 -1
- package/dist/assets/base.js +1 -1
- package/dist/assets/charts.js +1 -1
- package/dist/assets/styles.css +14 -0
- package/dist/cli.js +475 -109
- package/examples/m6/prepared-subtotals.json +24 -0
- package/examples/m6/report.md +17 -0
- package/examples/m6/summary.json +16 -0
- package/examples/report.md +14 -0
- package/package.json +3 -1
- package/schema/components.json +260 -1
- package/skills/plotleaf/SKILL.md +33 -2
package/dist/cli.js
CHANGED
|
@@ -65,9 +65,138 @@ function displayPath(filename) {
|
|
|
65
65
|
return path.relative(process.cwd(), filename) || path.basename(filename);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
// src/compiler/
|
|
68
|
+
// src/compiler/subtotal-schema.ts
|
|
69
69
|
import * as z from "zod";
|
|
70
70
|
|
|
71
|
+
// src/shared/limits.ts
|
|
72
|
+
var limits = {
|
|
73
|
+
markdownBytes: 1024 ** 2,
|
|
74
|
+
jsonBytes: 10 * 1024 ** 2,
|
|
75
|
+
totalBytes: 25 * 1024 ** 2,
|
|
76
|
+
datasets: 64,
|
|
77
|
+
rows: 1e5,
|
|
78
|
+
chartRows: 1e4,
|
|
79
|
+
donutSlices: 8,
|
|
80
|
+
funnelStages: 20,
|
|
81
|
+
tableRows: 1000,
|
|
82
|
+
tableColumns: 40,
|
|
83
|
+
sparklinePoints: 64,
|
|
84
|
+
imageFiles: 64,
|
|
85
|
+
imageBytes: 1024 ** 2,
|
|
86
|
+
imageDimension: 4096,
|
|
87
|
+
imagePixels: 4000000
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// src/compiler/subtotal-schema.ts
|
|
91
|
+
var row = z.record(z.string(), z.unknown());
|
|
92
|
+
var subtotalSchema = z.strictObject({
|
|
93
|
+
version: z.literal(1),
|
|
94
|
+
scope: z.string().trim().min(1).max(1000),
|
|
95
|
+
groups: z.array(z.strictObject({
|
|
96
|
+
id: z.string().trim().min(1).max(1000),
|
|
97
|
+
label: z.string().trim().min(1).max(1000),
|
|
98
|
+
rows: z.array(row).min(1).max(limits.tableRows),
|
|
99
|
+
subtotal: row
|
|
100
|
+
})).max(limits.tableRows)
|
|
101
|
+
}).superRefine((bundle, context) => {
|
|
102
|
+
const ids = new Set;
|
|
103
|
+
let count = 0;
|
|
104
|
+
for (const [index, group] of bundle.groups.entries()) {
|
|
105
|
+
if (ids.has(group.id))
|
|
106
|
+
context.addIssue({
|
|
107
|
+
code: "custom",
|
|
108
|
+
path: ["groups", index, "id"],
|
|
109
|
+
message: "Group IDs must be unique."
|
|
110
|
+
});
|
|
111
|
+
ids.add(group.id);
|
|
112
|
+
count += group.rows.length + 1;
|
|
113
|
+
}
|
|
114
|
+
if (count > limits.tableRows)
|
|
115
|
+
context.addIssue({
|
|
116
|
+
code: "custom",
|
|
117
|
+
path: ["groups"],
|
|
118
|
+
message: `At most ${limits.tableRows} detail and subtotal rows are supported.`
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// src/compiler/composition-schema.ts
|
|
123
|
+
import * as z2 from "zod";
|
|
124
|
+
var compositionLimits = {
|
|
125
|
+
depth: 16,
|
|
126
|
+
definitions: 64,
|
|
127
|
+
nodes: 512,
|
|
128
|
+
rows: 20000,
|
|
129
|
+
dataBytes: 8 * 1024 * 1024,
|
|
130
|
+
parameters: 32,
|
|
131
|
+
jsonDepth: 32,
|
|
132
|
+
jsonContainers: 1e4
|
|
133
|
+
};
|
|
134
|
+
var identifier = z2.string().regex(/^[A-Za-z][A-Za-z0-9_-]{0,63}$/);
|
|
135
|
+
var literal3 = z2.union([
|
|
136
|
+
z2.string().max(1000),
|
|
137
|
+
z2.number().finite(),
|
|
138
|
+
z2.boolean()
|
|
139
|
+
]);
|
|
140
|
+
var parameter = z2.discriminatedUnion("type", [
|
|
141
|
+
z2.strictObject({
|
|
142
|
+
type: z2.literal("string"),
|
|
143
|
+
default: z2.string().max(1000).optional()
|
|
144
|
+
}),
|
|
145
|
+
z2.strictObject({
|
|
146
|
+
type: z2.literal("number"),
|
|
147
|
+
default: z2.number().finite().optional()
|
|
148
|
+
}),
|
|
149
|
+
z2.strictObject({
|
|
150
|
+
type: z2.literal("boolean"),
|
|
151
|
+
default: z2.boolean().optional()
|
|
152
|
+
})
|
|
153
|
+
]);
|
|
154
|
+
var value = z2.union([literal3, z2.strictObject({ prop: identifier })]);
|
|
155
|
+
var node = z2.lazy(() => z2.strictObject({
|
|
156
|
+
name: identifier,
|
|
157
|
+
props: z2.record(identifier, value),
|
|
158
|
+
children: z2.array(node).max(compositionLimits.nodes).optional()
|
|
159
|
+
}));
|
|
160
|
+
var compositionSchema = z2.strictObject({
|
|
161
|
+
version: z2.literal(1),
|
|
162
|
+
props: z2.record(identifier, parameter),
|
|
163
|
+
components: z2.array(node).min(1).max(compositionLimits.nodes)
|
|
164
|
+
}).superRefine((definition, context) => {
|
|
165
|
+
if (Object.hasOwn(definition.props, "src"))
|
|
166
|
+
context.addIssue({
|
|
167
|
+
code: "custom",
|
|
168
|
+
path: ["props", "src"],
|
|
169
|
+
message: "src is reserved for the composition definition path."
|
|
170
|
+
});
|
|
171
|
+
if (Object.keys(definition.props).length > compositionLimits.parameters)
|
|
172
|
+
context.addIssue({
|
|
173
|
+
code: "custom",
|
|
174
|
+
path: ["props"],
|
|
175
|
+
message: `At most ${compositionLimits.parameters} parameters are supported.`
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
function checkCompositionStructure(value) {
|
|
179
|
+
const pending = [{ value, depth: 0 }];
|
|
180
|
+
let containers = 0;
|
|
181
|
+
while (pending.length) {
|
|
182
|
+
const current = pending.pop();
|
|
183
|
+
if (current.value === null || typeof current.value !== "object")
|
|
184
|
+
continue;
|
|
185
|
+
if (current.depth > compositionLimits.jsonDepth || ++containers > compositionLimits.jsonContainers)
|
|
186
|
+
return "Composition JSON exceeds the nesting or container limit.";
|
|
187
|
+
for (const [key, child] of Object.entries(current.value)) {
|
|
188
|
+
if (["__proto__", "constructor", "prototype"].includes(key))
|
|
189
|
+
return `Reserved object key ${JSON.stringify(key)}.`;
|
|
190
|
+
if (child !== null && typeof child === "object")
|
|
191
|
+
pending.push({ value: child, depth: current.depth + 1 });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/compiler/registry.ts
|
|
198
|
+
import * as z4 from "zod";
|
|
199
|
+
|
|
71
200
|
// src/shared/exploration.ts
|
|
72
201
|
var valueKey = (value) => typeof value === "object" ? `${value.start}/${value.end}` : `${typeof value}:${String(value)}`;
|
|
73
202
|
var explorationLimits = {
|
|
@@ -84,85 +213,56 @@ var explorationLimits = {
|
|
|
84
213
|
};
|
|
85
214
|
|
|
86
215
|
// src/compiler/exploration-schema.ts
|
|
87
|
-
|
|
88
|
-
var
|
|
89
|
-
var
|
|
216
|
+
import * as z3 from "zod";
|
|
217
|
+
var id = z3.string().regex(/^[a-z][a-z0-9_-]{0,47}$/).refine((value) => !["constructor", "prototype"].includes(value), "Reserved identifier.");
|
|
218
|
+
var label = z3.string().min(1).max(160);
|
|
219
|
+
var date = z3.string().regex(/^\d{4}-\d{2}-\d{2}$/).refine((value) => {
|
|
90
220
|
const timestamp = Date.parse(`${value}T00:00:00Z`);
|
|
91
221
|
return Number.isFinite(timestamp) && new Date(timestamp).toISOString().slice(0, 10) === value;
|
|
92
222
|
}, "Use a real ISO calendar date.");
|
|
93
|
-
var range =
|
|
223
|
+
var range = z3.strictObject({ start: date, end: date }).refine((value) => value.start <= value.end, "Range start must not follow its end.");
|
|
94
224
|
var common = { id, label };
|
|
95
|
-
var explorationInputSchema =
|
|
96
|
-
|
|
225
|
+
var explorationInputSchema = z3.discriminatedUnion("type", [
|
|
226
|
+
z3.strictObject({
|
|
97
227
|
...common,
|
|
98
|
-
type:
|
|
99
|
-
default:
|
|
100
|
-
options:
|
|
228
|
+
type: z3.enum(["select", "segmented", "text"]),
|
|
229
|
+
default: z3.string().max(120),
|
|
230
|
+
options: z3.array(z3.strictObject({ value: z3.string().max(120), label })).min(1).max(explorationLimits.options)
|
|
101
231
|
}),
|
|
102
|
-
|
|
232
|
+
z3.strictObject({
|
|
103
233
|
...common,
|
|
104
|
-
type:
|
|
105
|
-
default:
|
|
106
|
-
options:
|
|
234
|
+
type: z3.literal("number"),
|
|
235
|
+
default: z3.number().finite(),
|
|
236
|
+
options: z3.array(z3.strictObject({ value: z3.number().finite(), label })).min(1).max(explorationLimits.options)
|
|
107
237
|
}),
|
|
108
|
-
|
|
238
|
+
z3.strictObject({
|
|
109
239
|
...common,
|
|
110
|
-
type:
|
|
111
|
-
default:
|
|
240
|
+
type: z3.literal("toggle"),
|
|
241
|
+
default: z3.boolean()
|
|
112
242
|
}),
|
|
113
|
-
|
|
243
|
+
z3.strictObject({
|
|
114
244
|
...common,
|
|
115
|
-
type:
|
|
245
|
+
type: z3.literal("date-range"),
|
|
116
246
|
default: range,
|
|
117
|
-
options:
|
|
247
|
+
options: z3.array(z3.strictObject({ value: range, label })).min(1).max(explorationLimits.options)
|
|
118
248
|
})
|
|
119
249
|
]);
|
|
120
|
-
var explorationSchema =
|
|
121
|
-
inputs:
|
|
122
|
-
views:
|
|
250
|
+
var explorationSchema = z3.strictObject({
|
|
251
|
+
inputs: z3.array(explorationInputSchema).min(1).max(explorationLimits.inputs),
|
|
252
|
+
views: z3.array(z3.strictObject({
|
|
123
253
|
id,
|
|
124
254
|
label,
|
|
125
|
-
when:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
255
|
+
when: z3.record(id, z3.union([
|
|
256
|
+
z3.string().max(120),
|
|
257
|
+
z3.number().finite(),
|
|
258
|
+
z3.boolean(),
|
|
129
259
|
range
|
|
130
260
|
])),
|
|
131
|
-
markdown:
|
|
132
|
-
datasets:
|
|
261
|
+
markdown: z3.string().min(1).max(1024 ** 2),
|
|
262
|
+
datasets: z3.record(z3.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.-]*\.json$/), z3.array(z3.record(z3.string(), z3.unknown())).max(1e4)).default({})
|
|
133
263
|
})).min(1).max(explorationLimits.views)
|
|
134
264
|
});
|
|
135
265
|
|
|
136
|
-
// src/compiler/parse.ts
|
|
137
|
-
import { unified } from "unified";
|
|
138
|
-
import remarkParse from "remark-parse";
|
|
139
|
-
import remarkDirective from "remark-directive";
|
|
140
|
-
import remarkFrontmatter from "remark-frontmatter";
|
|
141
|
-
import { parseDocument, visit } from "yaml";
|
|
142
|
-
import * as z3 from "zod";
|
|
143
|
-
|
|
144
|
-
// src/compiler/registry.ts
|
|
145
|
-
import * as z2 from "zod";
|
|
146
|
-
|
|
147
|
-
// src/shared/limits.ts
|
|
148
|
-
var limits = {
|
|
149
|
-
markdownBytes: 1024 ** 2,
|
|
150
|
-
jsonBytes: 10 * 1024 ** 2,
|
|
151
|
-
totalBytes: 25 * 1024 ** 2,
|
|
152
|
-
datasets: 64,
|
|
153
|
-
rows: 1e5,
|
|
154
|
-
chartRows: 1e4,
|
|
155
|
-
donutSlices: 8,
|
|
156
|
-
funnelStages: 20,
|
|
157
|
-
tableRows: 1000,
|
|
158
|
-
tableColumns: 40,
|
|
159
|
-
sparklinePoints: 64,
|
|
160
|
-
imageFiles: 64,
|
|
161
|
-
imageBytes: 1024 ** 2,
|
|
162
|
-
imageDimension: 4096,
|
|
163
|
-
imagePixels: 4000000
|
|
164
|
-
};
|
|
165
|
-
|
|
166
266
|
// src/shared/payload.ts
|
|
167
267
|
var funnelStatuses = {
|
|
168
268
|
"on-target": "On target",
|
|
@@ -172,8 +272,8 @@ var funnelStatuses = {
|
|
|
172
272
|
};
|
|
173
273
|
|
|
174
274
|
// src/compiler/registry.ts
|
|
175
|
-
var text =
|
|
176
|
-
var format =
|
|
275
|
+
var text = z4.string().min(1).max(1000);
|
|
276
|
+
var format = z4.string().regex(/^(number|percent|percentage-points|currency:[A-Z]{3})$/, "Use number, percent, percentage-points or currency:GBP (an ISO currency code).");
|
|
177
277
|
var chart = {
|
|
178
278
|
data: text,
|
|
179
279
|
x: text,
|
|
@@ -181,20 +281,31 @@ var chart = {
|
|
|
181
281
|
title: text,
|
|
182
282
|
format: format.default("number")
|
|
183
283
|
};
|
|
184
|
-
var line =
|
|
284
|
+
var line = z4.strictObject({
|
|
185
285
|
...chart,
|
|
186
|
-
xType:
|
|
187
|
-
target:
|
|
286
|
+
xType: z4.enum(["category", "date", "number"]).default("category"),
|
|
287
|
+
target: z4.number().finite().optional(),
|
|
188
288
|
targetLabel: text.optional()
|
|
189
289
|
});
|
|
190
290
|
var registry = {
|
|
291
|
+
component: {
|
|
292
|
+
forms: ["leaf"],
|
|
293
|
+
children: [],
|
|
294
|
+
description: "A declarative composition from a local JSON definition.",
|
|
295
|
+
dataRules: {
|
|
296
|
+
schema: z4.toJSONSchema(compositionSchema),
|
|
297
|
+
limits: compositionLimits
|
|
298
|
+
},
|
|
299
|
+
schema: z4.object({ src: text }).catchall(z4.string().max(1000)),
|
|
300
|
+
example: '::component{src="./components/summary.json" label="Orders" value="1284"}'
|
|
301
|
+
},
|
|
191
302
|
explore: {
|
|
192
303
|
forms: ["leaf"],
|
|
193
304
|
children: [],
|
|
194
305
|
description: "Scoped native controls select complete agent-prepared views. No analytics or report JavaScript.",
|
|
195
|
-
schema:
|
|
306
|
+
schema: z4.strictObject({ data: text, title: text }),
|
|
196
307
|
dataRules: {
|
|
197
|
-
schema:
|
|
308
|
+
schema: z4.toJSONSchema(explorationSchema),
|
|
198
309
|
resolution: "Relative JSON bundle; inline view datasets override the same plain filename only within that view.",
|
|
199
310
|
limits: explorationLimits,
|
|
200
311
|
binding: "Every input affects exactly this exploration. Every view declares all input values. No matching combination hides the previous view."
|
|
@@ -205,6 +316,7 @@ var registry = {
|
|
|
205
316
|
forms: ["container"],
|
|
206
317
|
children: [
|
|
207
318
|
"metric",
|
|
319
|
+
"component",
|
|
208
320
|
"line",
|
|
209
321
|
"area",
|
|
210
322
|
"bar",
|
|
@@ -214,8 +326,8 @@ var registry = {
|
|
|
214
326
|
"callout"
|
|
215
327
|
],
|
|
216
328
|
description: "A responsive row of one to four components.",
|
|
217
|
-
schema:
|
|
218
|
-
columns:
|
|
329
|
+
schema: z4.strictObject({
|
|
330
|
+
columns: z4.number().int().min(1).max(4).default(2)
|
|
219
331
|
}),
|
|
220
332
|
example: `:::grid{columns="4"}
|
|
221
333
|
::metric{label="Teams" value="1492"}
|
|
@@ -225,13 +337,13 @@ var registry = {
|
|
|
225
337
|
forms: ["leaf"],
|
|
226
338
|
children: [],
|
|
227
339
|
description: "A prepared numeric value with optional comparison, direction and sentiment.",
|
|
228
|
-
schema:
|
|
340
|
+
schema: z4.strictObject({
|
|
229
341
|
label: text,
|
|
230
|
-
value:
|
|
342
|
+
value: z4.number().finite(),
|
|
231
343
|
format: format.default("number"),
|
|
232
344
|
comparison: text.optional(),
|
|
233
|
-
direction:
|
|
234
|
-
sentiment:
|
|
345
|
+
direction: z4.enum(["up", "down", "flat"]).optional(),
|
|
346
|
+
sentiment: z4.enum(["positive", "negative", "neutral"]).default("neutral")
|
|
235
347
|
}),
|
|
236
348
|
example: '::metric{label="Conversion" value="0.103" format="percent"}'
|
|
237
349
|
},
|
|
@@ -253,14 +365,14 @@ var registry = {
|
|
|
253
365
|
forms: ["leaf"],
|
|
254
366
|
children: [],
|
|
255
367
|
description: "An interactive category bar chart with a zero baseline.",
|
|
256
|
-
schema:
|
|
368
|
+
schema: z4.strictObject(chart),
|
|
257
369
|
example: '::bar{data="./errors.json" x="category" y="count" title="Errors by category"}'
|
|
258
370
|
},
|
|
259
371
|
donut: {
|
|
260
372
|
forms: ["leaf"],
|
|
261
373
|
children: [],
|
|
262
374
|
description: "A part-to-whole donut with a readable legend and native data table.",
|
|
263
|
-
schema:
|
|
375
|
+
schema: z4.strictObject({
|
|
264
376
|
data: text,
|
|
265
377
|
category: text,
|
|
266
378
|
value: text,
|
|
@@ -278,7 +390,7 @@ var registry = {
|
|
|
278
390
|
forms: ["leaf"],
|
|
279
391
|
children: [],
|
|
280
392
|
description: "Ordered stage/progress rows with prepared values, denominators, shares and optional explicit status. Works without JavaScript.",
|
|
281
|
-
schema:
|
|
393
|
+
schema: z4.strictObject({
|
|
282
394
|
data: text,
|
|
283
395
|
stage: text,
|
|
284
396
|
value: text,
|
|
@@ -302,7 +414,15 @@ var registry = {
|
|
|
302
414
|
forms: ["leaf", "container"],
|
|
303
415
|
children: ["column"],
|
|
304
416
|
description: "A semantic sortable table, with inferred or explicitly formatted columns.",
|
|
305
|
-
|
|
417
|
+
dataRules: {
|
|
418
|
+
grouped: z4.toJSONSchema(subtotalSchema),
|
|
419
|
+
sorting: "Details sort within groups; prepared subtotals stay at the end of their group. Scope is supplied explicitly."
|
|
420
|
+
},
|
|
421
|
+
schema: z4.strictObject({
|
|
422
|
+
data: text,
|
|
423
|
+
title: text.optional(),
|
|
424
|
+
prepared: z4.enum(["grouped"]).optional()
|
|
425
|
+
}),
|
|
306
426
|
example: `:::table{data="./plans.json" title="By plan"}
|
|
307
427
|
::column{field="plan" label="Plan"}
|
|
308
428
|
::column{field="change" label="Δ QoQ" format="percent" sign="always"}
|
|
@@ -312,15 +432,15 @@ var registry = {
|
|
|
312
432
|
forms: ["leaf"],
|
|
313
433
|
children: [],
|
|
314
434
|
description: "A scalar, local image or bounded sparkline column. Omitted kind means scalar.",
|
|
315
|
-
schema:
|
|
435
|
+
schema: z4.strictObject({
|
|
316
436
|
field: text,
|
|
317
437
|
label: text.optional(),
|
|
318
|
-
kind:
|
|
438
|
+
kind: z4.enum(["scalar", "image", "sparkline"]).optional(),
|
|
319
439
|
alt: text.optional(),
|
|
320
440
|
sortBy: text.optional(),
|
|
321
441
|
format: format.optional(),
|
|
322
|
-
sign:
|
|
323
|
-
semantics:
|
|
442
|
+
sign: z4.enum(["auto", "always"]).default("auto"),
|
|
443
|
+
semantics: z4.enum(["neutral", "higher-is-better", "lower-is-better"]).default("neutral")
|
|
324
444
|
}),
|
|
325
445
|
example: '::column{field="change" label="Δ QoQ" format="percent" sign="always" semantics="higher-is-better"}',
|
|
326
446
|
dataRules: {
|
|
@@ -333,8 +453,8 @@ var registry = {
|
|
|
333
453
|
forms: ["container"],
|
|
334
454
|
children: ["markdown"],
|
|
335
455
|
description: "A contextual note or warning containing ordinary Markdown.",
|
|
336
|
-
schema:
|
|
337
|
-
tone:
|
|
456
|
+
schema: z4.strictObject({
|
|
457
|
+
tone: z4.enum(["note", "warning"]).default("note"),
|
|
338
458
|
title: text.optional()
|
|
339
459
|
}),
|
|
340
460
|
example: `:::callout{title="Coverage"}
|
|
@@ -363,7 +483,7 @@ function componentSpec() {
|
|
|
363
483
|
description: entry.description,
|
|
364
484
|
forms: entry.forms,
|
|
365
485
|
children: entry.children,
|
|
366
|
-
props:
|
|
486
|
+
props: z4.toJSONSchema(entry.schema),
|
|
367
487
|
example: entry.example,
|
|
368
488
|
..."dataRules" in entry ? { dataRules: entry.dataRules } : {}
|
|
369
489
|
}
|
|
@@ -433,11 +553,187 @@ function decodeProps(name, attributes) {
|
|
|
433
553
|
return { props, errors };
|
|
434
554
|
}
|
|
435
555
|
|
|
556
|
+
// src/compiler/sources.ts
|
|
557
|
+
class LocalFileError extends Error {
|
|
558
|
+
kind;
|
|
559
|
+
constructor(kind, message) {
|
|
560
|
+
super(message);
|
|
561
|
+
this.kind = kind;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/compiler/composition.ts
|
|
566
|
+
var componentProperties = componentSpec().components;
|
|
567
|
+
var compositionState = () => ({
|
|
568
|
+
definitions: new Map,
|
|
569
|
+
nodes: 0,
|
|
570
|
+
rows: 0,
|
|
571
|
+
dataBytes: 0
|
|
572
|
+
});
|
|
573
|
+
function relativeTo(reference, base) {
|
|
574
|
+
if (!base)
|
|
575
|
+
return reference;
|
|
576
|
+
if (/^(?:\/|[A-Za-z][A-Za-z\d+.-]*:)/.test(reference) || reference.includes("\\"))
|
|
577
|
+
return reference;
|
|
578
|
+
return `${base}/${reference}`;
|
|
579
|
+
}
|
|
580
|
+
function parentOf(reference) {
|
|
581
|
+
const index = reference.lastIndexOf("/");
|
|
582
|
+
return index < 0 ? "" : reference.slice(0, index);
|
|
583
|
+
}
|
|
584
|
+
async function expandComposition(block, files, diagnostics, state, parent, stack = [], origins = {}) {
|
|
585
|
+
const fail = (code, message) => {
|
|
586
|
+
diagnostics.add(code, `${block.props.src}: ${message}`, block.position, "Correct the composition definition and its declared literal parameters.");
|
|
587
|
+
return [];
|
|
588
|
+
};
|
|
589
|
+
let filename;
|
|
590
|
+
try {
|
|
591
|
+
filename = await files.resolve(block.props.src, [".json"]);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
return fail(error instanceof LocalFileError && error.kind === "path" ? "E_COMPONENT_PATH" : "E_COMPONENT_READ", error instanceof Error ? error.message : "Cannot resolve definition.");
|
|
594
|
+
}
|
|
595
|
+
if (stack.includes(filename))
|
|
596
|
+
return fail("E_COMPONENT_CYCLE", "Composition definitions cannot contain cycles.");
|
|
597
|
+
if (stack.length >= compositionLimits.depth)
|
|
598
|
+
return fail("E_COMPONENT_LIMIT", `Composition nesting exceeds ${compositionLimits.depth}.`);
|
|
599
|
+
let definition = state.definitions.get(filename);
|
|
600
|
+
if (!definition) {
|
|
601
|
+
if (state.definitions.size >= compositionLimits.definitions)
|
|
602
|
+
return fail("E_COMPONENT_LIMIT", `At most ${compositionLimits.definitions} definitions are supported.`);
|
|
603
|
+
let raw;
|
|
604
|
+
try {
|
|
605
|
+
raw = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(await files.read(filename, limits.jsonBytes)));
|
|
606
|
+
} catch (error) {
|
|
607
|
+
return fail("E_COMPONENT_READ", error instanceof Error ? error.message : "Cannot read definition JSON.");
|
|
608
|
+
}
|
|
609
|
+
const structural = checkCompositionStructure(raw);
|
|
610
|
+
if (structural)
|
|
611
|
+
return fail("E_COMPONENT_SCHEMA", structural);
|
|
612
|
+
const parsed = compositionSchema.safeParse(raw);
|
|
613
|
+
if (!parsed.success)
|
|
614
|
+
return fail("E_COMPONENT_SCHEMA", parsed.error.issues.slice(0, 8).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; "));
|
|
615
|
+
definition = parsed.data;
|
|
616
|
+
state.definitions.set(filename, definition);
|
|
617
|
+
}
|
|
618
|
+
const supplied = Object.entries(block.props).filter(([name]) => name !== "src");
|
|
619
|
+
for (const [name] of supplied)
|
|
620
|
+
if (!Object.hasOwn(definition.props, name))
|
|
621
|
+
return fail("E_COMPONENT_PROP", `Unknown parameter ${JSON.stringify(name)}.`);
|
|
622
|
+
const base = parentOf(block.props.src);
|
|
623
|
+
const values = Object.create(null);
|
|
624
|
+
const valueOrigins = Object.create(null);
|
|
625
|
+
for (const [name, spec] of Object.entries(definition.props)) {
|
|
626
|
+
const input = block.props[name];
|
|
627
|
+
valueOrigins[name] = input === undefined ? base : origins[name] ?? "";
|
|
628
|
+
if (input === undefined) {
|
|
629
|
+
if (spec.default === undefined)
|
|
630
|
+
return fail("E_COMPONENT_PROP", `Missing required parameter ${JSON.stringify(name)}.`);
|
|
631
|
+
values[name] = spec.default;
|
|
632
|
+
} else if (spec.type === "number") {
|
|
633
|
+
if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(input) || !Number.isFinite(Number(input)))
|
|
634
|
+
return fail("E_COMPONENT_PROP", `${name} must be a finite number.`);
|
|
635
|
+
values[name] = Number(input);
|
|
636
|
+
} else if (spec.type === "boolean") {
|
|
637
|
+
if (input !== "true" && input !== "false")
|
|
638
|
+
return fail("E_COMPONENT_PROP", `${name} must be true or false.`);
|
|
639
|
+
values[name] = input === "true";
|
|
640
|
+
} else
|
|
641
|
+
values[name] = input;
|
|
642
|
+
}
|
|
643
|
+
const build = async (nodes, context, depth) => {
|
|
644
|
+
const out = [];
|
|
645
|
+
if (depth > compositionLimits.depth)
|
|
646
|
+
return fail("E_COMPONENT_LIMIT", "Component nesting exceeds the composition limit.");
|
|
647
|
+
for (const node of nodes) {
|
|
648
|
+
if (++state.nodes > compositionLimits.nodes)
|
|
649
|
+
return fail("E_COMPONENT_LIMIT", `Expanded components exceed ${compositionLimits.nodes}.`);
|
|
650
|
+
if (!Object.hasOwn(registry, node.name)) {
|
|
651
|
+
fail("E_COMPONENT_UNKNOWN", `Unknown built-in ${JSON.stringify(node.name)}.`);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const name = node.name;
|
|
655
|
+
const entry = registry[name];
|
|
656
|
+
const allowed = context === "root" ? name !== "column" : registry[context]?.children?.includes(name);
|
|
657
|
+
if (!allowed) {
|
|
658
|
+
fail("E_COMPONENT_CONTEXT", `${name} is not allowed inside ${context}.`);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
const attrs = Object.create(null);
|
|
662
|
+
const childOrigins = Object.create(null);
|
|
663
|
+
const properties = componentProperties[name].props.properties ?? {};
|
|
664
|
+
let bad = false;
|
|
665
|
+
for (const [key, value] of Object.entries(node.props)) {
|
|
666
|
+
const dynamic = typeof value === "object";
|
|
667
|
+
if (dynamic && !Object.hasOwn(values, value.prop)) {
|
|
668
|
+
fail("E_COMPONENT_PROP", `Unknown parameter reference ${JSON.stringify(value.prop)}.`);
|
|
669
|
+
bad = true;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
let resolved = dynamic ? values[value.prop] : value;
|
|
673
|
+
const property = properties[key];
|
|
674
|
+
const expected = property?.type === "integer" ? "number" : property?.type;
|
|
675
|
+
if (name !== "component" && expected && expected !== typeof resolved) {
|
|
676
|
+
fail("E_COMPONENT_PROP", `${name}.${key} requires ${expected}; received ${typeof resolved}.`);
|
|
677
|
+
bad = true;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
if (typeof resolved === "string" && (key === "data" && [
|
|
681
|
+
"line",
|
|
682
|
+
"area",
|
|
683
|
+
"bar",
|
|
684
|
+
"donut",
|
|
685
|
+
"funnel",
|
|
686
|
+
"table",
|
|
687
|
+
"explore"
|
|
688
|
+
].includes(name) || key === "src" && name === "component"))
|
|
689
|
+
resolved = relativeTo(resolved, dynamic ? valueOrigins[value.prop] : base);
|
|
690
|
+
attrs[key] = String(resolved);
|
|
691
|
+
if (name === "component" && key !== "src")
|
|
692
|
+
childOrigins[key] = dynamic ? valueOrigins[value.prop] : base;
|
|
693
|
+
}
|
|
694
|
+
if (bad)
|
|
695
|
+
continue;
|
|
696
|
+
const decoded = decodeProps(name, attrs);
|
|
697
|
+
if (decoded.errors.length) {
|
|
698
|
+
fail("E_COMPONENT_PROP", `${name}: ${decoded.errors.join("; ")}`);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
const children = node.children ?? [];
|
|
702
|
+
if (children.length && !entry.forms.includes("container")) {
|
|
703
|
+
fail("E_COMPONENT_CONTEXT", `${name} cannot contain children.`);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
const built = {
|
|
707
|
+
type: "component",
|
|
708
|
+
name,
|
|
709
|
+
props: decoded.props,
|
|
710
|
+
children: [],
|
|
711
|
+
position: block.position,
|
|
712
|
+
compositionOrigin: true
|
|
713
|
+
};
|
|
714
|
+
if (name === "component") {
|
|
715
|
+
out.push(...await expandComposition(built, files, diagnostics, state, context, [...stack, filename], childOrigins));
|
|
716
|
+
} else {
|
|
717
|
+
built.children = await build(children, name, depth + 1);
|
|
718
|
+
out.push(built);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return out;
|
|
722
|
+
};
|
|
723
|
+
return build(definition.components, parent, 0);
|
|
724
|
+
}
|
|
725
|
+
|
|
436
726
|
// src/compiler/parse.ts
|
|
727
|
+
import { unified } from "unified";
|
|
728
|
+
import remarkParse from "remark-parse";
|
|
729
|
+
import remarkDirective from "remark-directive";
|
|
730
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
731
|
+
import { parseDocument, visit } from "yaml";
|
|
732
|
+
import * as z5 from "zod";
|
|
437
733
|
var parser = unified().use(remarkParse).use(remarkFrontmatter).use(remarkDirective);
|
|
438
|
-
var metadataSchema =
|
|
439
|
-
title:
|
|
440
|
-
description:
|
|
734
|
+
var metadataSchema = z5.strictObject({
|
|
735
|
+
title: z5.string().min(1).max(500).optional(),
|
|
736
|
+
description: z5.string().max(2000).optional()
|
|
441
737
|
});
|
|
442
738
|
function textOf(node) {
|
|
443
739
|
return node.value ?? node.children?.map(textOf).join("") ?? "";
|
|
@@ -602,15 +898,6 @@ function parseReport(source, fallbackTitle, diagnostics) {
|
|
|
602
898
|
return { metadata, blocks: reportBlocks };
|
|
603
899
|
}
|
|
604
900
|
|
|
605
|
-
// src/compiler/sources.ts
|
|
606
|
-
class LocalFileError extends Error {
|
|
607
|
-
kind;
|
|
608
|
-
constructor(kind, message) {
|
|
609
|
-
super(message);
|
|
610
|
-
this.kind = kind;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
901
|
// src/compiler/exploration.ts
|
|
615
902
|
var expansionBudget = () => ({
|
|
616
903
|
explorations: 0,
|
|
@@ -814,12 +1101,16 @@ class DataResolver {
|
|
|
814
1101
|
files;
|
|
815
1102
|
images;
|
|
816
1103
|
expansion;
|
|
1104
|
+
compositions;
|
|
1105
|
+
inExploration;
|
|
817
1106
|
cache;
|
|
818
|
-
constructor(diagnostics, files, images, expansion = expansionBudget(), cache = new Map) {
|
|
1107
|
+
constructor(diagnostics, files, images, expansion = expansionBudget(), compositions = compositionState(), inExploration = false, cache = new Map) {
|
|
819
1108
|
this.diagnostics = diagnostics;
|
|
820
1109
|
this.files = files;
|
|
821
1110
|
this.images = images;
|
|
822
1111
|
this.expansion = expansion;
|
|
1112
|
+
this.compositions = compositions;
|
|
1113
|
+
this.inExploration = inExploration;
|
|
823
1114
|
this.cache = cache;
|
|
824
1115
|
}
|
|
825
1116
|
async load(reference, block) {
|
|
@@ -849,8 +1140,29 @@ class DataResolver {
|
|
|
849
1140
|
} catch {
|
|
850
1141
|
return fail("E_DATA_JSON", "Invalid JSON.", "Use a JSON array of records, without comments or trailing commas.");
|
|
851
1142
|
}
|
|
1143
|
+
let grouped;
|
|
1144
|
+
if (block.name === "table" && block.props.prepared === "grouped") {
|
|
1145
|
+
const structural = checkCompositionStructure(parsed);
|
|
1146
|
+
if (structural)
|
|
1147
|
+
return fail("E_TABLE_GROUPS", structural);
|
|
1148
|
+
const result = subtotalSchema.safeParse(parsed);
|
|
1149
|
+
if (!result.success)
|
|
1150
|
+
return fail("E_TABLE_GROUPS", result.error.issues.slice(0, 8).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; "));
|
|
1151
|
+
grouped = {
|
|
1152
|
+
scope: result.data.scope,
|
|
1153
|
+
groups: result.data.groups.map((group) => ({
|
|
1154
|
+
id: group.id,
|
|
1155
|
+
label: group.label,
|
|
1156
|
+
size: group.rows.length
|
|
1157
|
+
}))
|
|
1158
|
+
};
|
|
1159
|
+
parsed = result.data.groups.flatMap((group) => [
|
|
1160
|
+
...group.rows,
|
|
1161
|
+
group.subtotal
|
|
1162
|
+
]);
|
|
1163
|
+
}
|
|
852
1164
|
if (!Array.isArray(parsed))
|
|
853
|
-
return fail("E_DATA_SHAPE", "Expected an array of flat JSON records.");
|
|
1165
|
+
return fail("E_DATA_SHAPE", "Expected an array of flat JSON records, or declare prepared=grouped for a subtotal bundle.");
|
|
854
1166
|
if (parsed.length > limits.rows)
|
|
855
1167
|
return fail("E_DATA_LIMIT", `Dataset exceeds ${limits.rows.toLocaleString("en-GB")} rows.`, "Prepare a smaller dataset before rendering.");
|
|
856
1168
|
const arrays = new Map;
|
|
@@ -871,21 +1183,44 @@ class DataResolver {
|
|
|
871
1183
|
return fail("E_DATA_SHAPE", `Row ${index + 1}, field ${JSON.stringify(key)} must contain a finite number, string, boolean or null.`, "Prepare flat scalar records; use an explicit sparkline column for bounded numeric/null sequences.");
|
|
872
1184
|
}
|
|
873
1185
|
}
|
|
874
|
-
dataset = {
|
|
1186
|
+
dataset = {
|
|
1187
|
+
rows: parsed,
|
|
1188
|
+
arrays,
|
|
1189
|
+
...grouped ? { grouped } : {}
|
|
1190
|
+
};
|
|
875
1191
|
this.cache.set(canonical, dataset);
|
|
876
1192
|
}
|
|
1193
|
+
if (Boolean(dataset.grouped) !== (block.name === "table" && block.props.prepared === "grouped"))
|
|
1194
|
+
return fail("E_DATA_SHAPE", "Grouped subtotal bundles require prepared=grouped and cannot be used as ordinary datasets.");
|
|
1195
|
+
if (block.name === "table" && dataset.grouped)
|
|
1196
|
+
block.tableGroups = dataset.grouped;
|
|
877
1197
|
const declared = new Set(block.name === "table" ? block.children.flatMap((child) => child.type === "component" && child.name === "column" && child.props.kind === "sparkline" ? [child.props.field] : []) : []);
|
|
878
1198
|
for (const [field, index] of dataset.arrays)
|
|
879
1199
|
if (!declared.has(field))
|
|
880
1200
|
return fail("E_DATA_SHAPE", `Row ${index + 1}, field ${JSON.stringify(field)} is a sequence without an explicit sparkline column.`, 'Use a table column with kind="sparkline" for that field. Charts and scalar tables require flat scalar datasets.');
|
|
881
1201
|
return dataset.rows;
|
|
882
1202
|
}
|
|
883
|
-
async resolve(blocks) {
|
|
884
|
-
for (
|
|
1203
|
+
async resolve(blocks, parent = "root") {
|
|
1204
|
+
for (let index = 0;index < blocks.length; index++) {
|
|
1205
|
+
let block = blocks[index];
|
|
1206
|
+
if (block.type === "component" && block.name === "component") {
|
|
1207
|
+
const expanded = await expandComposition(block, this.files, this.diagnostics, this.compositions, parent);
|
|
1208
|
+
blocks.splice(index, 1, ...expanded);
|
|
1209
|
+
if (!expanded.length) {
|
|
1210
|
+
index--;
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
index--;
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
885
1216
|
if (block.type !== "component")
|
|
886
1217
|
continue;
|
|
887
1218
|
if (block.name === "explore") {
|
|
888
|
-
|
|
1219
|
+
if (this.inExploration) {
|
|
1220
|
+
this.diagnostics.add("E_EXPLORE_SCHEMA", "Nested explorations are unsupported, including inside compositions.", block.position);
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
await resolveExploration(block, this.files, this.diagnostics, (children, files, diagnostics) => new DataResolver(diagnostics, files, this.images, this.expansion, this.compositions, true, this.cache).resolve(children), this.expansion);
|
|
889
1224
|
continue;
|
|
890
1225
|
}
|
|
891
1226
|
if (block.name === "line" || block.name === "area" || block.name === "bar" || block.name === "donut" || block.name === "funnel" || block.name === "table") {
|
|
@@ -901,7 +1236,15 @@ class DataResolver {
|
|
|
901
1236
|
this.resolveChart(block, rows);
|
|
902
1237
|
}
|
|
903
1238
|
}
|
|
904
|
-
|
|
1239
|
+
if (block.compositionOrigin && block.rows) {
|
|
1240
|
+
this.compositions.rows += block.rows.length;
|
|
1241
|
+
this.compositions.dataBytes += new TextEncoder().encode(JSON.stringify(block.rows)).length;
|
|
1242
|
+
if (this.compositions.rows > compositionLimits.rows || this.compositions.dataBytes > compositionLimits.dataBytes) {
|
|
1243
|
+
this.diagnostics.add("E_COMPONENT_LIMIT", "Expanded composition data exceeds its row or byte budget.", block.position, "Prepare a smaller dataset or reuse fewer components.");
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
await this.resolve(block.children, block.name);
|
|
905
1248
|
}
|
|
906
1249
|
}
|
|
907
1250
|
field(rows, name, block) {
|
|
@@ -1686,7 +2029,8 @@ var serializer = unified2().use(rehypeSanitize, {
|
|
|
1686
2029
|
ul: ["className", "role"],
|
|
1687
2030
|
ol: ["start", "className", "role"],
|
|
1688
2031
|
li: ["className", "dataStatus"],
|
|
1689
|
-
th: ["scope", "ariaSort", "className"],
|
|
2032
|
+
th: ["scope", "ariaSort", "className", "colSpan"],
|
|
2033
|
+
tr: ["className", "dataPlotleafGroupHeader", "dataPlotleafSubtotal"],
|
|
1690
2034
|
td: ["className", "dataRaw"]
|
|
1691
2035
|
}
|
|
1692
2036
|
}).use(rehypeStringify);
|
|
@@ -1700,11 +2044,14 @@ function slugOf(text) {
|
|
|
1700
2044
|
function cellText(value, column) {
|
|
1701
2045
|
return value === null ? "—" : typeof value === "number" ? formatValue(value, column.format, column.sign) : String(value);
|
|
1702
2046
|
}
|
|
1703
|
-
function table(rows, columns, title, id, sortable = true) {
|
|
2047
|
+
function table(rows, columns, title, id, sortable = true, groups) {
|
|
1704
2048
|
const scales = columns.map((column) => column.kind === "sparkline" ? columnScale(rows, column.field) : undefined);
|
|
1705
2049
|
const canSort = columns.map((column) => sortable && (!column.kind || column.kind === "scalar" || Boolean(column.sortBy)));
|
|
1706
2050
|
const numeric = columns.map((column) => column.kind === "sparkline" || rows.some((row) => typeof row[column.field] === "number") || Boolean(column.format));
|
|
1707
|
-
const caption = h("caption", {}, [
|
|
2051
|
+
const caption = h("caption", {}, [
|
|
2052
|
+
t(title),
|
|
2053
|
+
...groups ? [h("span", { className: ["table-scope"] }, [t(groups.scope)])] : []
|
|
2054
|
+
]);
|
|
1708
2055
|
const headers = columns.map((column, index) => {
|
|
1709
2056
|
const name = column.label ?? label2(column.field);
|
|
1710
2057
|
const content = canSort[index] ? h("button", {
|
|
@@ -1738,12 +2085,27 @@ function table(rows, columns, title, id, sortable = true) {
|
|
|
1738
2085
|
});
|
|
1739
2086
|
return h("tr", {}, cells);
|
|
1740
2087
|
});
|
|
1741
|
-
|
|
2088
|
+
let offset = 0;
|
|
2089
|
+
const bodies = groups ? groups.groups.map((group) => {
|
|
2090
|
+
const entries = tableRows.slice(offset, offset + group.size + 1);
|
|
2091
|
+
offset += group.size + 1;
|
|
2092
|
+
const subtotal = entries.at(-1);
|
|
2093
|
+
subtotal.properties.dataPlotleafSubtotal = "";
|
|
2094
|
+
subtotal.properties.className = ["table-subtotal"];
|
|
2095
|
+
return h("tbody", {}, [
|
|
2096
|
+
h("tr", { dataPlotleafGroupHeader: "", className: ["table-group"] }, [
|
|
2097
|
+
h("th", { scope: "rowgroup", colSpan: columns.length }, [
|
|
2098
|
+
t(group.label)
|
|
2099
|
+
])
|
|
2100
|
+
]),
|
|
2101
|
+
...entries
|
|
2102
|
+
]);
|
|
2103
|
+
}) : [h("tbody", {}, tableRows)];
|
|
1742
2104
|
return h("div", { className: ["table-scroll"] }, [
|
|
1743
2105
|
h("table", { id, ...sortable ? { dataPlotleafTable: "" } : {} }, [
|
|
1744
2106
|
caption,
|
|
1745
2107
|
head,
|
|
1746
|
-
|
|
2108
|
+
...bodies
|
|
1747
2109
|
])
|
|
1748
2110
|
]);
|
|
1749
2111
|
}
|
|
@@ -1798,6 +2160,8 @@ function renderContent(report) {
|
|
|
1798
2160
|
}
|
|
1799
2161
|
const id = `plotleaf-component-${String(++sequence).padStart(4, "0")}`;
|
|
1800
2162
|
switch (block.name) {
|
|
2163
|
+
case "component":
|
|
2164
|
+
return [];
|
|
1801
2165
|
case "explore":
|
|
1802
2166
|
return exploration(block, id, render, payload.explorations ??= []);
|
|
1803
2167
|
case "grid":
|
|
@@ -1850,7 +2214,7 @@ function renderContent(report) {
|
|
|
1850
2214
|
];
|
|
1851
2215
|
case "table":
|
|
1852
2216
|
return [
|
|
1853
|
-
table(block.rows ?? [], block.columns ?? [], block.props.title ?? "Data", id),
|
|
2217
|
+
table(block.rows ?? [], block.columns ?? [], block.props.title ?? "Data", id, true, block.tableGroups),
|
|
1854
2218
|
...!block.rows?.length ? [
|
|
1855
2219
|
h("p", { className: ["empty-state"] }, [
|
|
1856
2220
|
t("No data to display.")
|
|
@@ -2335,7 +2699,7 @@ data: ${revision}
|
|
|
2335
2699
|
// package.json
|
|
2336
2700
|
var package_default = {
|
|
2337
2701
|
name: "plotleaf",
|
|
2338
|
-
version: "0.
|
|
2702
|
+
version: "0.5.0",
|
|
2339
2703
|
description: "Beautiful, interactive reports from Markdown and JSON. Built for agents.",
|
|
2340
2704
|
type: "module",
|
|
2341
2705
|
license: "MIT",
|
|
@@ -2362,6 +2726,8 @@ var package_default = {
|
|
|
2362
2726
|
"dist/assets/notices.txt",
|
|
2363
2727
|
"schema/components.json",
|
|
2364
2728
|
"examples/report.md",
|
|
2729
|
+
"examples/m6/*.md",
|
|
2730
|
+
"examples/m6/*.json",
|
|
2365
2731
|
"examples/interactions/*.md",
|
|
2366
2732
|
"examples/interactions/*.json",
|
|
2367
2733
|
"examples/storefront/*.md",
|