@chengxianglibra/dsh-data-analysis 0.1.1 → 0.1.2
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 +83 -34
- package/lib/bin/environment.js +1 -1
- package/lib/bin/presentation-lint.js +21 -0
- package/lib/bridges.js +2 -2
- package/lib/client/credentials/create-datasource.js +67 -0
- package/lib/client/credentials/install.js +191 -0
- package/lib/client/credentials/model.js +494 -0
- package/lib/client/credentials/styles.js +17 -0
- package/lib/client/workspace-header-action.js +11 -0
- package/lib/client.js +1133 -736
- package/lib/compatibility.js +1 -1
- package/lib/datasource/authoring.js +70 -0
- package/lib/datasource/bridge-programs.js +30 -18
- package/lib/datasource/bridge.js +100 -8
- package/lib/datasource/index.js +3 -2
- package/lib/datasource/python.js +115 -0
- package/lib/datasource/resolver-program.js +112 -0
- package/lib/datasource/rpc.js +88 -0
- package/lib/datasource/service.js +687 -0
- package/lib/datasource/shell-env.js +2 -333
- package/lib/datasource/test.js +24 -51
- package/lib/environment/binding.js +54 -9
- package/lib/environment/runtime.js +89 -78
- package/lib/environment/subprocess.js +6 -1
- package/lib/environment/summary.js +3 -3
- package/lib/environment/workspace.js +5 -0
- package/lib/index.js +1 -1
- package/lib/plugin.js +168 -51
- package/lib/presentation/assets/portable.js +606 -0
- package/lib/presentation/assets/static.js +945 -0
- package/lib/presentation/build/index.js +71 -0
- package/lib/presentation/commit.js +156 -0
- package/lib/presentation/contracts/charts.js +476 -0
- package/lib/presentation/contracts/editing.js +58 -0
- package/lib/presentation/contracts/index.js +651 -0
- package/lib/presentation/contracts/interaction.js +62 -0
- package/lib/presentation/contracts/types.js +15 -0
- package/lib/presentation/contracts/values.js +28 -0
- package/lib/presentation/delivery.js +56 -0
- package/lib/presentation/files.js +83 -0
- package/lib/presentation/index.js +6 -0
- package/lib/presentation/lint.js +46 -0
- package/lib/presentation/projection/files.js +85 -0
- package/lib/presentation/projection/index.js +195 -0
- package/lib/presentation/projection/program.js +144 -0
- package/lib/presentation/projection/sql-code.js +79 -0
- package/lib/presentation/receipt.js +34 -0
- package/lib/presentation/reports.js +122 -0
- package/lib/presentation/rpc.js +148 -0
- package/lib/presentation/tool.js +122 -0
- package/lib/python-execution-contracts.js +1 -0
- package/lib/python-execution.js +232 -0
- package/lib/semantic-browser/contracts.js +80 -0
- package/lib/semantic-browser/definition.js +64 -0
- package/lib/semantic-browser/program.js +119 -0
- package/lib/semantic-browser/service.js +72 -0
- package/lib/semantic-reference/bridge.js +118 -0
- package/lib/semantic-reference/contracts.js +113 -0
- package/lib/semantic-reference/index.js +3 -0
- package/lib/semantic-reference/rpc.js +122 -0
- package/lib/semantic-reference/search.js +94 -0
- package/lib/semantic-reference/usage.js +143 -0
- package/lib/types/bridges.d.ts +2 -2
- package/lib/types/client/credentials/create-datasource.d.ts +5 -0
- package/lib/types/client/credentials/install.d.ts +1 -0
- package/lib/types/client/credentials/model.d.ts +59 -0
- package/lib/types/client/credentials/styles.d.ts +1 -0
- package/lib/types/client/presentation/ask-dsh.d.ts +9 -0
- package/lib/types/client/presentation/chart-explorer.d.ts +12 -0
- package/lib/types/client/presentation/chart-geometry.d.ts +30 -0
- package/lib/types/client/presentation/chart-renderer.d.ts +10 -0
- package/lib/types/client/presentation/chart-specials.d.ts +10 -0
- package/lib/types/client/presentation/chart-styles.d.ts +1 -0
- package/lib/types/client/presentation/chart-tooltip.d.ts +13 -0
- package/lib/types/client/presentation/chart-view.d.ts +23 -0
- package/lib/types/client/presentation/copy-context.d.ts +5 -0
- package/lib/types/client/presentation/delivery-model.d.ts +54 -0
- package/lib/types/client/presentation/delivery.d.ts +77 -0
- package/lib/types/client/presentation/editor-controls.d.ts +12 -0
- package/lib/types/client/presentation/global-filters.d.ts +6 -0
- package/lib/types/client/presentation/host-entry.d.ts +12 -0
- package/lib/types/client/presentation/icons.d.ts +4 -0
- package/lib/types/client/presentation/install.d.ts +15 -0
- package/lib/types/client/presentation/markdown.d.ts +5 -0
- package/lib/types/client/presentation/model.d.ts +62 -0
- package/lib/types/client/presentation/reader.d.ts +11 -0
- package/lib/types/client/presentation/source-code-model.d.ts +16 -0
- package/lib/types/client/presentation/source-code.d.ts +11 -0
- package/lib/types/client/presentation/source-dialog.d.ts +13 -0
- package/lib/types/client/presentation/source-facts.d.ts +20 -0
- package/lib/types/client/presentation/source-format.d.ts +5 -0
- package/lib/types/client/presentation/source-highlight.d.ts +7 -0
- package/lib/types/client/presentation/sources.d.ts +13 -0
- package/lib/types/client/presentation/styles.d.ts +1 -0
- package/lib/types/client/presentation/table.d.ts +12 -0
- package/lib/types/client/presentation/use-element-width.d.ts +5 -0
- package/lib/types/client/workspace-header-action.d.ts +7 -0
- package/lib/types/client.d.ts +4 -122
- package/lib/types/compatibility.d.ts +2 -2
- package/lib/types/datasource/authoring.d.ts +25 -0
- package/lib/types/datasource/bridge.d.ts +21 -3
- package/lib/types/datasource/index.d.ts +3 -5
- package/lib/types/datasource/python.d.ts +4 -0
- package/lib/types/datasource/resolver-program.d.ts +6 -0
- package/lib/types/datasource/rpc.d.ts +4 -0
- package/lib/types/datasource/service.d.ts +112 -0
- package/lib/types/datasource/shell-env.d.ts +0 -29
- package/lib/types/datasource/test.d.ts +6 -29
- package/lib/types/environment/binding.d.ts +5 -1
- package/lib/types/environment/errors.d.ts +1 -1
- package/lib/types/environment/runtime.d.ts +5 -5
- package/lib/types/environment/types.d.ts +13 -2
- package/lib/types/index.d.ts +1 -1
- package/lib/types/plugin.d.ts +11 -5
- package/lib/types/presentation/build/index.d.ts +11 -0
- package/lib/types/presentation/commit.d.ts +4 -0
- package/lib/types/presentation/contracts/charts.d.ts +13 -0
- package/lib/types/presentation/contracts/editing.d.ts +8 -0
- package/lib/types/presentation/contracts/index.d.ts +11 -0
- package/lib/types/presentation/contracts/interaction.d.ts +8 -0
- package/lib/types/presentation/contracts/types.d.ts +252 -0
- package/lib/types/presentation/contracts/values.d.ts +9 -0
- package/lib/types/presentation/delivery.d.ts +3 -0
- package/lib/types/presentation/files.d.ts +7 -0
- package/lib/types/presentation/index.d.ts +6 -0
- package/lib/types/presentation/lint.d.ts +13 -0
- package/lib/types/presentation/projection/files.d.ts +2 -0
- package/lib/types/presentation/projection/index.d.ts +20 -0
- package/lib/types/presentation/projection/program.d.ts +2 -0
- package/lib/types/presentation/projection/sql-code.d.ts +2 -0
- package/lib/types/presentation/receipt.d.ts +14 -0
- package/lib/types/presentation/reports.d.ts +5 -0
- package/lib/types/presentation/rpc.d.ts +24 -0
- package/lib/types/presentation/tool.d.ts +12 -0
- package/lib/types/python-execution-contracts.d.ts +10 -0
- package/lib/types/python-execution.d.ts +10 -0
- package/lib/types/semantic-browser/contracts.d.ts +40 -0
- package/lib/types/semantic-browser/definition.d.ts +20 -0
- package/lib/types/semantic-browser/program.d.ts +2 -0
- package/lib/types/semantic-browser/service.d.ts +20 -0
- package/lib/types/semantic-reference/bridge.d.ts +11 -0
- package/lib/types/semantic-reference/contracts.d.ts +51 -0
- package/lib/types/semantic-reference/index.d.ts +3 -0
- package/lib/types/semantic-reference/rpc.d.ts +15 -0
- package/lib/types/semantic-reference/search.d.ts +10 -0
- package/lib/types/semantic-reference/usage.d.ts +43 -0
- package/package.json +43 -19
- package/python/presentation-kit/dist/dsh_data_analysis_presentation_kit-1.1.0-py3-none-any.whl +0 -0
- package/skills/dsh-data-analysis-presentation/SKILL.md +24 -0
- package/skills/dsh-data-analysis-presentation/references/chart-examples.md +18 -0
- package/skills/dsh-data-analysis-presentation/references/charts.md +55 -0
- package/skills/dsh-data-analysis-presentation/references/examples/artifact.draft.json +27 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-bins.dataset.json +33 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-boxes.dataset.json +45 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-composition.dataset.json +33 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-funnel.dataset.json +27 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-precision.dataset.json +21 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-series.dataset.json +58 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-shares.dataset.json +33 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts-waterfall.dataset.json +44 -0
- package/skills/dsh-data-analysis-presentation/references/examples/charts.draft.json +392 -0
- package/skills/dsh-data-analysis-presentation/references/examples/comparison-selected.dataset.json +24 -0
- package/skills/dsh-data-analysis-presentation/references/examples/comparison-summary.dataset.json +22 -0
- package/skills/dsh-data-analysis-presentation/references/examples/comparison.dataset.json +18 -0
- package/skills/dsh-data-analysis-presentation/references/examples/comparison.draft.json +79 -0
- package/skills/dsh-data-analysis-presentation/references/examples/computed-sources.draft.json +35 -0
- package/skills/dsh-data-analysis-presentation/references/examples/computed.dataset.json +12 -0
- package/skills/dsh-data-analysis-presentation/references/examples/computed.draft.json +57 -0
- package/skills/dsh-data-analysis-presentation/references/examples/interaction-detail.dataset.json +30 -0
- package/skills/dsh-data-analysis-presentation/references/examples/interaction-summary.dataset.json +22 -0
- package/skills/dsh-data-analysis-presentation/references/examples/interaction.draft.json +278 -0
- package/skills/dsh-data-analysis-presentation/references/examples/source-only.draft.json +19 -0
- package/skills/dsh-data-analysis-presentation/references/examples/write-charts.py +73 -0
- package/skills/dsh-data-analysis-presentation/references/examples/write-comparison.py +42 -0
- package/skills/dsh-data-analysis-presentation/references/examples/write-computed.py +21 -0
- package/skills/dsh-data-analysis-presentation/references/examples/write-interaction.py +33 -0
- package/skills/dsh-data-analysis-presentation/references/examples.md +42 -0
- package/skills/dsh-data-analysis-presentation/references/interaction.md +56 -0
- package/skills/dsh-data-analysis-presentation/references/narrative.md +37 -0
- package/skills/dsh-data-analysis-presentation/references/schema.md +138 -0
- package/lib/datasource/access.js +0 -60
- package/lib/datasource/credentials.js +0 -24
- package/lib/evidence/bridge-program.js +0 -153
- package/lib/evidence/bridge.js +0 -166
- package/lib/evidence/index.js +0 -2
- package/lib/evidence/sources.js +0 -263
- package/lib/types/datasource/access.d.ts +0 -27
- package/lib/types/evidence/bridge.d.ts +0 -40
- package/lib/types/evidence/index.d.ts +0 -4
- package/lib/types/evidence/sources.d.ts +0 -57
- 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 +0 -57
- package/skills/dsh-data-analysis-report/assets/marivo-artifact.js +0 -113
- package/skills/dsh-data-analysis-report/assets/marivo-session-dag.js +0 -1201
- package/skills/dsh-data-analysis-report/assets/report-data.js +0 -449
|
@@ -1,1201 +0,0 @@
|
|
|
1
|
-
;((scope) => {
|
|
2
|
-
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'
|
|
3
|
-
const TRACE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
|
|
4
|
-
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/
|
|
5
|
-
const QUERY_SQL_MAX_BYTES = 64 * 1024
|
|
6
|
-
const traces = new Map()
|
|
7
|
-
let nextId = 0
|
|
8
|
-
|
|
9
|
-
class ReportTraceError extends Error {
|
|
10
|
-
constructor(code, path, message) {
|
|
11
|
-
super(`${path}: ${message}`)
|
|
12
|
-
this.name = 'ReportTraceError'
|
|
13
|
-
this.code = code
|
|
14
|
-
this.path = path
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function fail(path, message, code = 'trace-invalid') {
|
|
19
|
-
throw new ReportTraceError(code, path, message)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function isObject(value) {
|
|
23
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function object(value, path) {
|
|
27
|
-
if (!isObject(value)) fail(path, 'must be an object')
|
|
28
|
-
return value
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function exactKeys(value, path, required) {
|
|
32
|
-
const allowed = new Set(required)
|
|
33
|
-
for (const key of required) {
|
|
34
|
-
if (!Object.hasOwn(value, key)) fail(`${path}.${key}`, 'is required')
|
|
35
|
-
}
|
|
36
|
-
for (const key of Object.keys(value)) {
|
|
37
|
-
if (!allowed.has(key)) fail(`${path}.${key}`, 'is not allowed')
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function string(value, path, nullable = false) {
|
|
42
|
-
if (nullable && value === null) return
|
|
43
|
-
if (typeof value !== 'string' || value.length === 0 || value.length > 2048) {
|
|
44
|
-
fail(path, 'must be a non-empty bounded string')
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function timestamp(value, path) {
|
|
49
|
-
if (
|
|
50
|
-
typeof value !== 'string' ||
|
|
51
|
-
!TIMESTAMP.test(value) ||
|
|
52
|
-
!Number.isFinite(Date.parse(value))
|
|
53
|
-
) {
|
|
54
|
-
fail(path, 'must be an RFC 3339 timestamp')
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function querySql(value, path) {
|
|
59
|
-
if (
|
|
60
|
-
typeof value !== 'string' ||
|
|
61
|
-
value.length === 0 ||
|
|
62
|
-
new TextEncoder().encode(value).length > QUERY_SQL_MAX_BYTES
|
|
63
|
-
) {
|
|
64
|
-
fail(path, `must be a non-empty UTF-8 string of at most ${QUERY_SQL_MAX_BYTES} bytes`)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function integer(value, path, nullable = false) {
|
|
69
|
-
if (nullable && value === null) return
|
|
70
|
-
if (!Number.isSafeInteger(value) || value < 0) fail(path, 'must be a non-negative safe integer')
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function number(value, path) {
|
|
74
|
-
if (value !== null && (typeof value !== 'number' || !Number.isFinite(value))) {
|
|
75
|
-
fail(path, 'must be null or a finite number')
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function identityArray(value, path, maximum = Number.MAX_SAFE_INTEGER) {
|
|
80
|
-
if (!Array.isArray(value) || value.length > maximum) {
|
|
81
|
-
fail(path, `must contain at most ${maximum} identities`)
|
|
82
|
-
}
|
|
83
|
-
const seen = new Set()
|
|
84
|
-
value.forEach((item, index) => {
|
|
85
|
-
string(item, `${path}[${index}]`)
|
|
86
|
-
if (seen.has(item)) fail(`${path}[${index}]`, 'must be unique')
|
|
87
|
-
seen.add(item)
|
|
88
|
-
})
|
|
89
|
-
return seen
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function validateQuality(value, path) {
|
|
93
|
-
if (value === null) return
|
|
94
|
-
const quality = object(value, path)
|
|
95
|
-
const fields = [
|
|
96
|
-
'coverage',
|
|
97
|
-
'null_rate',
|
|
98
|
-
'sample_size',
|
|
99
|
-
'sample_coverage_min',
|
|
100
|
-
'sample_coverage_avg',
|
|
101
|
-
'sample_coverage_partial_buckets',
|
|
102
|
-
'zero_denominator_rows',
|
|
103
|
-
'evaluated_check_count',
|
|
104
|
-
'failed_check_count',
|
|
105
|
-
'warning_check_count',
|
|
106
|
-
]
|
|
107
|
-
exactKeys(quality, path, fields)
|
|
108
|
-
for (const field of ['coverage', 'null_rate', 'sample_coverage_min', 'sample_coverage_avg']) {
|
|
109
|
-
number(quality[field], `${path}.${field}`)
|
|
110
|
-
}
|
|
111
|
-
for (const field of fields.filter(
|
|
112
|
-
(candidate) =>
|
|
113
|
-
!['coverage', 'null_rate', 'sample_coverage_min', 'sample_coverage_avg'].includes(
|
|
114
|
-
candidate,
|
|
115
|
-
),
|
|
116
|
-
)) {
|
|
117
|
-
integer(quality[field], `${path}.${field}`, true)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function validateArtifact(value, path) {
|
|
122
|
-
const artifact = object(value, path)
|
|
123
|
-
exactKeys(artifact, path, [
|
|
124
|
-
'ref',
|
|
125
|
-
'family',
|
|
126
|
-
'semantic_shape',
|
|
127
|
-
'created_at',
|
|
128
|
-
'produced_by_run',
|
|
129
|
-
'analysis_purpose',
|
|
130
|
-
'row_count',
|
|
131
|
-
'materialization',
|
|
132
|
-
'evidence',
|
|
133
|
-
'quality',
|
|
134
|
-
'issue_counts',
|
|
135
|
-
])
|
|
136
|
-
string(artifact.ref, `${path}.ref`)
|
|
137
|
-
string(artifact.family, `${path}.family`)
|
|
138
|
-
string(artifact.semantic_shape, `${path}.semantic_shape`, true)
|
|
139
|
-
timestamp(artifact.created_at, `${path}.created_at`)
|
|
140
|
-
string(artifact.produced_by_run, `${path}.produced_by_run`, true)
|
|
141
|
-
string(artifact.analysis_purpose, `${path}.analysis_purpose`, true)
|
|
142
|
-
integer(artifact.row_count, `${path}.row_count`)
|
|
143
|
-
string(artifact.materialization, `${path}.materialization`)
|
|
144
|
-
const evidence = object(artifact.evidence, `${path}.evidence`)
|
|
145
|
-
exactKeys(evidence, `${path}.evidence`, [
|
|
146
|
-
'status',
|
|
147
|
-
'finding_count',
|
|
148
|
-
'digest_present',
|
|
149
|
-
'digest_item_count',
|
|
150
|
-
'omitted_item_count',
|
|
151
|
-
])
|
|
152
|
-
string(evidence.status, `${path}.evidence.status`)
|
|
153
|
-
integer(evidence.finding_count, `${path}.evidence.finding_count`)
|
|
154
|
-
if (typeof evidence.digest_present !== 'boolean') {
|
|
155
|
-
fail(`${path}.evidence.digest_present`, 'must be boolean')
|
|
156
|
-
}
|
|
157
|
-
integer(evidence.digest_item_count, `${path}.evidence.digest_item_count`)
|
|
158
|
-
integer(evidence.omitted_item_count, `${path}.evidence.omitted_item_count`)
|
|
159
|
-
validateQuality(artifact.quality, `${path}.quality`)
|
|
160
|
-
const issueCounts = object(artifact.issue_counts, `${path}.issue_counts`)
|
|
161
|
-
exactKeys(issueCounts, `${path}.issue_counts`, ['warning', 'blocking'])
|
|
162
|
-
integer(issueCounts.warning, `${path}.issue_counts.warning`)
|
|
163
|
-
integer(issueCounts.blocking, `${path}.issue_counts.blocking`)
|
|
164
|
-
return artifact
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function validateQueries(value, path, detail, omitted) {
|
|
168
|
-
if (!Array.isArray(value) || value.length > 100) fail(path, 'must contain at most 100 queries')
|
|
169
|
-
integer(omitted, `${path}_omitted`)
|
|
170
|
-
if (detail === 'reader' && value.length !== 0) fail(path, 'must be omitted in reader detail')
|
|
171
|
-
value.forEach((candidate, index) => {
|
|
172
|
-
const queryPath = `${path}[${index}]`
|
|
173
|
-
const query = object(candidate, queryPath)
|
|
174
|
-
const fields = [
|
|
175
|
-
'query_id',
|
|
176
|
-
'datasource',
|
|
177
|
-
'dialect',
|
|
178
|
-
'sql',
|
|
179
|
-
'sql_digest',
|
|
180
|
-
'row_count',
|
|
181
|
-
'duration_ms',
|
|
182
|
-
'started_at',
|
|
183
|
-
'finished_at',
|
|
184
|
-
'status',
|
|
185
|
-
]
|
|
186
|
-
exactKeys(query, queryPath, fields)
|
|
187
|
-
for (const field of ['query_id', 'datasource', 'dialect', 'sql_digest', 'status']) {
|
|
188
|
-
string(query[field], `${queryPath}.${field}`)
|
|
189
|
-
}
|
|
190
|
-
querySql(query.sql, `${queryPath}.sql`)
|
|
191
|
-
integer(query.row_count, `${queryPath}.row_count`)
|
|
192
|
-
integer(query.duration_ms, `${queryPath}.duration_ms`)
|
|
193
|
-
timestamp(query.started_at, `${queryPath}.started_at`)
|
|
194
|
-
timestamp(query.finished_at, `${queryPath}.finished_at`)
|
|
195
|
-
})
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function validateRun(value, path, detail) {
|
|
199
|
-
const run = object(value, path)
|
|
200
|
-
string(run.lifecycle, `${path}.lifecycle`)
|
|
201
|
-
const common = [
|
|
202
|
-
'run_id',
|
|
203
|
-
'lifecycle',
|
|
204
|
-
'capability_id',
|
|
205
|
-
'analysis_purpose',
|
|
206
|
-
'input_artifact_refs',
|
|
207
|
-
'started_at',
|
|
208
|
-
]
|
|
209
|
-
if (run.lifecycle === 'incomplete') exactKeys(run, path, common)
|
|
210
|
-
else if (run.lifecycle === 'succeeded') {
|
|
211
|
-
exactKeys(run, path, [
|
|
212
|
-
...common,
|
|
213
|
-
'finished_at',
|
|
214
|
-
'output_artifact_ref',
|
|
215
|
-
'output_mode',
|
|
216
|
-
'queries',
|
|
217
|
-
'queries_omitted',
|
|
218
|
-
])
|
|
219
|
-
} else if (run.lifecycle === 'failed')
|
|
220
|
-
exactKeys(run, path, [...common, 'failed_at', 'failure', 'queries', 'queries_omitted'])
|
|
221
|
-
else fail(`${path}.lifecycle`, 'is unsupported')
|
|
222
|
-
string(run.run_id, `${path}.run_id`)
|
|
223
|
-
string(run.capability_id, `${path}.capability_id`)
|
|
224
|
-
string(run.analysis_purpose, `${path}.analysis_purpose`, true)
|
|
225
|
-
identityArray(run.input_artifact_refs, `${path}.input_artifact_refs`)
|
|
226
|
-
timestamp(run.started_at, `${path}.started_at`)
|
|
227
|
-
if (run.lifecycle === 'succeeded') {
|
|
228
|
-
timestamp(run.finished_at, `${path}.finished_at`)
|
|
229
|
-
string(run.output_artifact_ref, `${path}.output_artifact_ref`)
|
|
230
|
-
if (!['produced', 'reused'].includes(run.output_mode))
|
|
231
|
-
fail(`${path}.output_mode`, 'is unsupported')
|
|
232
|
-
validateQueries(run.queries, `${path}.queries`, detail, run.queries_omitted)
|
|
233
|
-
}
|
|
234
|
-
if (run.lifecycle === 'failed') {
|
|
235
|
-
timestamp(run.failed_at, `${path}.failed_at`)
|
|
236
|
-
const failure = object(run.failure, `${path}.failure`)
|
|
237
|
-
exactKeys(failure, `${path}.failure`, ['error_type', 'location'])
|
|
238
|
-
string(failure.error_type, `${path}.failure.error_type`)
|
|
239
|
-
string(failure.location, `${path}.failure.location`, true)
|
|
240
|
-
validateQueries(run.queries, `${path}.queries`, detail, run.queries_omitted)
|
|
241
|
-
}
|
|
242
|
-
return run
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function validateTrace(value, registrationId) {
|
|
246
|
-
const trace = object(value, '$')
|
|
247
|
-
exactKeys(trace, '$', [
|
|
248
|
-
'schema',
|
|
249
|
-
'detail',
|
|
250
|
-
'trace_id',
|
|
251
|
-
'emitted_at',
|
|
252
|
-
'session_id',
|
|
253
|
-
'report_artifact_refs',
|
|
254
|
-
'artifacts',
|
|
255
|
-
'runs',
|
|
256
|
-
'edges',
|
|
257
|
-
'root_run_ids',
|
|
258
|
-
'head_artifact_refs',
|
|
259
|
-
'failed_run_ids',
|
|
260
|
-
'incomplete_run_ids',
|
|
261
|
-
'boundary_artifact_refs',
|
|
262
|
-
'boundary_run_ids',
|
|
263
|
-
'truncated',
|
|
264
|
-
'projection',
|
|
265
|
-
'read_boundaries',
|
|
266
|
-
])
|
|
267
|
-
if (trace.schema !== 'dsh-data-analysis-session-trace/v2') fail('$.schema', 'is unsupported')
|
|
268
|
-
if (!['reader', 'audit'].includes(trace.detail)) fail('$.detail', 'is unsupported')
|
|
269
|
-
if (typeof trace.trace_id !== 'string' || !TRACE_ID.test(trace.trace_id))
|
|
270
|
-
fail('$.trace_id', 'is invalid')
|
|
271
|
-
if (trace.trace_id !== registrationId) {
|
|
272
|
-
fail('$.trace_id', 'must match the registered id', 'trace-id-mismatch')
|
|
273
|
-
}
|
|
274
|
-
timestamp(trace.emitted_at, '$.emitted_at')
|
|
275
|
-
string(trace.session_id, '$.session_id')
|
|
276
|
-
const reportRefs = identityArray(trace.report_artifact_refs, '$.report_artifact_refs', 20)
|
|
277
|
-
if (reportRefs.size === 0) fail('$.report_artifact_refs', 'must not be empty')
|
|
278
|
-
if (!Array.isArray(trace.artifacts) || trace.artifacts.length > 200) {
|
|
279
|
-
fail('$.artifacts', 'must contain at most 200 Artifacts')
|
|
280
|
-
}
|
|
281
|
-
if (!Array.isArray(trace.runs) || trace.runs.length > 200)
|
|
282
|
-
fail('$.runs', 'must contain at most 200 Runs')
|
|
283
|
-
if (trace.artifacts.length + trace.runs.length > 200)
|
|
284
|
-
fail('$', 'must contain at most 200 nodes')
|
|
285
|
-
const artifacts = trace.artifacts.map((artifact, index) =>
|
|
286
|
-
validateArtifact(artifact, `$.artifacts[${index}]`),
|
|
287
|
-
)
|
|
288
|
-
const runs = trace.runs.map((run, index) => validateRun(run, `$.runs[${index}]`, trace.detail))
|
|
289
|
-
const artifactSet = new Set(artifacts.map((artifact) => artifact.ref))
|
|
290
|
-
const runSet = new Set(runs.map((run) => run.run_id))
|
|
291
|
-
if (artifactSet.size !== artifacts.length) fail('$.artifacts', 'refs must be unique')
|
|
292
|
-
if (runSet.size !== runs.length) fail('$.runs', 'run_ids must be unique')
|
|
293
|
-
for (const ref of reportRefs)
|
|
294
|
-
if (!artifactSet.has(ref)) fail('$.report_artifact_refs', 'contains a dangling ref')
|
|
295
|
-
|
|
296
|
-
const roots = identityArray(trace.root_run_ids, '$.root_run_ids')
|
|
297
|
-
const heads = identityArray(trace.head_artifact_refs, '$.head_artifact_refs')
|
|
298
|
-
const failed = identityArray(trace.failed_run_ids, '$.failed_run_ids')
|
|
299
|
-
const incomplete = identityArray(trace.incomplete_run_ids, '$.incomplete_run_ids')
|
|
300
|
-
const boundaryArtifacts = identityArray(
|
|
301
|
-
trace.boundary_artifact_refs,
|
|
302
|
-
'$.boundary_artifact_refs',
|
|
303
|
-
)
|
|
304
|
-
const boundaryRuns = identityArray(trace.boundary_run_ids, '$.boundary_run_ids')
|
|
305
|
-
for (const id of [...roots, ...failed, ...incomplete, ...boundaryRuns]) {
|
|
306
|
-
if (!runSet.has(id)) fail('$', 'contains a dangling Run identity')
|
|
307
|
-
}
|
|
308
|
-
for (const ref of [...heads, ...boundaryArtifacts]) {
|
|
309
|
-
if (!artifactSet.has(ref)) fail('$', 'contains a dangling Artifact identity')
|
|
310
|
-
}
|
|
311
|
-
if (typeof trace.truncated !== 'boolean') fail('$.truncated', 'must be boolean')
|
|
312
|
-
if (!trace.truncated && (boundaryArtifacts.size > 0 || boundaryRuns.size > 0)) {
|
|
313
|
-
fail('$', 'boundary identities require truncated=true')
|
|
314
|
-
}
|
|
315
|
-
if (!Array.isArray(trace.edges) || trace.edges.length > 1000)
|
|
316
|
-
fail('$.edges', 'must contain at most 1000 edges')
|
|
317
|
-
const edgeKeys = new Set()
|
|
318
|
-
trace.edges.forEach((candidate, index) => {
|
|
319
|
-
const path = `$.edges[${index}]`
|
|
320
|
-
const edge = object(candidate, path)
|
|
321
|
-
exactKeys(edge, path, ['kind', 'run_id', 'artifact_ref'])
|
|
322
|
-
if (!['consumes', 'produces', 'reuses'].includes(edge.kind))
|
|
323
|
-
fail(`${path}.kind`, 'is unsupported')
|
|
324
|
-
string(edge.run_id, `${path}.run_id`)
|
|
325
|
-
string(edge.artifact_ref, `${path}.artifact_ref`)
|
|
326
|
-
if (!runSet.has(edge.run_id) || !artifactSet.has(edge.artifact_ref))
|
|
327
|
-
fail(path, 'contains a dangling identity')
|
|
328
|
-
const key = `${edge.kind}\u0000${edge.run_id}\u0000${edge.artifact_ref}`
|
|
329
|
-
if (edgeKeys.has(key)) fail(path, 'must be unique')
|
|
330
|
-
edgeKeys.add(key)
|
|
331
|
-
})
|
|
332
|
-
for (const run of runs) {
|
|
333
|
-
for (const ref of run.input_artifact_refs) {
|
|
334
|
-
if (!artifactSet.has(ref) && !boundaryRuns.has(run.run_id))
|
|
335
|
-
fail('$.runs', 'contains a dangling input ref')
|
|
336
|
-
}
|
|
337
|
-
if (run.lifecycle === 'succeeded') {
|
|
338
|
-
if (!artifactSet.has(run.output_artifact_ref) && !boundaryRuns.has(run.run_id)) {
|
|
339
|
-
fail('$.runs', 'contains a dangling output ref')
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
for (const artifact of artifacts) {
|
|
344
|
-
if (
|
|
345
|
-
artifact.produced_by_run !== null &&
|
|
346
|
-
!runSet.has(artifact.produced_by_run) &&
|
|
347
|
-
!boundaryArtifacts.has(artifact.ref)
|
|
348
|
-
) {
|
|
349
|
-
fail('$.artifacts', 'contains a dangling producer')
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const projection = object(trace.projection, '$.projection')
|
|
354
|
-
exactKeys(projection, '$.projection', ['run_arguments', 'failure_values', 'query_bind_values'])
|
|
355
|
-
if (
|
|
356
|
-
projection.run_arguments !== 'omitted' ||
|
|
357
|
-
projection.failure_values !== 'omitted' ||
|
|
358
|
-
projection.query_bind_values !== 'omitted'
|
|
359
|
-
) {
|
|
360
|
-
fail('$.projection', 'must omit private values')
|
|
361
|
-
}
|
|
362
|
-
if (
|
|
363
|
-
!Array.isArray(trace.read_boundaries) ||
|
|
364
|
-
trace.read_boundaries.length !== 3 ||
|
|
365
|
-
trace.read_boundaries[0] !== 'semantic_authority_not_checked' ||
|
|
366
|
-
trace.read_boundaries[1] !== 'datasource_freshness_not_checked' ||
|
|
367
|
-
trace.read_boundaries[2] !== 'report_entailment_not_checked'
|
|
368
|
-
) {
|
|
369
|
-
fail('$.read_boundaries', 'must contain the closed V1 boundary sequence')
|
|
370
|
-
}
|
|
371
|
-
return trace
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function deepFreeze(value, seen = new WeakSet()) {
|
|
375
|
-
if ((!isObject(value) && !Array.isArray(value)) || seen.has(value)) return value
|
|
376
|
-
seen.add(value)
|
|
377
|
-
for (const child of Object.values(value)) deepFreeze(child, seen)
|
|
378
|
-
return Object.freeze(value)
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function validateId(id) {
|
|
382
|
-
if (typeof id !== 'string' || !TRACE_ID.test(id)) fail('$id', 'is invalid', 'trace-id-invalid')
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function register(id, trace) {
|
|
386
|
-
validateId(id)
|
|
387
|
-
if (traces.has(id)) fail('$id', 'is already registered', 'trace-id-duplicate')
|
|
388
|
-
traces.set(id, deepFreeze(validateTrace(trace, id)))
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function get(id) {
|
|
392
|
-
validateId(id)
|
|
393
|
-
const trace = traces.get(id)
|
|
394
|
-
if (!trace) fail('$id', 'is not registered', 'trace-not-found')
|
|
395
|
-
return trace
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function has(id) {
|
|
399
|
-
validateId(id)
|
|
400
|
-
return traces.has(id)
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function list() {
|
|
404
|
-
return Object.freeze([...traces.keys()])
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function svgElement(tag, attributes = {}) {
|
|
408
|
-
const node = document.createElementNS(SVG_NAMESPACE, tag)
|
|
409
|
-
for (const [name, value] of Object.entries(attributes)) node.setAttribute(name, String(value))
|
|
410
|
-
return node
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function text(tag, value, className) {
|
|
414
|
-
const node = document.createElement(tag)
|
|
415
|
-
if (className) node.className = className
|
|
416
|
-
node.textContent = String(value)
|
|
417
|
-
return node
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function runLabel(run) {
|
|
421
|
-
return run.analysis_purpose || run.capability_id
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function artifactLabel(artifact) {
|
|
425
|
-
return artifact.family
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function compactLabel(value) {
|
|
429
|
-
return value.length <= 18 ? value : `${value.slice(0, 17)}…`
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function graphComponents(trace) {
|
|
433
|
-
const ordered = [
|
|
434
|
-
...trace.runs.map((run) => `run:${run.run_id}`),
|
|
435
|
-
...trace.artifacts.map((artifact) => `artifact:${artifact.ref}`),
|
|
436
|
-
]
|
|
437
|
-
const neighbors = new Map(ordered.map((node) => [node, new Set()]))
|
|
438
|
-
for (const edge of trace.edges) {
|
|
439
|
-
const run = `run:${edge.run_id}`
|
|
440
|
-
const artifact = `artifact:${edge.artifact_ref}`
|
|
441
|
-
neighbors.get(run).add(artifact)
|
|
442
|
-
neighbors.get(artifact).add(run)
|
|
443
|
-
}
|
|
444
|
-
const seen = new Set()
|
|
445
|
-
const components = []
|
|
446
|
-
for (const start of ordered) {
|
|
447
|
-
if (seen.has(start)) continue
|
|
448
|
-
const pending = [start]
|
|
449
|
-
const nodes = new Set()
|
|
450
|
-
while (pending.length > 0) {
|
|
451
|
-
const node = pending.shift()
|
|
452
|
-
if (seen.has(node)) continue
|
|
453
|
-
seen.add(node)
|
|
454
|
-
nodes.add(node)
|
|
455
|
-
for (const neighbor of neighbors.get(node)) if (!seen.has(neighbor)) pending.push(neighbor)
|
|
456
|
-
}
|
|
457
|
-
components.push({
|
|
458
|
-
nodes,
|
|
459
|
-
runs: trace.runs.filter((run) => nodes.has(`run:${run.run_id}`)),
|
|
460
|
-
artifacts: trace.artifacts.filter((artifact) => nodes.has(`artifact:${artifact.ref}`)),
|
|
461
|
-
edges: trace.edges.filter(
|
|
462
|
-
(edge) => nodes.has(`run:${edge.run_id}`) && nodes.has(`artifact:${edge.artifact_ref}`),
|
|
463
|
-
),
|
|
464
|
-
})
|
|
465
|
-
}
|
|
466
|
-
return components
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function positions(component) {
|
|
470
|
-
const ordered = [
|
|
471
|
-
...component.runs.map((run) => `run:${run.run_id}`),
|
|
472
|
-
...component.artifacts.map((artifact) => `artifact:${artifact.ref}`),
|
|
473
|
-
]
|
|
474
|
-
const predecessor = new Map(ordered.map((node) => [node, []]))
|
|
475
|
-
for (const edge of component.edges) {
|
|
476
|
-
const source =
|
|
477
|
-
edge.kind === 'consumes' ? `artifact:${edge.artifact_ref}` : `run:${edge.run_id}`
|
|
478
|
-
const target =
|
|
479
|
-
edge.kind === 'consumes' ? `run:${edge.run_id}` : `artifact:${edge.artifact_ref}`
|
|
480
|
-
predecessor.get(target).push(source)
|
|
481
|
-
}
|
|
482
|
-
const levels = new Map()
|
|
483
|
-
while (levels.size < ordered.length) {
|
|
484
|
-
let progressed = false
|
|
485
|
-
for (const node of ordered) {
|
|
486
|
-
if (levels.has(node)) continue
|
|
487
|
-
const parents = predecessor.get(node)
|
|
488
|
-
if (parents.every((parent) => levels.has(parent))) {
|
|
489
|
-
levels.set(
|
|
490
|
-
node,
|
|
491
|
-
parents.length === 0 ? 0 : Math.max(...parents.map((parent) => levels.get(parent))) + 1,
|
|
492
|
-
)
|
|
493
|
-
progressed = true
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
if (!progressed) fail('$.edges', 'cannot lay out a cyclic graph')
|
|
497
|
-
}
|
|
498
|
-
const byLevel = new Map()
|
|
499
|
-
for (const node of ordered) {
|
|
500
|
-
const level = levels.get(node)
|
|
501
|
-
const nodes = byLevel.get(level) ?? []
|
|
502
|
-
nodes.push(node)
|
|
503
|
-
byLevel.set(level, nodes)
|
|
504
|
-
}
|
|
505
|
-
const result = new Map()
|
|
506
|
-
for (const [level, nodes] of byLevel) {
|
|
507
|
-
nodes.forEach((node, index) => {
|
|
508
|
-
result.set(node, { x: 40 + level * 230, y: 40 + index * 126 })
|
|
509
|
-
})
|
|
510
|
-
}
|
|
511
|
-
return result
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function nodeGroup(kind, title, subtitle, position, attributes, live, activate) {
|
|
515
|
-
const group = svgElement('g', {
|
|
516
|
-
class:
|
|
517
|
-
attributes['data-boundary'] === 'true' ? 'trace-node trace-boundary-node' : 'trace-node',
|
|
518
|
-
'data-node-kind': kind,
|
|
519
|
-
role: 'button',
|
|
520
|
-
tabindex: 0,
|
|
521
|
-
transform: `translate(${position.x} ${position.y})`,
|
|
522
|
-
'aria-label': `${kind === 'run' ? '分析动作' : 'Frame'}:${title};${subtitle}`,
|
|
523
|
-
...attributes,
|
|
524
|
-
})
|
|
525
|
-
if (kind === 'run') group.append(svgElement('rect', { width: 170, height: 76, rx: 12 }))
|
|
526
|
-
else group.append(svgElement('path', { d: 'M18 0 H152 L170 38 L152 76 H18 L0 38 Z' }))
|
|
527
|
-
const heading = svgElement('text', { x: 85, y: 31, 'text-anchor': 'middle' })
|
|
528
|
-
heading.textContent = compactLabel(title)
|
|
529
|
-
const detail = svgElement('text', { x: 85, y: 53, 'text-anchor': 'middle' })
|
|
530
|
-
detail.textContent = compactLabel(subtitle)
|
|
531
|
-
group.append(heading, detail)
|
|
532
|
-
group.addEventListener('focus', () => {
|
|
533
|
-
live.textContent = `${kind === 'run' ? '分析动作' : 'Frame'}:${title};${subtitle}`
|
|
534
|
-
})
|
|
535
|
-
group.addEventListener('click', activate)
|
|
536
|
-
group.addEventListener('keydown', (event) => {
|
|
537
|
-
if (event.key === 'Enter' || event.key === ' ') activate()
|
|
538
|
-
})
|
|
539
|
-
return group
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function definitionList(entries, className = 'trace-detail-list') {
|
|
543
|
-
const list = document.createElement('dl')
|
|
544
|
-
list.className = className
|
|
545
|
-
for (const [label, value] of entries) {
|
|
546
|
-
list.append(text('dt', label), text('dd', value === null || value === '' ? '—' : value))
|
|
547
|
-
}
|
|
548
|
-
return list
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function elapsed(startedAt, finishedAt) {
|
|
552
|
-
if (!finishedAt) return '进行中'
|
|
553
|
-
return `${Math.max(0, Date.parse(finishedAt) - Date.parse(startedAt))} ms`
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
function matchingDataset(sessionId, artifactRef) {
|
|
557
|
-
const registry = scope.ReportData
|
|
558
|
-
if (!registry || typeof registry.list !== 'function') return null
|
|
559
|
-
for (const id of registry.list()) {
|
|
560
|
-
const dataset = registry.get(id)
|
|
561
|
-
if (
|
|
562
|
-
dataset.source.kind === 'marivo_artifact' &&
|
|
563
|
-
dataset.source.artifact.session_id === sessionId &&
|
|
564
|
-
dataset.source.artifact.ref === artifactRef
|
|
565
|
-
) {
|
|
566
|
-
return dataset
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
return null
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
function previewTable(dataset) {
|
|
573
|
-
const section = document.createElement('section')
|
|
574
|
-
section.className = 'trace-frame-preview'
|
|
575
|
-
section.append(text('h5', '数据预览'))
|
|
576
|
-
section.append(
|
|
577
|
-
text(
|
|
578
|
-
'p',
|
|
579
|
-
`显示 ${Math.min(10, dataset.table.rows.length)} / 快照 ${dataset.table.written_rows} / 总计 ${dataset.table.total_rows} / 省略 ${dataset.table.omitted_rows} 行`,
|
|
580
|
-
'trace-frame-count',
|
|
581
|
-
),
|
|
582
|
-
)
|
|
583
|
-
const wrap = document.createElement('div')
|
|
584
|
-
wrap.className = 'table-wrap trace-frame-table-wrap'
|
|
585
|
-
const table = document.createElement('table')
|
|
586
|
-
table.className = 'data-table trace-frame-table'
|
|
587
|
-
table.append(text('caption', 'Artifact dataset 有界预览', 'sr-only'))
|
|
588
|
-
const header = document.createElement('tr')
|
|
589
|
-
for (const column of dataset.table.columns) {
|
|
590
|
-
const cell = text('th', column.name)
|
|
591
|
-
cell.scope = 'col'
|
|
592
|
-
header.append(cell)
|
|
593
|
-
}
|
|
594
|
-
const head = document.createElement('thead')
|
|
595
|
-
head.append(header)
|
|
596
|
-
const body = document.createElement('tbody')
|
|
597
|
-
for (const row of dataset.table.rows.slice(0, 10)) {
|
|
598
|
-
const item = document.createElement('tr')
|
|
599
|
-
for (const value of row) item.append(text('td', value === null ? '—' : value))
|
|
600
|
-
body.append(item)
|
|
601
|
-
}
|
|
602
|
-
table.append(head, body)
|
|
603
|
-
wrap.append(table)
|
|
604
|
-
section.append(wrap)
|
|
605
|
-
return section
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
function sqlTokens(sql) {
|
|
609
|
-
const tokens = []
|
|
610
|
-
let index = 0
|
|
611
|
-
while (index < sql.length) {
|
|
612
|
-
const character = sql[index]
|
|
613
|
-
if (/\s/.test(character)) {
|
|
614
|
-
index += 1
|
|
615
|
-
continue
|
|
616
|
-
}
|
|
617
|
-
if (sql.startsWith('--', index)) {
|
|
618
|
-
const end = sql.indexOf('\n', index)
|
|
619
|
-
tokens.push({ kind: 'comment', text: sql.slice(index, end < 0 ? sql.length : end) })
|
|
620
|
-
index = end < 0 ? sql.length : end + 1
|
|
621
|
-
continue
|
|
622
|
-
}
|
|
623
|
-
if (sql.startsWith('/*', index)) {
|
|
624
|
-
const end = sql.indexOf('*/', index + 2)
|
|
625
|
-
const stop = end < 0 ? sql.length : end + 2
|
|
626
|
-
tokens.push({ kind: 'comment', text: sql.slice(index, stop) })
|
|
627
|
-
index = stop
|
|
628
|
-
continue
|
|
629
|
-
}
|
|
630
|
-
if (character === "'" || character === '"' || character === '`') {
|
|
631
|
-
const quote = character
|
|
632
|
-
let end = index + 1
|
|
633
|
-
while (end < sql.length) {
|
|
634
|
-
if (sql[end] === quote) {
|
|
635
|
-
if (sql[end + 1] === quote) {
|
|
636
|
-
end += 2
|
|
637
|
-
continue
|
|
638
|
-
}
|
|
639
|
-
end += 1
|
|
640
|
-
break
|
|
641
|
-
}
|
|
642
|
-
end += 1
|
|
643
|
-
}
|
|
644
|
-
tokens.push({ kind: 'quoted', text: sql.slice(index, end) })
|
|
645
|
-
index = end
|
|
646
|
-
continue
|
|
647
|
-
}
|
|
648
|
-
const word = sql.slice(index).match(/^[A-Za-z_][A-Za-z0-9_$]*/)?.[0]
|
|
649
|
-
if (word) {
|
|
650
|
-
tokens.push({ kind: 'word', text: word })
|
|
651
|
-
index += word.length
|
|
652
|
-
continue
|
|
653
|
-
}
|
|
654
|
-
const number = sql.slice(index).match(/^\d+(?:\.\d+)?(?:[Ee][+-]?\d+)?/)?.[0]
|
|
655
|
-
if (number) {
|
|
656
|
-
tokens.push({ kind: 'number', text: number })
|
|
657
|
-
index += number.length
|
|
658
|
-
continue
|
|
659
|
-
}
|
|
660
|
-
const operator = ['->>', '->', '<=', '>=', '<>', '!=', '||', '::'].find((value) =>
|
|
661
|
-
sql.startsWith(value, index),
|
|
662
|
-
)
|
|
663
|
-
if (operator) {
|
|
664
|
-
tokens.push({ kind: 'operator', text: operator })
|
|
665
|
-
index += operator.length
|
|
666
|
-
continue
|
|
667
|
-
}
|
|
668
|
-
tokens.push({
|
|
669
|
-
kind: ['(', ')', ',', '.', ';'].includes(character) ? 'punctuation' : 'operator',
|
|
670
|
-
text: character,
|
|
671
|
-
})
|
|
672
|
-
index += 1
|
|
673
|
-
}
|
|
674
|
-
return tokens
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function formatSql(sql) {
|
|
678
|
-
const tokens = sqlTokens(sql)
|
|
679
|
-
const lines = []
|
|
680
|
-
let line = ''
|
|
681
|
-
let indentation = 0
|
|
682
|
-
let continuation = 0
|
|
683
|
-
let previous = null
|
|
684
|
-
const parentheses = []
|
|
685
|
-
const scopes = [{ depth: 0, clause: null }]
|
|
686
|
-
const clauseKeywords = new Set([
|
|
687
|
-
'SELECT',
|
|
688
|
-
'FROM',
|
|
689
|
-
'WHERE',
|
|
690
|
-
'GROUP',
|
|
691
|
-
'HAVING',
|
|
692
|
-
'ORDER',
|
|
693
|
-
'LIMIT',
|
|
694
|
-
'OFFSET',
|
|
695
|
-
'FETCH',
|
|
696
|
-
'UNION',
|
|
697
|
-
'EXCEPT',
|
|
698
|
-
'INTERSECT',
|
|
699
|
-
'JOIN',
|
|
700
|
-
'ON',
|
|
701
|
-
])
|
|
702
|
-
const flush = (extra = 0) => {
|
|
703
|
-
if (line.trim()) lines.push(line.trimEnd())
|
|
704
|
-
line = ''
|
|
705
|
-
continuation = extra
|
|
706
|
-
previous = null
|
|
707
|
-
}
|
|
708
|
-
const write = (token) => {
|
|
709
|
-
if (!line) line = ' '.repeat(indentation + continuation)
|
|
710
|
-
const current = token.text
|
|
711
|
-
const prior = previous?.text
|
|
712
|
-
const keyword = previous?.kind === 'word' ? prior.toUpperCase() : ''
|
|
713
|
-
const spaceBefore =
|
|
714
|
-
previous !== null &&
|
|
715
|
-
![',', '.', ')', ';'].includes(current) &&
|
|
716
|
-
!['(', '.'].includes(prior) &&
|
|
717
|
-
!(
|
|
718
|
-
current === '(' &&
|
|
719
|
-
previous.kind === 'word' &&
|
|
720
|
-
!['IN', 'EXISTS', 'FROM', 'JOIN', 'ON', 'WHERE'].includes(keyword)
|
|
721
|
-
)
|
|
722
|
-
if (spaceBefore && !line.endsWith(' ')) line += ' '
|
|
723
|
-
line += current
|
|
724
|
-
previous = token
|
|
725
|
-
}
|
|
726
|
-
const nextWord = (start) => {
|
|
727
|
-
for (let index = start; index < tokens.length; index += 1) {
|
|
728
|
-
if (tokens[index].kind !== 'comment') return tokens[index].text.toUpperCase()
|
|
729
|
-
}
|
|
730
|
-
return ''
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
tokens.forEach((token, index) => {
|
|
734
|
-
const upper = token.kind === 'word' ? token.text.toUpperCase() : ''
|
|
735
|
-
const next = nextWord(index + 1)
|
|
736
|
-
if (token.kind === 'comment') {
|
|
737
|
-
flush()
|
|
738
|
-
write(token)
|
|
739
|
-
flush()
|
|
740
|
-
return
|
|
741
|
-
}
|
|
742
|
-
if (token.text === '(') {
|
|
743
|
-
const subquery = next === 'SELECT' || next === 'WITH'
|
|
744
|
-
write(token)
|
|
745
|
-
parentheses.push({ subquery })
|
|
746
|
-
if (subquery) {
|
|
747
|
-
indentation += 1
|
|
748
|
-
scopes.push({ depth: parentheses.length, clause: null })
|
|
749
|
-
flush()
|
|
750
|
-
}
|
|
751
|
-
return
|
|
752
|
-
}
|
|
753
|
-
if (token.text === ')') {
|
|
754
|
-
const parenthesis = parentheses.pop()
|
|
755
|
-
if (parenthesis?.subquery) {
|
|
756
|
-
indentation = Math.max(0, indentation - 1)
|
|
757
|
-
scopes.pop()
|
|
758
|
-
flush()
|
|
759
|
-
}
|
|
760
|
-
write(token)
|
|
761
|
-
return
|
|
762
|
-
}
|
|
763
|
-
const joinedClause =
|
|
764
|
-
['LEFT', 'RIGHT', 'FULL', 'INNER', 'CROSS'].includes(upper) && next === 'JOIN'
|
|
765
|
-
if (clauseKeywords.has(upper) || joinedClause) {
|
|
766
|
-
flush()
|
|
767
|
-
write(token)
|
|
768
|
-
scopes.at(-1).clause = upper.toLowerCase()
|
|
769
|
-
return
|
|
770
|
-
}
|
|
771
|
-
if (
|
|
772
|
-
(upper === 'AND' || upper === 'OR') &&
|
|
773
|
-
['where', 'on', 'having'].includes(scopes.at(-1).clause)
|
|
774
|
-
) {
|
|
775
|
-
flush(1)
|
|
776
|
-
write(token)
|
|
777
|
-
return
|
|
778
|
-
}
|
|
779
|
-
if (token.text === ',') {
|
|
780
|
-
write(token)
|
|
781
|
-
const scope = scopes.at(-1)
|
|
782
|
-
if (
|
|
783
|
-
parentheses.length === scope.depth &&
|
|
784
|
-
['select', 'group', 'order'].includes(scope.clause)
|
|
785
|
-
) {
|
|
786
|
-
flush(1)
|
|
787
|
-
}
|
|
788
|
-
return
|
|
789
|
-
}
|
|
790
|
-
write(token)
|
|
791
|
-
if (token.text === ';') flush()
|
|
792
|
-
})
|
|
793
|
-
flush()
|
|
794
|
-
return lines.join('\n')
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
function queryDetail(query, index, total) {
|
|
798
|
-
const disclosure = document.createElement('details')
|
|
799
|
-
disclosure.className = 'trace-query'
|
|
800
|
-
if (total === 1) disclosure.open = true
|
|
801
|
-
disclosure.append(
|
|
802
|
-
text(
|
|
803
|
-
'summary',
|
|
804
|
-
`Query ${index + 1} · ${query.datasource} · ${query.status} · ${query.duration_ms} ms`,
|
|
805
|
-
),
|
|
806
|
-
)
|
|
807
|
-
const body = document.createElement('div')
|
|
808
|
-
body.className = 'trace-query-body'
|
|
809
|
-
body.append(
|
|
810
|
-
definitionList(
|
|
811
|
-
[
|
|
812
|
-
['数据源', query.datasource],
|
|
813
|
-
['方言', query.dialect],
|
|
814
|
-
['状态', query.status],
|
|
815
|
-
['返回行数', query.row_count],
|
|
816
|
-
['耗时', `${query.duration_ms} ms`],
|
|
817
|
-
['开始', query.started_at],
|
|
818
|
-
['完成', query.finished_at],
|
|
819
|
-
['Query ID', query.query_id],
|
|
820
|
-
['SQL digest', query.sql_digest],
|
|
821
|
-
],
|
|
822
|
-
'trace-query-meta',
|
|
823
|
-
),
|
|
824
|
-
)
|
|
825
|
-
const sql = document.createElement('pre')
|
|
826
|
-
sql.className = 'trace-query-sql'
|
|
827
|
-
sql.setAttribute('aria-label', `Query ${index + 1} SQL`)
|
|
828
|
-
sql.append(text('code', formatSql(query.sql)))
|
|
829
|
-
body.append(sql)
|
|
830
|
-
disclosure.append(body)
|
|
831
|
-
return disclosure
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
function querySection(run) {
|
|
835
|
-
if (!Object.hasOwn(run, 'queries')) return null
|
|
836
|
-
if (run.queries.length === 0) {
|
|
837
|
-
if (run.queries_omitted === 0) return null
|
|
838
|
-
return text(
|
|
839
|
-
'p',
|
|
840
|
-
`该动作执行了 ${run.queries_omitted} 条 Query;reader 投影未包含 SQL。`,
|
|
841
|
-
'trace-query-omitted',
|
|
842
|
-
)
|
|
843
|
-
}
|
|
844
|
-
const section = document.createElement('section')
|
|
845
|
-
section.className = 'trace-query-section'
|
|
846
|
-
section.append(text('h5', `查询详情(${run.queries.length})`))
|
|
847
|
-
run.queries.forEach((query, index) => {
|
|
848
|
-
section.append(queryDetail(query, index, run.queries.length))
|
|
849
|
-
})
|
|
850
|
-
return section
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
function runDetail(trace, run) {
|
|
854
|
-
const article = document.createElement('article')
|
|
855
|
-
article.className = 'trace-detail'
|
|
856
|
-
article.dataset.nodeKey = `run:${run.run_id}`
|
|
857
|
-
article.append(text('p', `分析动作 · ${run.lifecycle}`, 'trace-detail-kind'))
|
|
858
|
-
article.append(text('h4', runLabel(run)))
|
|
859
|
-
const inputFrames = run.input_artifact_refs
|
|
860
|
-
.map((ref) => trace.artifacts.find((artifact) => artifact.ref === ref))
|
|
861
|
-
.filter(Boolean)
|
|
862
|
-
.map(artifactLabel)
|
|
863
|
-
const outputFrame =
|
|
864
|
-
run.lifecycle === 'succeeded'
|
|
865
|
-
? trace.artifacts.find((artifact) => artifact.ref === run.output_artifact_ref)
|
|
866
|
-
: null
|
|
867
|
-
const entries = [
|
|
868
|
-
['能力', run.capability_id],
|
|
869
|
-
['状态', run.lifecycle],
|
|
870
|
-
['开始', run.started_at],
|
|
871
|
-
[
|
|
872
|
-
run.lifecycle === 'failed' ? '失败' : '完成',
|
|
873
|
-
run.lifecycle === 'succeeded'
|
|
874
|
-
? run.finished_at
|
|
875
|
-
: run.lifecycle === 'failed'
|
|
876
|
-
? run.failed_at
|
|
877
|
-
: null,
|
|
878
|
-
],
|
|
879
|
-
[
|
|
880
|
-
'耗时',
|
|
881
|
-
elapsed(
|
|
882
|
-
run.started_at,
|
|
883
|
-
run.lifecycle === 'succeeded'
|
|
884
|
-
? run.finished_at
|
|
885
|
-
: run.lifecycle === 'failed'
|
|
886
|
-
? run.failed_at
|
|
887
|
-
: null,
|
|
888
|
-
),
|
|
889
|
-
],
|
|
890
|
-
['输入 Frame', inputFrames.join('、') || '无'],
|
|
891
|
-
]
|
|
892
|
-
if (run.lifecycle === 'succeeded') {
|
|
893
|
-
entries.push(['输出 Frame', outputFrame ? artifactLabel(outputFrame) : '链路边界外'])
|
|
894
|
-
entries.push(['输出模式', run.output_mode])
|
|
895
|
-
}
|
|
896
|
-
if (run.lifecycle === 'failed') entries.push(['失败类型', run.failure.error_type])
|
|
897
|
-
article.append(definitionList(entries))
|
|
898
|
-
const queries = querySection(run)
|
|
899
|
-
if (queries) article.append(queries)
|
|
900
|
-
return article
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
function artifactDetail(trace, artifact) {
|
|
904
|
-
const article = document.createElement('article')
|
|
905
|
-
article.className = 'trace-detail'
|
|
906
|
-
article.dataset.nodeKey = `artifact:${artifact.ref}`
|
|
907
|
-
article.append(text('h4', artifactLabel(artifact)))
|
|
908
|
-
const dataset = matchingDataset(trace.session_id, artifact.ref)
|
|
909
|
-
if (dataset) {
|
|
910
|
-
const component = scope.MarivoArtifact
|
|
911
|
-
if (!component || typeof component.render !== 'function') {
|
|
912
|
-
fail('$MarivoArtifact', 'must be loaded before rendering a Session DAG')
|
|
913
|
-
}
|
|
914
|
-
const summary = document.createElement('div')
|
|
915
|
-
summary.className = 'trace-artifact-summary'
|
|
916
|
-
component.render(summary, dataset.dataset_id)
|
|
917
|
-
article.append(summary, previewTable(dataset))
|
|
918
|
-
} else {
|
|
919
|
-
article.append(text('p', '此页面未注册该 Artifact 的 dataset 预览。', 'trace-frame-count'))
|
|
920
|
-
}
|
|
921
|
-
return article
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
function traceLegend(trace, componentCount) {
|
|
925
|
-
const counts = [
|
|
926
|
-
`${trace.runs.length} 个分析动作`,
|
|
927
|
-
`${trace.artifacts.length} 个 Frame`,
|
|
928
|
-
`${componentCount} 条链路`,
|
|
929
|
-
]
|
|
930
|
-
const overview = document.createElement('header')
|
|
931
|
-
overview.className = 'trace-overview'
|
|
932
|
-
overview.append(text('p', counts.join(' · '), 'trace-count'))
|
|
933
|
-
const legend = document.createElement('div')
|
|
934
|
-
legend.className = 'trace-legend'
|
|
935
|
-
for (const [label, className] of [
|
|
936
|
-
['分析动作', 'trace-legend-run'],
|
|
937
|
-
['Frame', 'trace-legend-artifact'],
|
|
938
|
-
]) {
|
|
939
|
-
legend.append(text('span', label, className))
|
|
940
|
-
}
|
|
941
|
-
overview.append(legend)
|
|
942
|
-
return overview
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
function readBoundaryDisclosure() {
|
|
946
|
-
return text(
|
|
947
|
-
'p',
|
|
948
|
-
'该链路记录分析执行关系,不证明当前语义权威、数据源新鲜度或报告结论正确性。',
|
|
949
|
-
'trace-boundary',
|
|
950
|
-
)
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
function renderComponent(trace, component, componentIndex, live) {
|
|
954
|
-
const section = document.createElement('section')
|
|
955
|
-
section.className = 'trace-component'
|
|
956
|
-
const heading = document.createElement('div')
|
|
957
|
-
heading.className = 'trace-component-heading'
|
|
958
|
-
heading.append(text('h3', `分析链路 ${componentIndex + 1}`))
|
|
959
|
-
const controls = document.createElement('div')
|
|
960
|
-
controls.className = 'trace-controls'
|
|
961
|
-
controls.setAttribute('aria-label', '链路缩放')
|
|
962
|
-
heading.append(controls)
|
|
963
|
-
section.append(heading)
|
|
964
|
-
|
|
965
|
-
const workspace = document.createElement('div')
|
|
966
|
-
workspace.className = 'trace-workspace'
|
|
967
|
-
const canvasWrap = document.createElement('div')
|
|
968
|
-
canvasWrap.className = 'trace-canvas-wrap'
|
|
969
|
-
const detailsPanel = document.createElement('aside')
|
|
970
|
-
detailsPanel.className = 'trace-detail-panel'
|
|
971
|
-
detailsPanel.setAttribute('aria-live', 'polite')
|
|
972
|
-
const detailByKey = new Map()
|
|
973
|
-
for (const run of component.runs) {
|
|
974
|
-
const detail = runDetail(trace, run)
|
|
975
|
-
detailByKey.set(`run:${run.run_id}`, detail)
|
|
976
|
-
detailsPanel.append(detail)
|
|
977
|
-
}
|
|
978
|
-
for (const artifact of component.artifacts) {
|
|
979
|
-
const detail = artifactDetail(trace, artifact)
|
|
980
|
-
detailByKey.set(`artifact:${artifact.ref}`, detail)
|
|
981
|
-
detailsPanel.append(detail)
|
|
982
|
-
}
|
|
983
|
-
const nodesByKey = new Map()
|
|
984
|
-
const activate = (key) => {
|
|
985
|
-
for (const [candidate, detail] of detailByKey) {
|
|
986
|
-
const selected = candidate === key
|
|
987
|
-
detail.hidden = !selected
|
|
988
|
-
detail.dataset.active = String(selected)
|
|
989
|
-
detail.setAttribute('aria-hidden', String(!selected))
|
|
990
|
-
}
|
|
991
|
-
for (const [candidate, node] of nodesByKey) node.dataset.selected = String(candidate === key)
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
const layout = positions(component)
|
|
995
|
-
const maxX = Math.max(...[...layout.values()].map((position) => position.x), 40)
|
|
996
|
-
const maxY = Math.max(...[...layout.values()].map((position) => position.y), 40)
|
|
997
|
-
const svgId = ++nextId
|
|
998
|
-
const svg = svgElement('svg', {
|
|
999
|
-
class: 'trace-svg',
|
|
1000
|
-
role: 'group',
|
|
1001
|
-
viewBox: `0 0 ${maxX + 230} ${maxY + 130}`,
|
|
1002
|
-
'aria-labelledby': `trace-title-${svgId}`,
|
|
1003
|
-
})
|
|
1004
|
-
const title = svgElement('title', { id: `trace-title-${svgId}` })
|
|
1005
|
-
title.textContent = `分析链路 ${componentIndex + 1}`
|
|
1006
|
-
const description = svgElement('desc')
|
|
1007
|
-
description.textContent =
|
|
1008
|
-
'选择分析动作或 Frame 节点可查看审计详情;成功状态不表示报告结论可信。'
|
|
1009
|
-
const definitions = svgElement('defs')
|
|
1010
|
-
const marker = svgElement('marker', {
|
|
1011
|
-
id: `trace-arrow-${svgId}`,
|
|
1012
|
-
markerHeight: 8,
|
|
1013
|
-
markerWidth: 8,
|
|
1014
|
-
orient: 'auto',
|
|
1015
|
-
refX: 7,
|
|
1016
|
-
refY: 4,
|
|
1017
|
-
viewBox: '0 0 8 8',
|
|
1018
|
-
})
|
|
1019
|
-
marker.append(svgElement('path', { d: 'M0 0 L8 4 L0 8 Z', fill: 'currentColor' }))
|
|
1020
|
-
definitions.append(marker)
|
|
1021
|
-
const viewport = svgElement('g', { 'data-trace-viewport': '' })
|
|
1022
|
-
svg.append(title, description, definitions, viewport)
|
|
1023
|
-
for (const edge of component.edges) {
|
|
1024
|
-
const sourceKey =
|
|
1025
|
-
edge.kind === 'consumes' ? `artifact:${edge.artifact_ref}` : `run:${edge.run_id}`
|
|
1026
|
-
const targetKey =
|
|
1027
|
-
edge.kind === 'consumes' ? `run:${edge.run_id}` : `artifact:${edge.artifact_ref}`
|
|
1028
|
-
const sourcePosition = layout.get(sourceKey)
|
|
1029
|
-
const targetPosition = layout.get(targetKey)
|
|
1030
|
-
const run = component.runs.find((candidate) => candidate.run_id === edge.run_id)
|
|
1031
|
-
const artifact = component.artifacts.find((candidate) => candidate.ref === edge.artifact_ref)
|
|
1032
|
-
const sourceLabel = edge.kind === 'consumes' ? artifactLabel(artifact) : runLabel(run)
|
|
1033
|
-
const targetLabel = edge.kind === 'consumes' ? runLabel(run) : artifactLabel(artifact)
|
|
1034
|
-
const line = svgElement('line', {
|
|
1035
|
-
class: 'trace-edge',
|
|
1036
|
-
'data-kind': edge.kind,
|
|
1037
|
-
role: 'img',
|
|
1038
|
-
x1: sourcePosition.x + 170,
|
|
1039
|
-
x2: targetPosition.x,
|
|
1040
|
-
y1: sourcePosition.y + 38,
|
|
1041
|
-
y2: targetPosition.y + 38,
|
|
1042
|
-
'marker-end': `url(#trace-arrow-${svgId})`,
|
|
1043
|
-
'aria-label': `连接:${sourceLabel} 到 ${targetLabel}`,
|
|
1044
|
-
})
|
|
1045
|
-
viewport.append(line)
|
|
1046
|
-
}
|
|
1047
|
-
for (const run of component.runs) {
|
|
1048
|
-
const key = `run:${run.run_id}`
|
|
1049
|
-
const node = nodeGroup(
|
|
1050
|
-
'run',
|
|
1051
|
-
runLabel(run),
|
|
1052
|
-
run.capability_id,
|
|
1053
|
-
layout.get(key),
|
|
1054
|
-
{
|
|
1055
|
-
'data-lifecycle': run.lifecycle,
|
|
1056
|
-
'data-boundary': String(trace.boundary_run_ids.includes(run.run_id)),
|
|
1057
|
-
},
|
|
1058
|
-
live,
|
|
1059
|
-
() => activate(key),
|
|
1060
|
-
)
|
|
1061
|
-
nodesByKey.set(key, node)
|
|
1062
|
-
viewport.append(node)
|
|
1063
|
-
}
|
|
1064
|
-
for (const artifact of component.artifacts) {
|
|
1065
|
-
const key = `artifact:${artifact.ref}`
|
|
1066
|
-
const node = nodeGroup(
|
|
1067
|
-
'artifact',
|
|
1068
|
-
artifactLabel(artifact),
|
|
1069
|
-
`${artifact.row_count} 行`,
|
|
1070
|
-
layout.get(key),
|
|
1071
|
-
{
|
|
1072
|
-
'data-boundary': String(trace.boundary_artifact_refs.includes(artifact.ref)),
|
|
1073
|
-
'data-report-artifact': String(trace.report_artifact_refs.includes(artifact.ref)),
|
|
1074
|
-
},
|
|
1075
|
-
live,
|
|
1076
|
-
() => activate(key),
|
|
1077
|
-
)
|
|
1078
|
-
nodesByKey.set(key, node)
|
|
1079
|
-
viewport.append(node)
|
|
1080
|
-
}
|
|
1081
|
-
canvasWrap.append(svg)
|
|
1082
|
-
workspace.append(canvasWrap, detailsPanel)
|
|
1083
|
-
section.append(workspace)
|
|
1084
|
-
let scale = 1
|
|
1085
|
-
const applyScale = () => viewport.setAttribute('transform', `scale(${scale})`)
|
|
1086
|
-
for (const [label, action] of [
|
|
1087
|
-
['−', 'out'],
|
|
1088
|
-
['+', 'in'],
|
|
1089
|
-
['重置', 'reset'],
|
|
1090
|
-
]) {
|
|
1091
|
-
const button = text('button', label)
|
|
1092
|
-
button.type = 'button'
|
|
1093
|
-
button.setAttribute(
|
|
1094
|
-
'aria-label',
|
|
1095
|
-
action === 'out' ? '缩小' : action === 'in' ? '放大' : '重置缩放',
|
|
1096
|
-
)
|
|
1097
|
-
button.addEventListener('click', () => {
|
|
1098
|
-
scale =
|
|
1099
|
-
action === 'reset'
|
|
1100
|
-
? 1
|
|
1101
|
-
: Math.min(2.5, Math.max(0.5, scale * (action === 'in' ? 1.2 : 0.8)))
|
|
1102
|
-
applyScale()
|
|
1103
|
-
})
|
|
1104
|
-
controls.append(button)
|
|
1105
|
-
}
|
|
1106
|
-
const reportArtifact = component.artifacts.find((artifact) =>
|
|
1107
|
-
trace.report_artifact_refs.includes(artifact.ref),
|
|
1108
|
-
)
|
|
1109
|
-
const initialKey = reportArtifact
|
|
1110
|
-
? `artifact:${reportArtifact.ref}`
|
|
1111
|
-
: component.artifacts.length > 0
|
|
1112
|
-
? `artifact:${component.artifacts[0].ref}`
|
|
1113
|
-
: `run:${component.runs[0].run_id}`
|
|
1114
|
-
activate(initialKey)
|
|
1115
|
-
return section
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
function renderSessionGraph(container, trace) {
|
|
1119
|
-
if (!(container instanceof Element))
|
|
1120
|
-
fail('$container', 'must be an Element', 'container-invalid')
|
|
1121
|
-
const value = validateTrace(trace, trace?.trace_id)
|
|
1122
|
-
container.replaceChildren()
|
|
1123
|
-
const live = text('p', '', 'sr-only')
|
|
1124
|
-
live.setAttribute('aria-live', 'polite')
|
|
1125
|
-
const liveId = `trace-live-${++nextId}`
|
|
1126
|
-
live.id = liveId
|
|
1127
|
-
if (value.truncated)
|
|
1128
|
-
container.append(text('p', '有界链路:以下视图包含截断边界。', 'trace-boundary'))
|
|
1129
|
-
const components = graphComponents(value)
|
|
1130
|
-
container.append(traceLegend(value, components.length))
|
|
1131
|
-
for (const [index, component] of components.entries()) {
|
|
1132
|
-
container.append(renderComponent(value, component, index, live))
|
|
1133
|
-
}
|
|
1134
|
-
container.append(live, readBoundaryDisclosure())
|
|
1135
|
-
return container
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
function tracesForRender(traceIds) {
|
|
1139
|
-
if (!Array.isArray(traceIds) || traceIds.length === 0 || traceIds.length > 20) {
|
|
1140
|
-
fail('$traceIds', 'must contain between 1 and 20 registered trace ids')
|
|
1141
|
-
}
|
|
1142
|
-
const seen = new Set()
|
|
1143
|
-
return traceIds.map((id, index) => {
|
|
1144
|
-
validateId(id)
|
|
1145
|
-
if (seen.has(id)) fail(`$traceIds[${index}]`, 'must be unique')
|
|
1146
|
-
seen.add(id)
|
|
1147
|
-
return [id, get(id)]
|
|
1148
|
-
})
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
function renderSessionGraphs(container, traceIds = list()) {
|
|
1152
|
-
if (!(container instanceof Element))
|
|
1153
|
-
fail('$container', 'must be an Element', 'container-invalid')
|
|
1154
|
-
const selected = tracesForRender(traceIds)
|
|
1155
|
-
const sessions = new Map()
|
|
1156
|
-
for (const entry of selected) {
|
|
1157
|
-
const sessionId = entry[1].session_id
|
|
1158
|
-
const session = sessions.get(sessionId) ?? []
|
|
1159
|
-
session.push(entry)
|
|
1160
|
-
sessions.set(sessionId, session)
|
|
1161
|
-
}
|
|
1162
|
-
container.replaceChildren()
|
|
1163
|
-
container.append(
|
|
1164
|
-
text(
|
|
1165
|
-
'p',
|
|
1166
|
-
`${sessions.size} 个 Session · ${selected.length} 个聚焦 Graph`,
|
|
1167
|
-
'trace-session-count',
|
|
1168
|
-
),
|
|
1169
|
-
)
|
|
1170
|
-
let sessionIndex = 0
|
|
1171
|
-
for (const [sessionId, entries] of sessions) {
|
|
1172
|
-
sessionIndex += 1
|
|
1173
|
-
const section = document.createElement('section')
|
|
1174
|
-
section.className = 'trace-session'
|
|
1175
|
-
const heading = document.createElement('header')
|
|
1176
|
-
heading.className = 'trace-session-heading'
|
|
1177
|
-
heading.append(text('h2', `Session ${sessionIndex}`), text('code', sessionId))
|
|
1178
|
-
section.append(heading)
|
|
1179
|
-
for (const [traceId, trace] of entries) {
|
|
1180
|
-
const graph = document.createElement('section')
|
|
1181
|
-
graph.className = 'trace-session-graph'
|
|
1182
|
-
graph.append(text('h3', `聚焦 Graph · ${traceId}`))
|
|
1183
|
-
const host = document.createElement('div')
|
|
1184
|
-
host.className = 'trace-session-graph-host'
|
|
1185
|
-
renderSessionGraph(host, trace)
|
|
1186
|
-
graph.append(host)
|
|
1187
|
-
section.append(graph)
|
|
1188
|
-
}
|
|
1189
|
-
container.append(section)
|
|
1190
|
-
}
|
|
1191
|
-
return container
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
const api = Object.freeze({ get, has, list, register, renderSessionGraph, renderSessionGraphs })
|
|
1195
|
-
Object.defineProperty(scope, 'ReportTrace', {
|
|
1196
|
-
configurable: false,
|
|
1197
|
-
enumerable: true,
|
|
1198
|
-
value: api,
|
|
1199
|
-
writable: false,
|
|
1200
|
-
})
|
|
1201
|
-
})(globalThis)
|