@anchrd/colab-sdk 0.1.10 → 0.1.12
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/dist/actions/actions.d.ts +64 -11
- package/dist/actions/actions.js +22 -7
- package/dist/boards/boards.d.ts +321 -10
- package/dist/boards/boards.js +111 -13
- package/dist/card-kinds/card-kinds.d.ts +265 -0
- package/dist/card-kinds/card-kinds.js +153 -0
- package/dist/error/error.d.ts +6 -4
- package/dist/error/error.js +4 -3
- package/dist/gadgets/gadgets.d.ts +532 -0
- package/dist/gadgets/gadgets.js +329 -0
- package/dist/grammar/grammar.d.ts +1 -1
- package/dist/grammar/grammar.js +8 -1
- package/dist/nodes/nodes.d.ts +10 -10
- package/dist/nodes/nodes.js +4 -3
- package/dist/operators/operators.d.ts +4 -4
- package/dist/operators/operators.js +2 -2
- package/dist/runs/runs.d.ts +41 -70
- package/dist/runs/runs.js +14 -73
- package/dist/schedule/schedule.d.ts +36 -0
- package/dist/schedule/schedule.js +93 -0
- package/dist/sdk.d.ts +8 -6
- package/dist/sdk.js +8 -6
- package/dist/tables/tables.d.ts +8 -0
- package/dist/tables/tables.js +29 -4
- package/dist/users/users.js +2 -2
- package/dist/validation/validation.d.ts +2 -55
- package/dist/validation/validation.js +3 -22
- package/dist/versions/versions.d.ts +233 -1
- package/dist/versions/versions.js +57 -10
- package/package.json +1 -1
- package/dist/dashboards/dashboards.d.ts +0 -221
- package/dist/dashboards/dashboards.js +0 -520
|
@@ -1,520 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { idSchema } from "../ids/ids.js";
|
|
3
|
-
import { NodeId } from "../nodes/nodes.js";
|
|
4
|
-
import { IdempotencyKey, Rev } from "../primitives/primitives.js";
|
|
5
|
-
import { TimeZone } from "../profile/profile.js";
|
|
6
|
-
// Dashboards (#44, Nodes › Dashboard, Funktionen › Läufe, E46, E70): the schedule, the configuration
|
|
7
|
-
// (metrics, sections, boxes) and the run, the inputs of `intelligence_dashboard_create`, `_get`,
|
|
8
|
-
// `_update`, and the answer. The configuration is ONE schema for the UI's forms (#61) and for the
|
|
9
|
-
// YAML an AI writes: the backend parses the YAML first and then runs this schema, so both ways meet
|
|
10
|
-
// the same checks. Every rule of the check is its own function below, and every finding names the
|
|
11
|
-
// box it is about by its path (`["sections", 1, "boxes", 0, "uses"]`).
|
|
12
|
-
//
|
|
13
|
-
// ⚠️ No rule here may generate code from strings (trap P081): `workerd` refuses `eval` and
|
|
14
|
-
// `new Function`, and the vitest pool patches them, so a test would pass where the Worker fails.
|
|
15
|
-
// The checks are plain functions over the parsed object.
|
|
16
|
-
export const DashboardId = idSchema("node").describe("The dashboard's id, its node id `node_…`, as intelligence_node_list or intelligence_dashboard_create returned it.");
|
|
17
|
-
// ---------------------------------------------------------------------------------------------
|
|
18
|
-
// The schedule (Ausführung): the same for a dashboard and a process board (#43).
|
|
19
|
-
export const rhythms = ["manual", "daily", "weekly", "monthly", "dates"];
|
|
20
|
-
export const weekdays = [
|
|
21
|
-
"monday",
|
|
22
|
-
"tuesday",
|
|
23
|
-
"wednesday",
|
|
24
|
-
"thursday",
|
|
25
|
-
"friday",
|
|
26
|
-
"saturday",
|
|
27
|
-
"sunday",
|
|
28
|
-
];
|
|
29
|
-
// No seconds and no zone on the string: a run happens at eight, and the zone is the schedule's own.
|
|
30
|
-
// `24:00` is refused rather than read as midnight, so one moment has one spelling (anchrd/intel
|
|
31
|
-
// `contract/process.ts`, f4eccd16).
|
|
32
|
-
const TimeOfDay = z
|
|
33
|
-
.string()
|
|
34
|
-
.regex(/^([01]\d|2[0-3]):[0-5]\d$/, "A time of day is HH:MM, from 00:00 to 23:59, like 08:00.");
|
|
35
|
-
// A fixed moment on the wall clock of the schedule's zone, without seconds and without an offset.
|
|
36
|
-
const LocalMoment = z
|
|
37
|
-
.string()
|
|
38
|
-
.regex(/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d$/, "A date is YYYY-MM-DDTHH:MM on the schedule's clock, like 2026-10-15T08:00.");
|
|
39
|
-
// Which fields each rhythm takes; every other one is refused, so nothing lies stored that no rhythm
|
|
40
|
-
// reads.
|
|
41
|
-
const rhythmFields = {
|
|
42
|
-
manual: [],
|
|
43
|
-
daily: ["times"],
|
|
44
|
-
weekly: ["days", "times"],
|
|
45
|
-
monthly: ["dayOfMonth", "times"],
|
|
46
|
-
dates: ["dates"],
|
|
47
|
-
};
|
|
48
|
-
const scheduleFields = ["days", "dayOfMonth", "times", "dates"];
|
|
49
|
-
// What happens when a run falls due while the last one is still open (E75, Funktionen › Läufe):
|
|
50
|
-
// wait (the default) starts none until the last is completed, continue starts it anyway and the
|
|
51
|
-
// open ones stay open.
|
|
52
|
-
export const whileOpenModes = ["wait", "continue"];
|
|
53
|
-
export const WhileOpen = z
|
|
54
|
-
.enum(whileOpenModes)
|
|
55
|
-
.describe("When the last run is still open: wait (the default) starts no new run until it is completed, and the process stands overdue in the Actions; continue starts the new run anyway, and the open ones stay open.");
|
|
56
|
-
export const Schedule = z
|
|
57
|
-
.strictObject({
|
|
58
|
-
rhythm: z
|
|
59
|
-
.enum(rhythms)
|
|
60
|
-
.describe("How often a run falls due: manual (a person starts it), daily, weekly (on days), monthly (on dayOfMonth), or dates (fixed moments, each once)."),
|
|
61
|
-
days: z
|
|
62
|
-
.array(z.enum(weekdays))
|
|
63
|
-
.min(1)
|
|
64
|
-
.max(7)
|
|
65
|
-
.optional()
|
|
66
|
-
.describe("The weekdays of a weekly rhythm, like [monday, thursday]. Only with weekly."),
|
|
67
|
-
dayOfMonth: z
|
|
68
|
-
.array(z.int().min(1).max(31))
|
|
69
|
-
.min(1)
|
|
70
|
-
.max(31)
|
|
71
|
-
.optional()
|
|
72
|
-
.describe("The days of the month of a monthly rhythm, like [1, 15]. A month without that day is skipped, never moved to its last day. Only with monthly."),
|
|
73
|
-
times: z
|
|
74
|
-
.array(TimeOfDay)
|
|
75
|
-
.min(1)
|
|
76
|
-
.max(24)
|
|
77
|
-
.optional()
|
|
78
|
-
.describe("The times of day a daily, weekly or monthly run falls due, HH:MM in zone, like [08:00]."),
|
|
79
|
-
dates: z
|
|
80
|
-
.array(LocalMoment)
|
|
81
|
-
.min(1)
|
|
82
|
-
.max(200)
|
|
83
|
-
.optional()
|
|
84
|
-
.describe("The fixed moments of the rhythm dates, YYYY-MM-DDTHH:MM in zone, each falling due once. Only with dates."),
|
|
85
|
-
zone: TimeZone.optional().describe("The IANA time zone the times and dates are read in, like Europe/Berlin. Leaving it out takes the time zone of the creator of the dashboard or process board."),
|
|
86
|
-
whileOpen: WhileOpen.optional().describe("When the last run is still open: wait starts no new run until it is completed, continue starts it anyway and the open ones stay open. Leaving it out is wait."),
|
|
87
|
-
})
|
|
88
|
-
.superRefine((schedule, ctx) => {
|
|
89
|
-
const wanted = rhythmFields[schedule.rhythm];
|
|
90
|
-
for (const field of scheduleFields) {
|
|
91
|
-
const present = schedule[field] !== undefined;
|
|
92
|
-
if (wanted.includes(field) && !present) {
|
|
93
|
-
ctx.addIssue({
|
|
94
|
-
code: "custom",
|
|
95
|
-
path: [field],
|
|
96
|
-
message: `A ${schedule.rhythm} rhythm needs ${field}${field === "times" ? ", like [08:00]" : ""}.`,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
if (!wanted.includes(field) && present) {
|
|
100
|
-
ctx.addIssue({
|
|
101
|
-
code: "custom",
|
|
102
|
-
path: [field],
|
|
103
|
-
message: `${field} belongs to ${field === "times" ? "a daily, weekly or monthly" : `a ${Object.entries(rhythmFields).find(([, fields]) => fields.includes(field))?.[0]}`} rhythm, not to ${schedule.rhythm}; leave it out.`,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
// ---------------------------------------------------------------------------------------------
|
|
109
|
-
// The configuration: metrics, sections, boxes (Nodes › Dashboard).
|
|
110
|
-
export const boxTypes = ["metric", "line", "bar", "area", "summary", "note"];
|
|
111
|
-
export const chartTypes = ["line", "bar", "area"];
|
|
112
|
-
// A key stays fixed when the title changes, so old runs and table columns keep their place. It
|
|
113
|
-
// starts with a letter, so an object keeps the order it was written in (JavaScript puts keys that
|
|
114
|
-
// look like numbers first).
|
|
115
|
-
export const DashboardKey = z
|
|
116
|
-
.string()
|
|
117
|
-
.regex(/^[a-z][a-z0-9_]{0,63}$/, "A key is lowercase letters, digits and underscores, starting with a letter, like lead_fit.");
|
|
118
|
-
// Who delivers a metric's value or a summary's text in every run (E78, Nodes › Dashboard, Zuständig
|
|
119
|
-
// je Kennzahl): a person or a machine that reads the dashboard. Without one the run has nobody for
|
|
120
|
-
// that entry. Spelled out here instead of importing `users`, whose module graph reaches far.
|
|
121
|
-
const Assignee = idSchema("user").describe("Who delivers this in every run, `user_…`: a person or a machine that can read the dashboard (find them with intelligence_node_grant_list or intelligence_user_list). Only they write its value, report and recommendation. Leaving it out leaves the entry without anybody.");
|
|
122
|
-
// A required text whose absence says what it is for (trap P026: the schema's own sentence is all
|
|
123
|
-
// a model reads).
|
|
124
|
-
function required(max, missing) {
|
|
125
|
-
return z
|
|
126
|
-
.string({
|
|
127
|
-
error: (issue) => (issue.input === undefined ? missing : "This is text."),
|
|
128
|
-
})
|
|
129
|
-
.trim()
|
|
130
|
-
.min(1, missing)
|
|
131
|
-
.max(max);
|
|
132
|
-
}
|
|
133
|
-
const Text = z.string().max(10_000);
|
|
134
|
-
const DashboardMetric = z
|
|
135
|
-
.strictObject({
|
|
136
|
-
title: required(200, "A metric needs a title: the label of its box and its name in every chart.").describe("The metric's name as people read it: the label of its box and of chart legends."),
|
|
137
|
-
description: Text.optional().describe("What people see as the tooltip of the metric."),
|
|
138
|
-
instructions: required(10_000, "A metric needs instructions: what it is and where the AI finds it.").describe("What the metric is and where the AI finds it, in plain sentences: what the Operator reads before every run."),
|
|
139
|
-
source: Text.optional().describe("Where the number comes from in words, like Leads Pipeline or DWH. The exact source of each value is in the run."),
|
|
140
|
-
assignee: Assignee.optional(),
|
|
141
|
-
})
|
|
142
|
-
.describe("A metric, defined once: each run gives it a value and becomes one column of the table.");
|
|
143
|
-
const DashboardBox = z
|
|
144
|
-
.strictObject({
|
|
145
|
-
metric: DashboardKey.optional().describe("A box showing one metric, named by its key in metrics. Takes no other field; it is 1 wide."),
|
|
146
|
-
line: DashboardKey.optional().describe("A line chart and its own key. Takes title, description and shows; it is 2 wide."),
|
|
147
|
-
bar: DashboardKey.optional().describe("A bar chart and its own key. Takes title, description and shows; it is 2 wide."),
|
|
148
|
-
area: DashboardKey.optional().describe("An area chart and its own key. Takes title, description and shows; it is 2 wide."),
|
|
149
|
-
summary: DashboardKey.optional().describe("A text the Operator writes each run, and its own key. Takes title, description, instructions, uses and width."),
|
|
150
|
-
note: DashboardKey.optional().describe("A fixed text by a person, and its own key. Takes title, description (its Markdown) and width."),
|
|
151
|
-
title: z.string().trim().max(200).optional().describe("The label of a chart, summary or note."),
|
|
152
|
-
description: Text.optional().describe("The tooltip of a chart's or summary's label; a note's visible text in Markdown."),
|
|
153
|
-
instructions: Text.optional().describe("What the Operator writes in a summary each run. A summary needs it."),
|
|
154
|
-
width: z
|
|
155
|
-
.int({ error: "width is 1, 2 or 3." })
|
|
156
|
-
.min(1, "width is 1, 2 or 3.")
|
|
157
|
-
.max(3, "width is 1, 2 or 3.")
|
|
158
|
-
.optional()
|
|
159
|
-
.describe("How many of the three columns a summary or note spans: 1, 2 or 3. Every other box has a fixed width."),
|
|
160
|
-
shows: z
|
|
161
|
-
.array(DashboardKey)
|
|
162
|
-
.max(20)
|
|
163
|
-
.optional()
|
|
164
|
-
.describe("The metrics a chart shows, by key, at least one, in the order of the legend."),
|
|
165
|
-
uses: z
|
|
166
|
-
.array(DashboardKey)
|
|
167
|
-
.max(50)
|
|
168
|
-
.optional()
|
|
169
|
-
.describe("The boxes of the same section a summary builds on, by key: a metric box by its metric's key."),
|
|
170
|
-
assignee: Assignee.optional().describe("Who writes a summary in every run, `user_…`: a person or a machine that can read the dashboard. Only on a summary. Leaving it out leaves the summary without anybody."),
|
|
171
|
-
})
|
|
172
|
-
.describe("A box, written as its type and key (`metric: leads`, `line: trend`, `summary: verdict`, `note: hint`), then its fields.");
|
|
173
|
-
const DashboardSection = z
|
|
174
|
-
.strictObject({
|
|
175
|
-
title: required(200, "A section needs a title: its heading.").describe("The section's heading."),
|
|
176
|
-
description: Text.optional().describe("The introduction under the heading, visible."),
|
|
177
|
-
boxes: z
|
|
178
|
-
.array(DashboardBox)
|
|
179
|
-
.max(60)
|
|
180
|
-
.default([])
|
|
181
|
-
.describe("The section's boxes in reading order: left to right, then the next row once the widths pass 3."),
|
|
182
|
-
})
|
|
183
|
-
.describe("A section of the dashboard: a heading, an introduction and its boxes, 3 wide.");
|
|
184
|
-
function placed(config) {
|
|
185
|
-
return config.sections.flatMap((section, s) => section.boxes.map((box, index) => ({
|
|
186
|
-
box,
|
|
187
|
-
section: s,
|
|
188
|
-
index,
|
|
189
|
-
types: boxTypes.filter((type) => box[type] !== undefined),
|
|
190
|
-
})));
|
|
191
|
-
}
|
|
192
|
-
const at = (entry, ...rest) => [
|
|
193
|
-
"sections",
|
|
194
|
-
entry.section,
|
|
195
|
-
"boxes",
|
|
196
|
-
entry.index,
|
|
197
|
-
...rest,
|
|
198
|
-
];
|
|
199
|
-
// The boxes whose type is clear: the other rules ask nothing of a box `oneType` refused.
|
|
200
|
-
function typed(config) {
|
|
201
|
-
return placed(config).flatMap((entry) => {
|
|
202
|
-
const [type] = entry.types;
|
|
203
|
-
if (type === undefined || entry.types.length !== 1)
|
|
204
|
-
return [];
|
|
205
|
-
return [{ ...entry, type, key: entry.box[type] }];
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
const isChart = (type) => chartTypes.includes(type);
|
|
209
|
-
// Every box names exactly one type with its key.
|
|
210
|
-
function oneType(config) {
|
|
211
|
-
return placed(config).flatMap((entry) => {
|
|
212
|
-
if (entry.types.length === 1)
|
|
213
|
-
return [];
|
|
214
|
-
return [
|
|
215
|
-
{
|
|
216
|
-
path: at(entry),
|
|
217
|
-
message: entry.types.length === 0
|
|
218
|
-
? "A box names its type and key, one of metric, line, bar, area, summary or note, like `metric: leads`."
|
|
219
|
-
: `A box has one type; this one names ${entry.types.join(" and ")}.`,
|
|
220
|
-
},
|
|
221
|
-
];
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
// Each type takes its own fields (width is `widthFits`'s).
|
|
225
|
-
const fieldsOf = {
|
|
226
|
-
metric: [],
|
|
227
|
-
line: ["title", "description", "shows"],
|
|
228
|
-
bar: ["title", "description", "shows"],
|
|
229
|
-
area: ["title", "description", "shows"],
|
|
230
|
-
summary: ["title", "description", "instructions", "uses", "assignee"],
|
|
231
|
-
note: ["title", "description"],
|
|
232
|
-
};
|
|
233
|
-
function fieldsFit(config) {
|
|
234
|
-
return typed(config).flatMap((entry) => ["title", "description", "instructions", "shows", "uses", "assignee"]
|
|
235
|
-
.filter((field) => entry.box[field] !== undefined && !fieldsOf[entry.type].includes(field))
|
|
236
|
-
.map((field) => ({
|
|
237
|
-
path: at(entry, field),
|
|
238
|
-
message: entry.type === "metric"
|
|
239
|
-
? `A metric box takes only its key; its ${field} belongs to metrics.${entry.key}.`
|
|
240
|
-
: `A ${entry.type} box takes ${fieldsOf[entry.type].join(", ")}; ${field} does not belong here.`,
|
|
241
|
-
})));
|
|
242
|
-
}
|
|
243
|
-
// Rule: keys are unique. A metric shows as a box once, and every other box has a key of its own
|
|
244
|
-
// that is no metric's.
|
|
245
|
-
function uniqueKeys(config) {
|
|
246
|
-
const seen = new Map();
|
|
247
|
-
return typed(config).flatMap((entry) => {
|
|
248
|
-
const first = seen.get(entry.key);
|
|
249
|
-
if (first) {
|
|
250
|
-
return [
|
|
251
|
-
{
|
|
252
|
-
path: at(entry, entry.type),
|
|
253
|
-
message: `The key ${entry.key} is taken by the box at sections[${first.section}].boxes[${first.index}]; every key appears once.`,
|
|
254
|
-
},
|
|
255
|
-
];
|
|
256
|
-
}
|
|
257
|
-
seen.set(entry.key, entry);
|
|
258
|
-
if (entry.type !== "metric" && Object.hasOwn(config.metrics, entry.key)) {
|
|
259
|
-
return [
|
|
260
|
-
{
|
|
261
|
-
path: at(entry, entry.type),
|
|
262
|
-
message: `The key ${entry.key} is a metric's; a ${entry.type} box needs a key of its own.`,
|
|
263
|
-
},
|
|
264
|
-
];
|
|
265
|
-
}
|
|
266
|
-
return [];
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
// Rule: every box refers to a key that exists.
|
|
270
|
-
function refsExist(config) {
|
|
271
|
-
const boxKeys = new Set(typed(config).map((entry) => entry.key));
|
|
272
|
-
const metrics = Object.keys(config.metrics);
|
|
273
|
-
const known = metrics.length > 0 ? `the metrics are ${metrics.join(", ")}` : "there is no metric";
|
|
274
|
-
return typed(config).flatMap((entry) => {
|
|
275
|
-
if (entry.type === "metric" && !Object.hasOwn(config.metrics, entry.key)) {
|
|
276
|
-
return [
|
|
277
|
-
{
|
|
278
|
-
path: at(entry, "metric"),
|
|
279
|
-
message: `There is no metric ${entry.key}; ${known}. Define it under metrics first.`,
|
|
280
|
-
},
|
|
281
|
-
];
|
|
282
|
-
}
|
|
283
|
-
const shows = (entry.box.shows ?? []).flatMap((key, i) => Object.hasOwn(config.metrics, key) || boxKeys.has(key)
|
|
284
|
-
? []
|
|
285
|
-
: [{ path: at(entry, "shows", i), message: `There is no metric ${key}; ${known}.` }]);
|
|
286
|
-
const uses = (entry.box.uses ?? []).flatMap((key, i) => boxKeys.has(key)
|
|
287
|
-
? []
|
|
288
|
-
: [
|
|
289
|
-
{
|
|
290
|
-
path: at(entry, "uses", i),
|
|
291
|
-
message: `There is no box ${key}; a summary uses boxes of its section by their key.`,
|
|
292
|
-
},
|
|
293
|
-
]);
|
|
294
|
-
return [...shows, ...uses];
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
// Rule: a chart shows metrics, at least one.
|
|
298
|
-
function chartsShowMetrics(config) {
|
|
299
|
-
const boxKeys = new Set(typed(config).map((entry) => entry.key));
|
|
300
|
-
return typed(config).flatMap((entry) => {
|
|
301
|
-
if (!isChart(entry.type))
|
|
302
|
-
return [];
|
|
303
|
-
const shows = entry.box.shows ?? [];
|
|
304
|
-
if (shows.length === 0) {
|
|
305
|
-
return [
|
|
306
|
-
{
|
|
307
|
-
path: at(entry, "shows"),
|
|
308
|
-
message: `A ${entry.type} chart shows at least one metric, like shows: [leads].`,
|
|
309
|
-
},
|
|
310
|
-
];
|
|
311
|
-
}
|
|
312
|
-
return shows.flatMap((key, i) => !Object.hasOwn(config.metrics, key) && boxKeys.has(key)
|
|
313
|
-
? [
|
|
314
|
-
{
|
|
315
|
-
path: at(entry, "shows", i),
|
|
316
|
-
message: `${key} is a box, not a metric; a chart shows metrics.`,
|
|
317
|
-
},
|
|
318
|
-
]
|
|
319
|
-
: []);
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
// Rule: a summary uses only boxes of its own section, and never itself.
|
|
323
|
-
function usesOwnSection(config) {
|
|
324
|
-
const boxes = typed(config);
|
|
325
|
-
return boxes.flatMap((entry) => (entry.box.uses ?? []).flatMap((key, i) => {
|
|
326
|
-
if (key === entry.key) {
|
|
327
|
-
return [{ path: at(entry, "uses", i), message: "A summary cannot use itself." }];
|
|
328
|
-
}
|
|
329
|
-
const home = boxes.filter((other) => other.key === key);
|
|
330
|
-
// Unknown is `refsExist`'s; a key written twice is `uniqueKeys`', so one in the own section
|
|
331
|
-
// is enough here.
|
|
332
|
-
if (home.length === 0 || home.some((other) => other.section === entry.section))
|
|
333
|
-
return [];
|
|
334
|
-
const section = home[0]?.section;
|
|
335
|
-
return [
|
|
336
|
-
{
|
|
337
|
-
path: at(entry, "uses", i),
|
|
338
|
-
message: `${key} is a box of section ${section} (${config.sections[section]?.title}); a summary uses only boxes of its own section.`,
|
|
339
|
-
},
|
|
340
|
-
];
|
|
341
|
-
}));
|
|
342
|
-
}
|
|
343
|
-
// Rule: width only on a summary or a note (its range is the field's own check).
|
|
344
|
-
function widthFits(config) {
|
|
345
|
-
return typed(config).flatMap((entry) => entry.box.width !== undefined && entry.type !== "summary" && entry.type !== "note"
|
|
346
|
-
? [
|
|
347
|
-
{
|
|
348
|
-
path: at(entry, "width"),
|
|
349
|
-
message: `width is only for a summary or a note; a metric is 1 wide, a chart 2, a section 3. Leave it out of this ${entry.type} box.`,
|
|
350
|
-
},
|
|
351
|
-
]
|
|
352
|
-
: []);
|
|
353
|
-
}
|
|
354
|
-
// Rule: a note has a description, its visible text.
|
|
355
|
-
function noteHasDescription(config) {
|
|
356
|
-
return typed(config).flatMap((entry) => entry.type === "note" && !entry.box.description?.trim()
|
|
357
|
-
? [
|
|
358
|
-
{
|
|
359
|
-
path: at(entry, "description"),
|
|
360
|
-
message: "A note shows its description; write the note's text there.",
|
|
361
|
-
},
|
|
362
|
-
]
|
|
363
|
-
: []);
|
|
364
|
-
}
|
|
365
|
-
// Rule: a summary has instructions (a metric's are required by its own schema).
|
|
366
|
-
function summaryHasInstructions(config) {
|
|
367
|
-
return typed(config).flatMap((entry) => entry.type === "summary" && !entry.box.instructions?.trim()
|
|
368
|
-
? [
|
|
369
|
-
{
|
|
370
|
-
path: at(entry, "instructions"),
|
|
371
|
-
message: "A summary needs instructions: what the Operator writes in it each run.",
|
|
372
|
-
},
|
|
373
|
-
]
|
|
374
|
-
: []);
|
|
375
|
-
}
|
|
376
|
-
const configRules = [
|
|
377
|
-
oneType,
|
|
378
|
-
fieldsFit,
|
|
379
|
-
uniqueKeys,
|
|
380
|
-
refsExist,
|
|
381
|
-
chartsShowMetrics,
|
|
382
|
-
usesOwnSection,
|
|
383
|
-
widthFits,
|
|
384
|
-
noteHasDescription,
|
|
385
|
-
summaryHasInstructions,
|
|
386
|
-
];
|
|
387
|
-
export const DashboardConfig = z
|
|
388
|
-
.strictObject({
|
|
389
|
-
instruction: z
|
|
390
|
-
.string()
|
|
391
|
-
.max(200_000)
|
|
392
|
-
.optional()
|
|
393
|
-
.describe("The dashboard's instruction, Markdown: what the dashboard is about and how every AI filling it goes about it (E87). It comes along with every read of the dashboard over MCP; the instructions of a metric or a summary stay for the single entry. Leaving it out, or an empty text, means none."),
|
|
394
|
-
schedule: Schedule.default({ rhythm: "manual" }).describe("When a run falls due (Ausführung). Leaving it out is manual."),
|
|
395
|
-
metrics: z
|
|
396
|
-
.record(DashboardKey, DashboardMetric)
|
|
397
|
-
.default({})
|
|
398
|
-
.describe("Every metric once, under its fixed key, in the order of the table's columns: title, description, instructions, source, assignee."),
|
|
399
|
-
sections: z
|
|
400
|
-
.array(DashboardSection)
|
|
401
|
-
.max(30)
|
|
402
|
-
.default([])
|
|
403
|
-
.describe("The sections from top to bottom, each with its boxes in reading order."),
|
|
404
|
-
})
|
|
405
|
-
.superRefine((config, ctx) => {
|
|
406
|
-
for (const rule of configRules) {
|
|
407
|
-
for (const finding of rule(config))
|
|
408
|
-
ctx.addIssue({ code: "custom", ...finding });
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
// ---------------------------------------------------------------------------------------------
|
|
412
|
-
// A run: the filled-in template (Funktionen › Läufe). Whether it fits ITS configuration (every
|
|
413
|
-
// metric and summary once, no unknown key) is asked by `validateRun` in `intelligence`.
|
|
414
|
-
const ReportText = z
|
|
415
|
-
.string({ error: "report is text: a sentence on what is missing or wrong." })
|
|
416
|
-
.max(10_000)
|
|
417
|
-
.nullable()
|
|
418
|
-
.optional()
|
|
419
|
-
.describe("Only when something is missing or wrong: what, in a sentence. Leave it out otherwise.");
|
|
420
|
-
const RecommendationText = z
|
|
421
|
-
.string({ error: "recommendation is text: how the instructions could become clearer." })
|
|
422
|
-
.max(10_000)
|
|
423
|
-
.nullable()
|
|
424
|
-
.optional()
|
|
425
|
-
.describe("Only with a report: how the instructions could become clearer.");
|
|
426
|
-
const DashboardRunMetric = z
|
|
427
|
-
.strictObject({
|
|
428
|
-
value: z
|
|
429
|
-
.union([z.number(), z.string().max(1_000), z.null()], {
|
|
430
|
-
error: (issue) => issue.input === undefined
|
|
431
|
-
? "value is required: the number or text, or ~ (null) when it is missing, then with a report."
|
|
432
|
-
: "value is a number, a short text, or ~ (null).",
|
|
433
|
-
})
|
|
434
|
-
.describe("The metric's value in this run: a number, a short text, or null when missing."),
|
|
435
|
-
source: z
|
|
436
|
-
.string({
|
|
437
|
-
error: (issue) => issue.input === undefined
|
|
438
|
-
? "source is required: the Colab node (colab://node_…) or the tool the value came from, or ~ (null)."
|
|
439
|
-
: "source is text: a Colab node as colab://node_… or a tool name.",
|
|
440
|
-
})
|
|
441
|
-
.max(2_000)
|
|
442
|
-
.nullable()
|
|
443
|
-
.describe("Where the value came from: a Colab node as colab://node_… (its id, which intelligence_node_list answers), or the tool called, with parameter."),
|
|
444
|
-
parameter: z
|
|
445
|
-
.record(z.string(), z.unknown())
|
|
446
|
-
.optional()
|
|
447
|
-
.describe("The parameters of the tool named in source, so the value can be checked."),
|
|
448
|
-
report: ReportText,
|
|
449
|
-
recommendation: RecommendationText,
|
|
450
|
-
})
|
|
451
|
-
.describe("One metric of the run.");
|
|
452
|
-
const DashboardRunSummary = z
|
|
453
|
-
.strictObject({
|
|
454
|
-
text: z
|
|
455
|
-
.string({
|
|
456
|
-
error: (issue) => issue.input === undefined
|
|
457
|
-
? "text is required: the summary, or ~ (null) when it could not be written, then with a report."
|
|
458
|
-
: "text is the summary as text.",
|
|
459
|
-
})
|
|
460
|
-
.max(20_000)
|
|
461
|
-
.nullable()
|
|
462
|
-
.describe("The summary the instructions ask for, or null when it could not be written."),
|
|
463
|
-
report: ReportText,
|
|
464
|
-
recommendation: RecommendationText,
|
|
465
|
-
})
|
|
466
|
-
.describe("One summary of the run.");
|
|
467
|
-
export const DashboardRun = z
|
|
468
|
-
.strictObject({
|
|
469
|
-
metrics: z
|
|
470
|
-
.record(DashboardKey, DashboardRunMetric)
|
|
471
|
-
.default({})
|
|
472
|
-
.describe("Every metric of the configuration once, under its key."),
|
|
473
|
-
summaries: z
|
|
474
|
-
.record(DashboardKey, DashboardRunSummary)
|
|
475
|
-
.default({})
|
|
476
|
-
.describe("Every summary of the configuration once, under its key."),
|
|
477
|
-
})
|
|
478
|
-
.describe("A run of a dashboard as the Operator fills in its template.");
|
|
479
|
-
// ---------------------------------------------------------------------------------------------
|
|
480
|
-
// The tools.
|
|
481
|
-
const Name = z.string().max(200);
|
|
482
|
-
const config = z
|
|
483
|
-
.record(z.string(), z.unknown())
|
|
484
|
-
.optional()
|
|
485
|
-
.describe("The configuration as an object, the way the UI sends it, in the shape intelligence_dashboard_get answers: instruction, schedule, metrics, sections. Send exactly one of config and yaml.");
|
|
486
|
-
const yaml = z
|
|
487
|
-
.string()
|
|
488
|
-
.max(200_000)
|
|
489
|
-
.optional()
|
|
490
|
-
.describe("The configuration as YAML text: instruction (the dashboard's Markdown for every AI, optional), schedule (rhythm, days, dayOfMonth, times, dates, zone, whileOpen), metrics (each once under a fixed key: title, description, instructions, source, assignee) and sections (title, description, boxes written as `metric: leads`, `line: trend` with shows, `summary: verdict` with instructions, uses and assignee, `note: hint` with description). Send exactly one of config and yaml.");
|
|
491
|
-
export const DashboardCreateInput = z.strictObject({
|
|
492
|
-
// ⚠️ Required and nullable (trap C015): null is the top level of Shared.
|
|
493
|
-
parentId: NodeId.nullable().describe("The folder to create the dashboard in, one you can write to; null creates it on the top level of Shared, visible to you until you share it."),
|
|
494
|
-
name: Name.describe("The dashboard's name as people read it."),
|
|
495
|
-
config,
|
|
496
|
-
yaml,
|
|
497
|
-
idempotencyKey: IdempotencyKey,
|
|
498
|
-
});
|
|
499
|
-
export const DashboardGetInput = z.strictObject({ dashboardId: DashboardId });
|
|
500
|
-
export const DashboardUpdateInput = z.strictObject({
|
|
501
|
-
dashboardId: DashboardId,
|
|
502
|
-
rev: Rev,
|
|
503
|
-
config: config.describe("The whole new configuration as an object, replacing the old one. Send exactly one of config and yaml."),
|
|
504
|
-
yaml: yaml.describe("The whole new configuration as YAML text, replacing the old one; read it from intelligence_dashboard_get, change it, send it back. Send exactly one of config and yaml."),
|
|
505
|
-
});
|
|
506
|
-
export const DashboardDetail = z.object({
|
|
507
|
-
dashboardId: DashboardId,
|
|
508
|
-
name: z.string(),
|
|
509
|
-
config: DashboardConfig,
|
|
510
|
-
yaml: z.string(),
|
|
511
|
-
// The next moment a run falls due (#43), null when manual or every date has passed.
|
|
512
|
-
nextRunAt: z.iso.datetime().nullable(),
|
|
513
|
-
// The moment a run is owed for now (passed, unanswered, no run open), else null.
|
|
514
|
-
dueAt: z.iso.datetime().nullable(),
|
|
515
|
-
// Whether a machine (an Operator) can write on it: a schedule other than manual needs one (#334).
|
|
516
|
-
processCheck: z.object({ operator: z.boolean() }),
|
|
517
|
-
rights: z.object({ write: z.boolean() }),
|
|
518
|
-
rev: z.number().int(),
|
|
519
|
-
updatedAt: z.iso.datetime(),
|
|
520
|
-
});
|