@chengxianglibra/dsh-data-analysis 0.1.1
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/LICENSE +21 -0
- package/README.md +72 -0
- package/cordis.patch.yml +11 -0
- package/lib/bin/environment.js +91 -0
- package/lib/bridges.js +11 -0
- package/lib/client.js +744 -0
- package/lib/compatibility.js +66 -0
- package/lib/datasource/access.js +60 -0
- package/lib/datasource/bridge-programs.js +80 -0
- package/lib/datasource/bridge.js +204 -0
- package/lib/datasource/credentials.js +24 -0
- package/lib/datasource/index.js +4 -0
- package/lib/datasource/shell-env.js +386 -0
- package/lib/datasource/test.js +65 -0
- package/lib/disclosure/activation.js +369 -0
- package/lib/disclosure/bridge-program.js +11 -0
- package/lib/disclosure/bridge.js +65 -0
- package/lib/disclosure/help.js +227 -0
- package/lib/disclosure/index.js +3 -0
- package/lib/environment/binding.js +273 -0
- package/lib/environment/doctor.js +123 -0
- package/lib/environment/errors.js +11 -0
- package/lib/environment/index.js +7 -0
- package/lib/environment/runtime.js +558 -0
- package/lib/environment/source.js +6 -0
- package/lib/environment/subprocess.js +178 -0
- package/lib/environment/summary.js +19 -0
- package/lib/environment/workspace.js +49 -0
- package/lib/evidence/bridge-program.js +153 -0
- package/lib/evidence/bridge.js +166 -0
- package/lib/evidence/index.js +2 -0
- package/lib/evidence/sources.js +263 -0
- package/lib/index.js +7 -0
- package/lib/plugin.js +179 -0
- package/lib/types/bridges.d.ts +11 -0
- package/lib/types/client.d.ts +125 -0
- package/lib/types/compatibility.d.ts +29 -0
- package/lib/types/datasource/access.d.ts +27 -0
- package/lib/types/datasource/bridge.d.ts +53 -0
- package/lib/types/datasource/index.d.ts +8 -0
- package/lib/types/datasource/shell-env.d.ts +38 -0
- package/lib/types/datasource/test.d.ts +32 -0
- package/lib/types/disclosure/activation.d.ts +73 -0
- package/lib/types/disclosure/bridge.d.ts +28 -0
- package/lib/types/disclosure/help.d.ts +64 -0
- package/lib/types/disclosure/index.d.ts +5 -0
- package/lib/types/environment/binding.d.ts +26 -0
- package/lib/types/environment/doctor.d.ts +8 -0
- package/lib/types/environment/errors.d.ts +7 -0
- package/lib/types/environment/index.d.ts +9 -0
- package/lib/types/environment/runtime.d.ts +17 -0
- package/lib/types/environment/source.d.ts +7 -0
- package/lib/types/environment/subprocess.d.ts +19 -0
- package/lib/types/environment/types.d.ts +96 -0
- package/lib/types/environment/workspace.d.ts +10 -0
- package/lib/types/evidence/bridge.d.ts +40 -0
- package/lib/types/evidence/index.d.ts +4 -0
- package/lib/types/evidence/sources.d.ts +57 -0
- package/lib/types/index.d.ts +9 -0
- package/lib/types/plugin.d.ts +43 -0
- package/package.json +149 -0
- package/python/report-kit/dist/dsh_data_analysis_report_kit-3.0.0-py3-none-any.whl +0 -0
- package/skills/dsh-data-analysis-report/SKILL.md +57 -0
- package/skills/dsh-data-analysis-report/assets/marivo-artifact.js +113 -0
- package/skills/dsh-data-analysis-report/assets/marivo-session-dag.js +1201 -0
- package/skills/dsh-data-analysis-report/assets/report-data.js +449 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
;((scope) => {
|
|
2
|
+
const datasets = new Map()
|
|
3
|
+
const recordViews = new Map()
|
|
4
|
+
const DATASET_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
|
|
5
|
+
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/
|
|
6
|
+
const ARTIFACT_ROLES = new Set(['time', 'dimension', 'value', 'measure', 'unknown'])
|
|
7
|
+
const ISSUE_CATEGORIES = new Set([
|
|
8
|
+
'data_quality',
|
|
9
|
+
'comparability',
|
|
10
|
+
'evidence_availability',
|
|
11
|
+
'candidate_resolution',
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
class ReportDataError extends Error {
|
|
15
|
+
constructor(code, path, message) {
|
|
16
|
+
super(`${path}: ${message}`)
|
|
17
|
+
this.name = 'ReportDataError'
|
|
18
|
+
this.code = code
|
|
19
|
+
this.path = path
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fail(path, message, code = 'dataset-invalid') {
|
|
24
|
+
throw new ReportDataError(code, path, message)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isObject(value) {
|
|
28
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function object(value, path) {
|
|
32
|
+
if (!isObject(value)) fail(path, 'must be an object')
|
|
33
|
+
return value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function exactKeys(value, path, required, optional = []) {
|
|
37
|
+
const keys = Object.keys(value)
|
|
38
|
+
const allowed = new Set([...required, ...optional])
|
|
39
|
+
for (const key of required) {
|
|
40
|
+
if (!Object.hasOwn(value, key)) fail(`${path}.${key}`, 'is required')
|
|
41
|
+
}
|
|
42
|
+
for (const key of keys) {
|
|
43
|
+
if (!allowed.has(key)) fail(`${path}.${key}`, 'is not allowed')
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function string(value, path, { nullable = false, maxLength = 2048 } = {}) {
|
|
48
|
+
if (nullable && value === null) return
|
|
49
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > maxLength) {
|
|
50
|
+
fail(path, `must be a non-empty string of at most ${maxLength} characters`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function integer(value, path, { nullable = false, maximum = Number.MAX_SAFE_INTEGER } = {}) {
|
|
55
|
+
if (nullable && value === null) return
|
|
56
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > maximum) {
|
|
57
|
+
fail(path, `must be an integer between 0 and ${maximum}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function number(value, path, { nullable = false } = {}) {
|
|
62
|
+
if (nullable && value === null) return
|
|
63
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) fail(path, 'must be a finite number')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function timestamp(value, path) {
|
|
67
|
+
if (
|
|
68
|
+
typeof value !== 'string' ||
|
|
69
|
+
!TIMESTAMP.test(value) ||
|
|
70
|
+
!Number.isFinite(Date.parse(value))
|
|
71
|
+
) {
|
|
72
|
+
fail(path, 'must be an RFC 3339 timestamp')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stringArray(value, path, maximum = 100) {
|
|
77
|
+
if (!Array.isArray(value) || value.length > maximum) {
|
|
78
|
+
fail(path, `must be an array with at most ${maximum} items`)
|
|
79
|
+
}
|
|
80
|
+
value.forEach((item, index) => {
|
|
81
|
+
string(item, `${path}[${index}]`)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateRepair(value, path, nullable) {
|
|
86
|
+
if (nullable && value === null) return
|
|
87
|
+
const repair = object(value, path)
|
|
88
|
+
exactKeys(repair, path, ['kind', 'action', 'help_target'])
|
|
89
|
+
if (
|
|
90
|
+
!['retry', 'inspect', 'user_choice', 'semantic_authoring', 'environment'].includes(
|
|
91
|
+
repair.kind,
|
|
92
|
+
)
|
|
93
|
+
) {
|
|
94
|
+
fail(`${path}.kind`, 'is unsupported')
|
|
95
|
+
}
|
|
96
|
+
string(repair.action, `${path}.action`)
|
|
97
|
+
string(repair.help_target, `${path}.help_target`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validateIssue(value, path, allowEvidenceRule = false) {
|
|
101
|
+
const issue = object(value, path)
|
|
102
|
+
string(issue.category, `${path}.category`)
|
|
103
|
+
if (allowEvidenceRule && issue.category === 'evidence_rule') {
|
|
104
|
+
exactKeys(issue, path, ['category', 'kind', 'severity', 'expected', 'received', 'repair'])
|
|
105
|
+
string(issue.kind, `${path}.kind`)
|
|
106
|
+
if (!['warning', 'blocking'].includes(issue.severity))
|
|
107
|
+
fail(`${path}.severity`, 'is unsupported')
|
|
108
|
+
string(issue.expected, `${path}.expected`)
|
|
109
|
+
string(issue.received, `${path}.received`)
|
|
110
|
+
validateRepair(issue.repair, `${path}.repair`, false)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if (!ISSUE_CATEGORIES.has(issue.category)) fail(`${path}.category`, 'is unsupported')
|
|
114
|
+
if (issue.category === 'data_quality') {
|
|
115
|
+
exactKeys(issue, path, ['category', 'kind', 'severity', 'check_id', 'expectation', 'repair'])
|
|
116
|
+
string(issue.check_id, `${path}.check_id`)
|
|
117
|
+
string(issue.expectation, `${path}.expectation`)
|
|
118
|
+
} else if (issue.category === 'comparability') {
|
|
119
|
+
exactKeys(issue, path, [
|
|
120
|
+
'category',
|
|
121
|
+
'kind',
|
|
122
|
+
'severity',
|
|
123
|
+
'incompatible_fields',
|
|
124
|
+
'approximation_details',
|
|
125
|
+
'repair',
|
|
126
|
+
])
|
|
127
|
+
stringArray(issue.incompatible_fields, `${path}.incompatible_fields`)
|
|
128
|
+
stringArray(issue.approximation_details, `${path}.approximation_details`)
|
|
129
|
+
} else if (issue.category === 'evidence_availability') {
|
|
130
|
+
exactKeys(issue, path, [
|
|
131
|
+
'category',
|
|
132
|
+
'kind',
|
|
133
|
+
'severity',
|
|
134
|
+
'failed_stage',
|
|
135
|
+
'findings_available',
|
|
136
|
+
'stable_error_category',
|
|
137
|
+
'repair',
|
|
138
|
+
])
|
|
139
|
+
if (!['extract', 'digest', 'store'].includes(issue.failed_stage)) {
|
|
140
|
+
fail(`${path}.failed_stage`, 'is unsupported')
|
|
141
|
+
}
|
|
142
|
+
if (typeof issue.findings_available !== 'boolean') {
|
|
143
|
+
fail(`${path}.findings_available`, 'must be boolean')
|
|
144
|
+
}
|
|
145
|
+
string(issue.stable_error_category, `${path}.stable_error_category`)
|
|
146
|
+
} else {
|
|
147
|
+
exactKeys(issue, path, ['category', 'kind', 'severity', 'historical', 'repair'])
|
|
148
|
+
if (issue.severity !== 'warning') fail(`${path}.severity`, 'must be warning')
|
|
149
|
+
if (typeof issue.historical !== 'boolean') fail(`${path}.historical`, 'must be boolean')
|
|
150
|
+
}
|
|
151
|
+
string(issue.kind, `${path}.kind`)
|
|
152
|
+
if (!['warning', 'blocking'].includes(issue.severity))
|
|
153
|
+
fail(`${path}.severity`, 'is unsupported')
|
|
154
|
+
validateRepair(issue.repair, `${path}.repair`, issue.category !== 'candidate_resolution')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function validateQuality(value, path) {
|
|
158
|
+
if (value === null) return
|
|
159
|
+
const quality = object(value, path)
|
|
160
|
+
const fields = [
|
|
161
|
+
'coverage',
|
|
162
|
+
'null_rate',
|
|
163
|
+
'sample_size',
|
|
164
|
+
'sample_coverage_min',
|
|
165
|
+
'sample_coverage_avg',
|
|
166
|
+
'sample_coverage_partial_buckets',
|
|
167
|
+
'zero_denominator_rows',
|
|
168
|
+
'evaluated_check_count',
|
|
169
|
+
'failed_check_count',
|
|
170
|
+
'warning_check_count',
|
|
171
|
+
]
|
|
172
|
+
exactKeys(quality, path, fields)
|
|
173
|
+
for (const field of ['coverage', 'null_rate', 'sample_coverage_min', 'sample_coverage_avg']) {
|
|
174
|
+
number(quality[field], `${path}.${field}`, { nullable: true })
|
|
175
|
+
}
|
|
176
|
+
for (const field of fields
|
|
177
|
+
.slice(2)
|
|
178
|
+
.filter((field) => !['sample_coverage_min', 'sample_coverage_avg'].includes(field))) {
|
|
179
|
+
integer(quality[field], `${path}.${field}`, { nullable: true })
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function validateRevalidation(value, path) {
|
|
184
|
+
const revalidation = object(value, path)
|
|
185
|
+
string(revalidation.status, `${path}.status`)
|
|
186
|
+
if (revalidation.status === 'not_checked') {
|
|
187
|
+
exactKeys(revalidation, path, ['status'])
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
if (revalidation.status !== 'checked') fail(`${path}.status`, 'is unsupported')
|
|
191
|
+
exactKeys(revalidation, path, [
|
|
192
|
+
'status',
|
|
193
|
+
'result',
|
|
194
|
+
'semantic_status',
|
|
195
|
+
'evidence_status',
|
|
196
|
+
'dependency_status',
|
|
197
|
+
'checked_at',
|
|
198
|
+
'issues',
|
|
199
|
+
'issues_omitted',
|
|
200
|
+
])
|
|
201
|
+
if (!['admissible', 'stale', 'indeterminate'].includes(revalidation.result)) {
|
|
202
|
+
fail(`${path}.result`, 'is unsupported')
|
|
203
|
+
}
|
|
204
|
+
if (!['current', 'stale', 'indeterminate'].includes(revalidation.semantic_status)) {
|
|
205
|
+
fail(`${path}.semantic_status`, 'is unsupported')
|
|
206
|
+
}
|
|
207
|
+
if (!['complete', 'partial', 'unavailable'].includes(revalidation.evidence_status)) {
|
|
208
|
+
fail(`${path}.evidence_status`, 'is unsupported')
|
|
209
|
+
}
|
|
210
|
+
if (!['admissible', 'stale', 'indeterminate'].includes(revalidation.dependency_status)) {
|
|
211
|
+
fail(`${path}.dependency_status`, 'is unsupported')
|
|
212
|
+
}
|
|
213
|
+
timestamp(revalidation.checked_at, `${path}.checked_at`)
|
|
214
|
+
if (!Array.isArray(revalidation.issues) || revalidation.issues.length > 100) {
|
|
215
|
+
fail(`${path}.issues`, 'must contain at most 100 issues')
|
|
216
|
+
}
|
|
217
|
+
revalidation.issues.forEach((issue, index) => {
|
|
218
|
+
validateIssue(issue, `${path}.issues[${index}]`, true)
|
|
219
|
+
})
|
|
220
|
+
integer(revalidation.issues_omitted, `${path}.issues_omitted`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function validateSource(value, path, table) {
|
|
224
|
+
const source = object(value, path)
|
|
225
|
+
string(source.kind, `${path}.kind`)
|
|
226
|
+
if (source.kind === 'computed') {
|
|
227
|
+
exactKeys(source, path, ['kind'])
|
|
228
|
+
if (Object.hasOwn(table, 'semantic_shape')) {
|
|
229
|
+
fail('$.table.semantic_shape', 'is not allowed for a computed dataset')
|
|
230
|
+
}
|
|
231
|
+
return 'computed'
|
|
232
|
+
}
|
|
233
|
+
if (source.kind !== 'marivo_artifact') fail(`${path}.kind`, 'is unsupported')
|
|
234
|
+
exactKeys(source, path, [
|
|
235
|
+
'kind',
|
|
236
|
+
'detail',
|
|
237
|
+
'artifact',
|
|
238
|
+
'quality_summary',
|
|
239
|
+
'issues',
|
|
240
|
+
'issues_omitted',
|
|
241
|
+
'lineage',
|
|
242
|
+
'revalidation',
|
|
243
|
+
])
|
|
244
|
+
if (!['reader', 'audit'].includes(source.detail)) fail(`${path}.detail`, 'is unsupported')
|
|
245
|
+
const artifact = object(source.artifact, `${path}.artifact`)
|
|
246
|
+
exactKeys(artifact, `${path}.artifact`, [
|
|
247
|
+
'session_id',
|
|
248
|
+
'ref',
|
|
249
|
+
'kind',
|
|
250
|
+
'artifact_schema_version',
|
|
251
|
+
'content_hash',
|
|
252
|
+
'created_at',
|
|
253
|
+
'row_count',
|
|
254
|
+
'evidence_status',
|
|
255
|
+
'finding_count',
|
|
256
|
+
])
|
|
257
|
+
for (const field of ['session_id', 'ref', 'kind', 'artifact_schema_version']) {
|
|
258
|
+
string(artifact[field], `${path}.artifact.${field}`)
|
|
259
|
+
}
|
|
260
|
+
string(artifact.content_hash, `${path}.artifact.content_hash`, { nullable: true })
|
|
261
|
+
timestamp(artifact.created_at, `${path}.artifact.created_at`)
|
|
262
|
+
integer(artifact.row_count, `${path}.artifact.row_count`)
|
|
263
|
+
integer(artifact.finding_count, `${path}.artifact.finding_count`)
|
|
264
|
+
if (!['complete', 'partial', 'unavailable'].includes(artifact.evidence_status)) {
|
|
265
|
+
fail(`${path}.artifact.evidence_status`, 'is unsupported')
|
|
266
|
+
}
|
|
267
|
+
validateQuality(source.quality_summary, `${path}.quality_summary`)
|
|
268
|
+
if (!Array.isArray(source.issues) || source.issues.length > 100) {
|
|
269
|
+
fail(`${path}.issues`, 'must contain at most 100 issues')
|
|
270
|
+
}
|
|
271
|
+
source.issues.forEach((issue, index) => {
|
|
272
|
+
validateIssue(issue, `${path}.issues[${index}]`)
|
|
273
|
+
})
|
|
274
|
+
integer(source.issues_omitted, `${path}.issues_omitted`)
|
|
275
|
+
const lineage = object(source.lineage, `${path}.lineage`)
|
|
276
|
+
exactKeys(lineage, `${path}.lineage`, [
|
|
277
|
+
'external_inputs',
|
|
278
|
+
'external_inputs_omitted',
|
|
279
|
+
'steps',
|
|
280
|
+
'steps_omitted',
|
|
281
|
+
])
|
|
282
|
+
stringArray(lineage.external_inputs, `${path}.lineage.external_inputs`)
|
|
283
|
+
integer(lineage.external_inputs_omitted, `${path}.lineage.external_inputs_omitted`)
|
|
284
|
+
if (!Array.isArray(lineage.steps) || lineage.steps.length > 100) {
|
|
285
|
+
fail(`${path}.lineage.steps`, 'must contain at most 100 steps')
|
|
286
|
+
}
|
|
287
|
+
lineage.steps.forEach((candidate, index) => {
|
|
288
|
+
const stepPath = `${path}.lineage.steps[${index}]`
|
|
289
|
+
const step = object(candidate, stepPath)
|
|
290
|
+
exactKeys(step, stepPath, [
|
|
291
|
+
'intent',
|
|
292
|
+
'job_ref',
|
|
293
|
+
'inputs',
|
|
294
|
+
'params_digest',
|
|
295
|
+
'analysis_purpose',
|
|
296
|
+
])
|
|
297
|
+
string(step.intent, `${stepPath}.intent`)
|
|
298
|
+
string(step.job_ref, `${stepPath}.job_ref`)
|
|
299
|
+
stringArray(step.inputs, `${stepPath}.inputs`)
|
|
300
|
+
string(step.params_digest, `${stepPath}.params_digest`)
|
|
301
|
+
string(step.analysis_purpose, `${stepPath}.analysis_purpose`, { nullable: true })
|
|
302
|
+
})
|
|
303
|
+
integer(lineage.steps_omitted, `${path}.lineage.steps_omitted`)
|
|
304
|
+
validateRevalidation(source.revalidation, `${path}.revalidation`)
|
|
305
|
+
if (!Object.hasOwn(table, 'semantic_shape')) {
|
|
306
|
+
fail('$.table.semantic_shape', 'is required for an Artifact dataset')
|
|
307
|
+
}
|
|
308
|
+
string(table.semantic_shape, '$.table.semantic_shape', { nullable: true })
|
|
309
|
+
if (artifact.row_count !== table.total_rows) {
|
|
310
|
+
fail(`${path}.artifact.row_count`, 'must equal $.table.total_rows')
|
|
311
|
+
}
|
|
312
|
+
return 'marivo_artifact'
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function validateDataset(value, registrationId) {
|
|
316
|
+
const dataset = object(value, '$')
|
|
317
|
+
exactKeys(dataset, '$', ['schema', 'dataset_id', 'emitted_at', 'source', 'table'])
|
|
318
|
+
if (dataset.schema !== 'dsh-data-analysis-dataset/v2') fail('$.schema', 'is unsupported')
|
|
319
|
+
if (typeof dataset.dataset_id !== 'string' || !DATASET_ID.test(dataset.dataset_id)) {
|
|
320
|
+
fail('$.dataset_id', 'is invalid')
|
|
321
|
+
}
|
|
322
|
+
if (dataset.dataset_id !== registrationId) {
|
|
323
|
+
fail('$.dataset_id', 'must match the registered id', 'dataset-id-mismatch')
|
|
324
|
+
}
|
|
325
|
+
timestamp(dataset.emitted_at, '$.emitted_at')
|
|
326
|
+
const table = object(dataset.table, '$.table')
|
|
327
|
+
exactKeys(
|
|
328
|
+
table,
|
|
329
|
+
'$.table',
|
|
330
|
+
['total_rows', 'written_rows', 'omitted_rows', 'columns', 'rows'],
|
|
331
|
+
['semantic_shape'],
|
|
332
|
+
)
|
|
333
|
+
integer(table.total_rows, '$.table.total_rows')
|
|
334
|
+
integer(table.written_rows, '$.table.written_rows', { maximum: 100000 })
|
|
335
|
+
integer(table.omitted_rows, '$.table.omitted_rows')
|
|
336
|
+
if (table.total_rows !== table.written_rows + table.omitted_rows) {
|
|
337
|
+
fail('$.table.total_rows', 'must equal written_rows + omitted_rows')
|
|
338
|
+
}
|
|
339
|
+
if (!Array.isArray(table.columns) || table.columns.length > 100) {
|
|
340
|
+
fail('$.table.columns', 'must contain at most 100 columns')
|
|
341
|
+
}
|
|
342
|
+
const sourceKind = validateSource(dataset.source, '$.source', table)
|
|
343
|
+
const names = new Set()
|
|
344
|
+
table.columns.forEach((candidate, index) => {
|
|
345
|
+
const columnPath = `$.table.columns[${index}]`
|
|
346
|
+
const column = object(candidate, columnPath)
|
|
347
|
+
const computedKeys = ['name', 'dtype', 'contains_null']
|
|
348
|
+
const artifactKeys = [...computedKeys, 'artifact_dtype', 'nullable', 'role']
|
|
349
|
+
exactKeys(column, columnPath, sourceKind === 'computed' ? computedKeys : artifactKeys)
|
|
350
|
+
string(column.name, `${columnPath}.name`)
|
|
351
|
+
string(column.dtype, `${columnPath}.dtype`)
|
|
352
|
+
if (names.has(column.name)) fail(`${columnPath}.name`, 'must be unique')
|
|
353
|
+
names.add(column.name)
|
|
354
|
+
if (typeof column.contains_null !== 'boolean')
|
|
355
|
+
fail(`${columnPath}.contains_null`, 'must be boolean')
|
|
356
|
+
if (sourceKind === 'marivo_artifact') {
|
|
357
|
+
string(column.artifact_dtype, `${columnPath}.artifact_dtype`)
|
|
358
|
+
if (typeof column.nullable !== 'boolean') fail(`${columnPath}.nullable`, 'must be boolean')
|
|
359
|
+
if (!ARTIFACT_ROLES.has(column.role)) fail(`${columnPath}.role`, 'is unsupported')
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
if (!Array.isArray(table.rows) || table.rows.length > 100000) {
|
|
363
|
+
fail('$.table.rows', 'must contain at most 100000 rows')
|
|
364
|
+
}
|
|
365
|
+
if (table.rows.length !== table.written_rows) {
|
|
366
|
+
fail('$.table.written_rows', 'must equal rows.length')
|
|
367
|
+
}
|
|
368
|
+
table.rows.forEach((row, rowIndex) => {
|
|
369
|
+
const rowPath = `$.table.rows[${rowIndex}]`
|
|
370
|
+
if (!Array.isArray(row) || row.length !== table.columns.length) {
|
|
371
|
+
fail(rowPath, `must contain exactly ${table.columns.length} cells`)
|
|
372
|
+
}
|
|
373
|
+
row.forEach((cell, cellIndex) => {
|
|
374
|
+
if (!['string', 'number', 'boolean'].includes(typeof cell) && cell !== null) {
|
|
375
|
+
fail(`${rowPath}[${cellIndex}]`, 'must be a JSON scalar')
|
|
376
|
+
}
|
|
377
|
+
if (typeof cell === 'number' && !Number.isFinite(cell)) {
|
|
378
|
+
fail(`${rowPath}[${cellIndex}]`, 'must be finite')
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
return dataset
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function deepFreeze(value, seen = new WeakSet()) {
|
|
386
|
+
if (!isObject(value) && !Array.isArray(value)) return value
|
|
387
|
+
if (seen.has(value)) return value
|
|
388
|
+
seen.add(value)
|
|
389
|
+
for (const child of Object.values(value)) deepFreeze(child, seen)
|
|
390
|
+
return Object.freeze(value)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function validateId(id) {
|
|
394
|
+
if (typeof id !== 'string' || !DATASET_ID.test(id)) {
|
|
395
|
+
fail('$id', 'is invalid', 'dataset-id-invalid')
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function register(id, dataset) {
|
|
400
|
+
validateId(id)
|
|
401
|
+
if (datasets.has(id)) fail('$id', 'is already registered', 'dataset-id-duplicate')
|
|
402
|
+
datasets.set(id, deepFreeze(validateDataset(dataset, id)))
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function get(id) {
|
|
406
|
+
validateId(id)
|
|
407
|
+
const dataset = datasets.get(id)
|
|
408
|
+
if (!dataset) fail('$id', 'is not registered', 'dataset-not-found')
|
|
409
|
+
return dataset
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function records(id) {
|
|
413
|
+
if (recordViews.has(id)) return recordViews.get(id)
|
|
414
|
+
const dataset = get(id)
|
|
415
|
+
const names = dataset.table.columns.map((column) => column.name)
|
|
416
|
+
const recordsForId = dataset.table.rows.map((row) => {
|
|
417
|
+
const record = {}
|
|
418
|
+
names.forEach((name, index) => {
|
|
419
|
+
Object.defineProperty(record, name, {
|
|
420
|
+
configurable: false,
|
|
421
|
+
enumerable: true,
|
|
422
|
+
value: row[index],
|
|
423
|
+
writable: false,
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
return Object.freeze(record)
|
|
427
|
+
})
|
|
428
|
+
const frozen = Object.freeze(recordsForId)
|
|
429
|
+
recordViews.set(id, frozen)
|
|
430
|
+
return frozen
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function has(id) {
|
|
434
|
+
validateId(id)
|
|
435
|
+
return datasets.has(id)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function list() {
|
|
439
|
+
return Object.freeze([...datasets.keys()])
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const api = Object.freeze({ get, has, list, records, register })
|
|
443
|
+
Object.defineProperty(scope, 'ReportData', {
|
|
444
|
+
configurable: false,
|
|
445
|
+
enumerable: true,
|
|
446
|
+
value: api,
|
|
447
|
+
writable: false,
|
|
448
|
+
})
|
|
449
|
+
})(globalThis)
|