@baron1996/kline-scene-schema 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/decimal-normalization.d.ts +6 -0
- package/dist/decimal-normalization.d.ts.map +1 -0
- package/dist/decimal-normalization.js +47 -0
- package/dist/drawable-workspace-canonical-json.d.ts +3 -0
- package/dist/drawable-workspace-canonical-json.d.ts.map +1 -0
- package/dist/drawable-workspace-canonical-json.js +15 -0
- package/dist/drawable-workspace-canonicalize.d.ts +3 -0
- package/dist/drawable-workspace-canonicalize.d.ts.map +1 -0
- package/dist/drawable-workspace-canonicalize.js +19 -0
- package/dist/drawable-workspace-errors.d.ts +14 -0
- package/dist/drawable-workspace-errors.d.ts.map +1 -0
- package/dist/drawable-workspace-errors.js +12 -0
- package/dist/drawable-workspace-semantic-validator.d.ts +5 -0
- package/dist/drawable-workspace-semantic-validator.d.ts.map +1 -0
- package/dist/drawable-workspace-semantic-validator.js +127 -0
- package/dist/drawable-workspace-validator.d.ts +4 -0
- package/dist/drawable-workspace-validator.d.ts.map +1 -0
- package/dist/drawable-workspace-validator.js +74 -0
- package/dist/drawing-canonical-json.d.ts +3 -0
- package/dist/drawing-canonical-json.d.ts.map +1 -0
- package/dist/drawing-canonical-json.js +15 -0
- package/dist/drawing-canonicalize.d.ts +3 -0
- package/dist/drawing-canonicalize.d.ts.map +1 -0
- package/dist/drawing-canonicalize.js +19 -0
- package/dist/drawing-errors.d.ts +13 -0
- package/dist/drawing-errors.d.ts.map +1 -0
- package/dist/drawing-errors.js +12 -0
- package/dist/drawing-semantic-validator.d.ts +6 -0
- package/dist/drawing-semantic-validator.d.ts.map +1 -0
- package/dist/drawing-semantic-validator.js +123 -0
- package/dist/drawing-validator.d.ts +4 -0
- package/dist/drawing-validator.d.ts.map +1 -0
- package/dist/drawing-validator.js +66 -0
- package/dist/generated/chart-scene.d.ts +11 -1
- package/dist/generated/chart-scene.d.ts.map +1 -1
- package/dist/generated/drawable-workspace.d.ts +526 -0
- package/dist/generated/drawable-workspace.d.ts.map +1 -0
- package/dist/generated/drawable-workspace.js +2 -0
- package/dist/generated/drawing-document.d.ts +216 -0
- package/dist/generated/drawing-document.d.ts.map +1 -0
- package/dist/generated/drawing-document.js +2 -0
- package/dist/generated/schemas.d.ts +1062 -17
- package/dist/generated/schemas.d.ts.map +1 -1
- package/dist/generated/schemas.js +1391 -28
- package/dist/generated/time-series-scene.d.ts +88 -0
- package/dist/generated/time-series-scene.d.ts.map +1 -0
- package/dist/generated/time-series-scene.js +2 -0
- package/dist/generated/validate-chart-scene.d.ts.map +1 -1
- package/dist/generated/validate-chart-scene.js +550 -237
- package/dist/generated/validate-drawable-workspace.d.ts +15 -0
- package/dist/generated/validate-drawable-workspace.d.ts.map +1 -0
- package/dist/generated/validate-drawable-workspace.js +13449 -0
- package/dist/generated/validate-drawing-document.d.ts +15 -0
- package/dist/generated/validate-drawing-document.d.ts.map +1 -0
- package/dist/generated/validate-drawing-document.js +4307 -0
- package/dist/generated/validate-time-series-scene.d.ts +15 -0
- package/dist/generated/validate-time-series-scene.d.ts.map +1 -0
- package/dist/generated/validate-time-series-scene.js +2359 -0
- package/dist/index.d.ts +20 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -1
- package/dist/time-series-canonical-json.d.ts +3 -0
- package/dist/time-series-canonical-json.d.ts.map +1 -0
- package/dist/time-series-canonical-json.js +15 -0
- package/dist/time-series-canonicalize.d.ts +3 -0
- package/dist/time-series-canonicalize.d.ts.map +1 -0
- package/dist/time-series-canonicalize.js +19 -0
- package/dist/time-series-errors.d.ts +13 -0
- package/dist/time-series-errors.d.ts.map +1 -0
- package/dist/time-series-errors.js +12 -0
- package/dist/time-series-semantic-validator.d.ts +5 -0
- package/dist/time-series-semantic-validator.d.ts.map +1 -0
- package/dist/time-series-semantic-validator.js +81 -0
- package/dist/time-series-validator.d.ts +4 -0
- package/dist/time-series-validator.d.ts.map +1 -0
- package/dist/time-series-validator.js +52 -0
- package/package.json +1 -1
- package/schema/chart-config.schema.json +68 -1
- package/schema/chart-scene.schema.json +1 -27
- package/schema/common.schema.json +26 -0
- package/schema/drawable-workspace.schema.json +110 -0
- package/schema/drawing-document.schema.json +811 -0
- package/schema/time-series-chart-config.schema.json +75 -0
- package/schema/time-series-runtime.schema.json +13 -0
- package/schema/time-series-scene.schema.json +145 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decimal-normalization.d.ts","sourceRoot":"","sources":["../src/decimal-normalization.ts"],"names":[],"mappings":"AAsBA;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CA8B9E"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
function toDecimalParts(value) {
|
|
2
|
+
const [coefficient = '', exponentText] = value.toString().toLowerCase().split('e');
|
|
3
|
+
const [integer = '', fraction = ''] = coefficient.split('.');
|
|
4
|
+
return {
|
|
5
|
+
digits: BigInt(`${integer}${fraction}`),
|
|
6
|
+
exponent: Number(exponentText ?? 0) - fraction.length,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function toDecimalNumber(value, precision, negative) {
|
|
10
|
+
const digits = value.toString().padStart(precision + 1, '0');
|
|
11
|
+
const unsigned = precision === 0
|
|
12
|
+
? digits
|
|
13
|
+
: `${digits.slice(0, -precision)}.${digits.slice(-precision)}`;
|
|
14
|
+
return Number(`${negative ? '-' : ''}${unsigned}`);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 仓库唯一精确十进制规范化核心:最接近、恰好半值时远离零,负零统一转正零。
|
|
18
|
+
* 非法输入抛 RangeError,由调用方映射到各自稳定错误类型。
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeDecimalValue(value, precision) {
|
|
21
|
+
if (!Number.isFinite(value)) {
|
|
22
|
+
throw new RangeError('Decimal value must be finite.');
|
|
23
|
+
}
|
|
24
|
+
if (!Number.isInteger(precision) ||
|
|
25
|
+
precision < 0 ||
|
|
26
|
+
precision > 16) {
|
|
27
|
+
throw new RangeError('Precision must be an integer from 0 through 16.');
|
|
28
|
+
}
|
|
29
|
+
const negative = value < 0;
|
|
30
|
+
const { digits, exponent } = toDecimalParts(Math.abs(value));
|
|
31
|
+
const scaledExponent = exponent + precision;
|
|
32
|
+
let rounded;
|
|
33
|
+
if (scaledExponent >= 0) {
|
|
34
|
+
rounded = digits * (10n ** BigInt(scaledExponent));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const divisor = 10n ** BigInt(-scaledExponent);
|
|
38
|
+
const quotient = digits / divisor;
|
|
39
|
+
const remainder = digits % divisor;
|
|
40
|
+
rounded = quotient + (remainder * 2n >= divisor ? 1n : 0n);
|
|
41
|
+
}
|
|
42
|
+
const normalized = toDecimalNumber(rounded, precision, negative);
|
|
43
|
+
if (!Number.isFinite(normalized)) {
|
|
44
|
+
throw new RangeError('Normalized decimal value is not finite.');
|
|
45
|
+
}
|
|
46
|
+
return Object.is(normalized, -0) ? 0 : normalized;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawable-workspace-canonical-json.d.ts","sourceRoot":"","sources":["../src/drawable-workspace-canonical-json.ts"],"names":[],"mappings":"AAKA,wBAAgB,mCAAmC,CAClD,KAAK,EAAE,OAAO,GACZ,UAAU,CAAC,WAAW,CAAC,CAWzB;AAED,wBAAsB,8BAA8B,CACnD,KAAK,EAAE,OAAO,GACZ,OAAO,CAAC,MAAM,CAAC,CASjB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import canonicalize from 'canonicalize';
|
|
2
|
+
import { DrawableWorkspaceError } from './drawable-workspace-errors.js';
|
|
3
|
+
import { parseDrawableWorkspaceDocument } from './drawable-workspace-validator.js';
|
|
4
|
+
export function serializeCanonicalDrawableWorkspace(value) {
|
|
5
|
+
const parsed = parseDrawableWorkspaceDocument(value);
|
|
6
|
+
const serialized = canonicalize(parsed);
|
|
7
|
+
if (serialized === undefined) {
|
|
8
|
+
throw new DrawableWorkspaceError('DRAWABLE_WORKSPACE_SCHEMA_INVALID', '/', 'DrawableWorkspaceDocument could not be serialized.');
|
|
9
|
+
}
|
|
10
|
+
return new TextEncoder().encode(serialized);
|
|
11
|
+
}
|
|
12
|
+
export async function hashCanonicalDrawableWorkspace(value) {
|
|
13
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', serializeCanonicalDrawableWorkspace(value));
|
|
14
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawable-workspace-canonicalize.d.ts","sourceRoot":"","sources":["../src/drawable-workspace-canonicalize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,yBAAyB,EAEzB,MAAM,mCAAmC,CAAC;AAwB3C,wBAAgB,6BAA6B,CAC5C,SAAS,EAAE,yBAAyB,GAClC,yBAAyB,CAI3B"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function sortWorkspaceJson(value) {
|
|
2
|
+
if (Array.isArray(value)) {
|
|
3
|
+
return value.map((item) => sortWorkspaceJson(item));
|
|
4
|
+
}
|
|
5
|
+
if (value !== null && typeof value === 'object') {
|
|
6
|
+
const sorted = {};
|
|
7
|
+
for (const key of Object.keys(value).sort()) {
|
|
8
|
+
const child = value[key];
|
|
9
|
+
if (child !== undefined) {
|
|
10
|
+
sorted[key] = sortWorkspaceJson(child);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return sorted;
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
export function canonicalizeDrawableWorkspace(workspace) {
|
|
18
|
+
return sortWorkspaceJson(structuredClone(workspace));
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DrawingDocumentErrorCode } from './drawing-errors.js';
|
|
2
|
+
export type DrawableWorkspaceErrorCode = 'DRAWABLE_WORKSPACE_SCHEMA_INVALID' | 'DRAWABLE_WORKSPACE_BINDING_MISMATCH' | 'DRAWABLE_WORKSPACE_DOUBLE_AUTHORITY' | 'DRAWABLE_SCENE_KIND_UNSUPPORTED' | DrawingDocumentErrorCode;
|
|
3
|
+
export interface DrawableWorkspaceIssue {
|
|
4
|
+
readonly code: DrawableWorkspaceErrorCode;
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class DrawableWorkspaceError extends Error {
|
|
9
|
+
readonly code: DrawableWorkspaceErrorCode;
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly issues: readonly DrawableWorkspaceIssue[];
|
|
12
|
+
constructor(code: DrawableWorkspaceErrorCode, path: string, message: string, issues?: readonly DrawableWorkspaceIssue[]);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=drawable-workspace-errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawable-workspace-errors.d.ts","sourceRoot":"","sources":["../src/drawable-workspace-errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAEpE,MAAM,MAAM,0BAA0B,GACnC,mCAAmC,GACnC,qCAAqC,GACrC,qCAAqC,GACrC,iCAAiC,GACjC,wBAAwB,CAAC;AAE5B,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,IAAI,EAAE,0BAA0B,CAAC;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,sBAAuB,SAAQ,KAAK;IAChD,SAAgB,IAAI,EAAE,0BAA0B,CAAC;IACjD,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,SAAgB,MAAM,EAAE,SAAS,sBAAsB,EAAE,CAAC;gBAGzD,IAAI,EAAE,0BAA0B,EAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,SAAS,sBAAsB,EAAE;CAQ3C"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class DrawableWorkspaceError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
path;
|
|
4
|
+
issues;
|
|
5
|
+
constructor(code, path, message, issues) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'DrawableWorkspaceError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.path = path;
|
|
10
|
+
this.issues = issues ?? [{ code, path, message }];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DrawableWorkspaceDocument } from './generated/drawable-workspace.js';
|
|
2
|
+
import { type DrawableWorkspaceIssue } from './drawable-workspace-errors.js';
|
|
3
|
+
export declare function collectWorkspaceSemanticIssues(workspace: DrawableWorkspaceDocument): readonly DrawableWorkspaceIssue[];
|
|
4
|
+
export declare function assertSemanticDrawableWorkspace(workspace: DrawableWorkspaceDocument): void;
|
|
5
|
+
//# sourceMappingURL=drawable-workspace-semantic-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawable-workspace-semantic-validator.d.ts","sourceRoot":"","sources":["../src/drawable-workspace-semantic-validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGX,yBAAyB,EAIzB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAGN,KAAK,sBAAsB,EAC3B,MAAM,gCAAgC,CAAC;AAoKxC,wBAAgB,8BAA8B,CAC7C,SAAS,EAAE,yBAAyB,GAClC,SAAS,sBAAsB,EAAE,CA2EnC;AAED,wBAAgB,+BAA+B,CAC9C,SAAS,EAAE,yBAAyB,GAClC,IAAI,CAMN"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { DrawableWorkspaceError, } from './drawable-workspace-errors.js';
|
|
2
|
+
function issue(code, path, message) {
|
|
3
|
+
return { code, path, message };
|
|
4
|
+
}
|
|
5
|
+
function sameAxes(left, right) {
|
|
6
|
+
if (left.length !== right.length) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
return left.every((axis, index) => {
|
|
10
|
+
const other = right[index];
|
|
11
|
+
return other !== undefined
|
|
12
|
+
&& axis.paneRole === other.paneRole
|
|
13
|
+
&& axis.yAxisRole === other.yAxisRole
|
|
14
|
+
&& axis.valuePrecision === other.valuePrecision;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function indicatorsOf(scene) {
|
|
18
|
+
return scene.panes.flatMap((pane) => pane.indicators);
|
|
19
|
+
}
|
|
20
|
+
function validateChartBinding(scene, drawings, issues) {
|
|
21
|
+
const indicators = indicatorsOf(scene);
|
|
22
|
+
for (let index = 0; index < drawings.drawings.length; index++) {
|
|
23
|
+
const drawing = drawings.drawings[index];
|
|
24
|
+
if (drawing === undefined) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const path = `/drawings/${index}/target`;
|
|
28
|
+
const target = drawing.target;
|
|
29
|
+
if (target.yAxisRole !== 'primary') {
|
|
30
|
+
issues.push(issue('DRAWING_TARGET_INVALID', `${path}/yAxisRole`, 'v1 Workspace drawings only bind the primary y-axis role.'));
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (target.paneRole === 'candle') {
|
|
34
|
+
const expectedPrecision = scene.symbol.pricePrecision;
|
|
35
|
+
const axis = drawings.coordinateSystem.valueAxes.find((candidate) => candidate.paneRole === 'candle' &&
|
|
36
|
+
candidate.yAxisRole === 'primary');
|
|
37
|
+
if (axis === undefined || axis.valuePrecision !== expectedPrecision) {
|
|
38
|
+
issues.push(issue('DRAWING_TARGET_INVALID', path, 'Candle target precision must equal symbol.pricePrecision.'));
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (target.paneRole.startsWith('indicator:')) {
|
|
43
|
+
const indicatorId = target.paneRole.slice('indicator:'.length);
|
|
44
|
+
const matches = indicators.filter((indicator) => indicator.id === indicatorId);
|
|
45
|
+
if (matches.length !== 1) {
|
|
46
|
+
issues.push(issue('DRAWING_TARGET_INVALID', path, `Indicator target must resolve to exactly one SceneIndicator: ${indicatorId}.`));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const indicator = matches[0];
|
|
50
|
+
const pane = scene.panes.find((candidate) => candidate.id === indicator.paneId);
|
|
51
|
+
const primaryAxis = pane?.yAxes.find((axis) => axis.role === 'primary');
|
|
52
|
+
if (primaryAxis === undefined || indicator.yAxisId !== primaryAxis.id) {
|
|
53
|
+
issues.push(issue('DRAWING_TARGET_INVALID', path, 'Indicator target must bind the primary axis of its owning Pane.'));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const axis = drawings.coordinateSystem.valueAxes.find((candidate) => candidate.paneRole === target.paneRole &&
|
|
57
|
+
candidate.yAxisRole === 'primary');
|
|
58
|
+
if (axis === undefined || axis.valuePrecision !== indicator.precision) {
|
|
59
|
+
issues.push(issue('DRAWING_TARGET_INVALID', path, 'Indicator target precision must equal the indicator precision.'));
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
issues.push(issue('DRAWABLE_SCENE_KIND_UNSUPPORTED', `${path}/paneRole`, `Chart Scene cannot interpret pane role: ${target.paneRole}.`));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function validateTimeSeriesBinding(scene, drawings, issues) {
|
|
67
|
+
const sharedPrecision = scene.series[0]?.precision;
|
|
68
|
+
const axis = drawings.coordinateSystem.valueAxes.find((candidate) => candidate.paneRole === 'time-series' &&
|
|
69
|
+
candidate.yAxisRole === 'primary');
|
|
70
|
+
if (axis === undefined || axis.valuePrecision !== sharedPrecision) {
|
|
71
|
+
issues.push(issue('DRAWING_TARGET_INVALID', '/binding/valueAxes', 'TimeSeries primary binding precision must equal the shared series precision.'));
|
|
72
|
+
}
|
|
73
|
+
for (let index = 0; index < drawings.drawings.length; index++) {
|
|
74
|
+
const drawing = drawings.drawings[index];
|
|
75
|
+
if (drawing === undefined) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const target = drawing.target;
|
|
79
|
+
if (target.paneRole !== 'time-series' ||
|
|
80
|
+
target.yAxisRole !== 'primary') {
|
|
81
|
+
issues.push(issue('DRAWING_TARGET_INVALID', `/drawings/${index}/target`, 'TimeSeries drawings must target time-series / primary.'));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function collectWorkspaceSemanticIssues(workspace) {
|
|
86
|
+
const issues = [];
|
|
87
|
+
const binding = workspace.binding;
|
|
88
|
+
const drawings = workspace.drawings;
|
|
89
|
+
if (binding.scopeKey !== drawings.scopeKey) {
|
|
90
|
+
issues.push(issue('DRAWABLE_WORKSPACE_BINDING_MISMATCH', '/binding/scopeKey', 'Workspace binding scopeKey must equal the DrawingDocument scopeKey.'));
|
|
91
|
+
}
|
|
92
|
+
if (binding.timezone !== drawings.coordinateSystem.timezone) {
|
|
93
|
+
issues.push(issue('DRAWABLE_WORKSPACE_BINDING_MISMATCH', '/binding/timezone', 'Workspace binding timezone must equal the DrawingDocument timezone.'));
|
|
94
|
+
}
|
|
95
|
+
if (!sameAxes(binding.valueAxes, drawings.coordinateSystem.valueAxes)) {
|
|
96
|
+
issues.push(issue('DRAWABLE_WORKSPACE_BINDING_MISMATCH', '/binding/valueAxes', 'Workspace binding valueAxes must exactly equal the DrawingDocument valueAxes.'));
|
|
97
|
+
}
|
|
98
|
+
const scene = workspace.scene;
|
|
99
|
+
if (scene.kind === 'chart') {
|
|
100
|
+
const document = scene.document;
|
|
101
|
+
if (document.chart.timezone !== binding.timezone) {
|
|
102
|
+
issues.push(issue('DRAWABLE_WORKSPACE_BINDING_MISMATCH', '/scene/document/chart/timezone', 'Chart Scene timezone must equal the Workspace binding timezone.'));
|
|
103
|
+
}
|
|
104
|
+
if (document.overlays.length > 0) {
|
|
105
|
+
issues.push(issue('DRAWABLE_WORKSPACE_DOUBLE_AUTHORITY', '/scene/document/overlays', 'Workspace chart Scenes must not carry legacy overlays.'));
|
|
106
|
+
}
|
|
107
|
+
validateChartBinding(document, drawings, issues);
|
|
108
|
+
}
|
|
109
|
+
else if (scene.kind === 'time-series') {
|
|
110
|
+
const document = scene.document;
|
|
111
|
+
if (document.chart.timezone !== binding.timezone) {
|
|
112
|
+
issues.push(issue('DRAWABLE_WORKSPACE_BINDING_MISMATCH', '/scene/document/chart/timezone', 'TimeSeries Scene timezone must equal the Workspace binding timezone.'));
|
|
113
|
+
}
|
|
114
|
+
validateTimeSeriesBinding(document, drawings, issues);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
issues.push(issue('DRAWABLE_SCENE_KIND_UNSUPPORTED', '/scene/kind', `Unsupported Workspace scene kind: ${String(scene.kind)}.`));
|
|
118
|
+
}
|
|
119
|
+
return issues;
|
|
120
|
+
}
|
|
121
|
+
export function assertSemanticDrawableWorkspace(workspace) {
|
|
122
|
+
const issues = collectWorkspaceSemanticIssues(workspace);
|
|
123
|
+
const first = issues[0];
|
|
124
|
+
if (first !== undefined) {
|
|
125
|
+
throw new DrawableWorkspaceError(first.code, first.path, first.message, issues);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DrawableWorkspaceDocument } from './generated/drawable-workspace.js';
|
|
2
|
+
export declare function parseStructuralDrawableWorkspace(value: unknown): DrawableWorkspaceDocument;
|
|
3
|
+
export declare function parseDrawableWorkspaceDocument(value: unknown): DrawableWorkspaceDocument;
|
|
4
|
+
//# sourceMappingURL=drawable-workspace-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawable-workspace-validator.d.ts","sourceRoot":"","sources":["../src/drawable-workspace-validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEX,yBAAyB,EAGzB,MAAM,mCAAmC,CAAC;AA4E3C,wBAAgB,gCAAgC,CAC/C,KAAK,EAAE,OAAO,GACZ,yBAAyB,CA8B3B;AAED,wBAAgB,8BAA8B,CAC7C,KAAK,EAAE,OAAO,GACZ,yBAAyB,CAgB3B"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { validateDrawableWorkspace as generatedValidator } from './generated/validate-drawable-workspace.js';
|
|
2
|
+
import { canonicalizeDrawableWorkspace } from './drawable-workspace-canonicalize.js';
|
|
3
|
+
import { DrawableWorkspaceError, } from './drawable-workspace-errors.js';
|
|
4
|
+
import { assertSemanticDrawableWorkspace } from './drawable-workspace-semantic-validator.js';
|
|
5
|
+
import { parseChartScene } from './validator.js';
|
|
6
|
+
import { parseTimeSeriesScene } from './time-series-validator.js';
|
|
7
|
+
import { parseDrawingDocument } from './drawing-validator.js';
|
|
8
|
+
const validateStructural = generatedValidator;
|
|
9
|
+
function pointerToken(value) {
|
|
10
|
+
return value.replaceAll('~', '~0').replaceAll('/', '~1');
|
|
11
|
+
}
|
|
12
|
+
function structuralPath(error) {
|
|
13
|
+
if (error.keyword === 'additionalProperties' &&
|
|
14
|
+
error.params?.additionalProperty !== undefined) {
|
|
15
|
+
return `${error.instancePath}/${pointerToken(error.params.additionalProperty)}` || '/';
|
|
16
|
+
}
|
|
17
|
+
if (error.keyword === 'required' &&
|
|
18
|
+
error.params?.missingProperty !== undefined) {
|
|
19
|
+
return `${error.instancePath}/${pointerToken(error.params.missingProperty)}` || '/';
|
|
20
|
+
}
|
|
21
|
+
return error.instancePath || '/';
|
|
22
|
+
}
|
|
23
|
+
function structuralCode(_error) {
|
|
24
|
+
return 'DRAWABLE_WORKSPACE_SCHEMA_INVALID';
|
|
25
|
+
}
|
|
26
|
+
function isTopLevelStructuralError(error, all) {
|
|
27
|
+
if (error.keyword === 'oneOf') {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
const oneOfPaths = all
|
|
31
|
+
.filter((candidate) => candidate.keyword === 'oneOf')
|
|
32
|
+
.map((candidate) => candidate.instancePath);
|
|
33
|
+
const descendant = oneOfPaths.some((prefix) => error.instancePath.length > prefix.length &&
|
|
34
|
+
error.instancePath.startsWith(`${prefix}/`));
|
|
35
|
+
if (!descendant) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return error.schemaPath.startsWith('#/properties/type/');
|
|
39
|
+
}
|
|
40
|
+
export function parseStructuralDrawableWorkspace(value) {
|
|
41
|
+
if (!validateStructural(value)) {
|
|
42
|
+
const all = validateStructural.errors ?? [];
|
|
43
|
+
const errors = all
|
|
44
|
+
.filter((error) => isTopLevelStructuralError(error, all))
|
|
45
|
+
.map((error) => ({
|
|
46
|
+
code: structuralCode(error),
|
|
47
|
+
path: structuralPath(error),
|
|
48
|
+
message: error.message ?? `Schema validation failed at ${error.schemaPath}.`,
|
|
49
|
+
}))
|
|
50
|
+
.sort((left, right) => (left.message.includes('exactly one schema in oneOf') ? 1 : 0) -
|
|
51
|
+
(right.message.includes('exactly one schema in oneOf') ? 1 : 0) ||
|
|
52
|
+
left.path.localeCompare(right.path));
|
|
53
|
+
const issues = errors;
|
|
54
|
+
const first = issues[0] ?? {
|
|
55
|
+
code: 'DRAWABLE_WORKSPACE_SCHEMA_INVALID',
|
|
56
|
+
path: '/',
|
|
57
|
+
message: 'DrawableWorkspaceDocument schema validation failed.',
|
|
58
|
+
};
|
|
59
|
+
throw new DrawableWorkspaceError(first.code, first.path, first.message, issues);
|
|
60
|
+
}
|
|
61
|
+
return structuredClone(value);
|
|
62
|
+
}
|
|
63
|
+
export function parseDrawableWorkspaceDocument(value) {
|
|
64
|
+
const structural = parseStructuralDrawableWorkspace(value);
|
|
65
|
+
if (structural.scene.kind === 'chart') {
|
|
66
|
+
structural.scene.document = parseChartScene(structural.scene.document);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
structural.scene.document = parseTimeSeriesScene(structural.scene.document);
|
|
70
|
+
}
|
|
71
|
+
structural.drawings = parseDrawingDocument(structural.drawings);
|
|
72
|
+
assertSemanticDrawableWorkspace(structural);
|
|
73
|
+
return canonicalizeDrawableWorkspace(structural);
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawing-canonical-json.d.ts","sourceRoot":"","sources":["../src/drawing-canonical-json.ts"],"names":[],"mappings":"AAKA,wBAAgB,iCAAiC,CAChD,KAAK,EAAE,OAAO,GACZ,UAAU,CAAC,WAAW,CAAC,CAWzB;AAED,wBAAsB,4BAA4B,CACjD,KAAK,EAAE,OAAO,GACZ,OAAO,CAAC,MAAM,CAAC,CASjB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import canonicalize from 'canonicalize';
|
|
2
|
+
import { DrawingDocumentError } from './drawing-errors.js';
|
|
3
|
+
import { parseDrawingDocument } from './drawing-validator.js';
|
|
4
|
+
export function serializeCanonicalDrawingDocument(value) {
|
|
5
|
+
const parsed = parseDrawingDocument(value);
|
|
6
|
+
const serialized = canonicalize(parsed);
|
|
7
|
+
if (serialized === undefined) {
|
|
8
|
+
throw new DrawingDocumentError('DRAWING_DOCUMENT_SCHEMA_INVALID', '/', 'DrawingDocument could not be serialized.');
|
|
9
|
+
}
|
|
10
|
+
return new TextEncoder().encode(serialized);
|
|
11
|
+
}
|
|
12
|
+
export async function hashCanonicalDrawingDocument(value) {
|
|
13
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', serializeCanonicalDrawingDocument(value));
|
|
14
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawing-canonicalize.d.ts","sourceRoot":"","sources":["../src/drawing-canonicalize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,eAAe,EAEf,MAAM,iCAAiC,CAAC;AAwBzC,wBAAgB,2BAA2B,CAC1C,QAAQ,EAAE,eAAe,GACvB,eAAe,CAIjB"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function sortDrawingJson(value) {
|
|
2
|
+
if (Array.isArray(value)) {
|
|
3
|
+
return value.map((item) => sortDrawingJson(item));
|
|
4
|
+
}
|
|
5
|
+
if (value !== null && typeof value === 'object') {
|
|
6
|
+
const sorted = {};
|
|
7
|
+
for (const key of Object.keys(value).sort()) {
|
|
8
|
+
const child = value[key];
|
|
9
|
+
if (child !== undefined) {
|
|
10
|
+
sorted[key] = sortDrawingJson(child);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return sorted;
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
export function canonicalizeDrawingDocument(document) {
|
|
18
|
+
return sortDrawingJson(structuredClone(document));
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type DrawingDocumentErrorCode = 'DRAWING_DOCUMENT_SCHEMA_INVALID' | 'DRAWING_DOCUMENT_SEMANTIC_INVALID' | 'DRAWING_DUPLICATE_ID' | 'DRAWING_GEOMETRY_INVALID' | 'DRAWING_TARGET_INVALID';
|
|
2
|
+
export interface DrawingDocumentIssue {
|
|
3
|
+
readonly code: DrawingDocumentErrorCode;
|
|
4
|
+
readonly path: string;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class DrawingDocumentError extends Error {
|
|
8
|
+
readonly code: DrawingDocumentErrorCode;
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly issues: readonly DrawingDocumentIssue[];
|
|
11
|
+
constructor(code: DrawingDocumentErrorCode, path: string, message: string, issues?: readonly DrawingDocumentIssue[]);
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=drawing-errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawing-errors.d.ts","sourceRoot":"","sources":["../src/drawing-errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,wBAAwB,GACjC,iCAAiC,GACjC,mCAAmC,GACnC,sBAAsB,GACtB,0BAA0B,GAC1B,wBAAwB,CAAC;AAE5B,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IAC9C,SAAgB,IAAI,EAAE,wBAAwB,CAAC;IAC/C,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,SAAgB,MAAM,EAAE,SAAS,oBAAoB,EAAE,CAAC;gBAGvD,IAAI,EAAE,wBAAwB,EAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,SAAS,oBAAoB,EAAE;CAQzC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class DrawingDocumentError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
path;
|
|
4
|
+
issues;
|
|
5
|
+
constructor(code, path, message, issues) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'DrawingDocumentError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.path = path;
|
|
10
|
+
this.issues = issues ?? [{ code, path, message }];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DrawingDocument, ValueAxis } from './generated/drawing-document.js';
|
|
2
|
+
import { type DrawingDocumentIssue } from './drawing-errors.js';
|
|
3
|
+
export declare function collectDrawingSemanticIssues(document: DrawingDocument): readonly DrawingDocumentIssue[];
|
|
4
|
+
export declare function assertSemanticDrawingDocument(document: DrawingDocument): void;
|
|
5
|
+
export type { ValueAxis };
|
|
6
|
+
//# sourceMappingURL=drawing-semantic-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawing-semantic-validator.d.ts","sourceRoot":"","sources":["../src/drawing-semantic-validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,eAAe,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAC3F,OAAO,EAAuD,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAsKrH,wBAAgB,4BAA4B,CAC3C,QAAQ,EAAE,eAAe,GACvB,SAAS,oBAAoB,EAAE,CAsBjC;AAED,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAM7E;AAED,YAAY,EAAE,SAAS,EAAE,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { DrawingDocumentError } from './drawing-errors.js';
|
|
2
|
+
import { normalizeDecimalValue } from './decimal-normalization.js';
|
|
3
|
+
function issue(code, path, message) {
|
|
4
|
+
return { code, path, message };
|
|
5
|
+
}
|
|
6
|
+
function drawingGeometryValues(drawing, path) {
|
|
7
|
+
const result = [];
|
|
8
|
+
const add = (value, valuePath) => {
|
|
9
|
+
result.push({ path: valuePath, value });
|
|
10
|
+
};
|
|
11
|
+
switch (drawing.type) {
|
|
12
|
+
case 'horizontalStraightLine':
|
|
13
|
+
case 'priceLine':
|
|
14
|
+
add(drawing.geometry.value, `${path}/geometry/value`);
|
|
15
|
+
break;
|
|
16
|
+
case 'simpleTag':
|
|
17
|
+
add(drawing.geometry.value, `${path}/geometry/value`);
|
|
18
|
+
break;
|
|
19
|
+
case 'verticalStraightLine':
|
|
20
|
+
break;
|
|
21
|
+
case 'horizontalRayLine':
|
|
22
|
+
case 'horizontalSegment':
|
|
23
|
+
add(drawing.geometry.value, `${path}/geometry/value`);
|
|
24
|
+
break;
|
|
25
|
+
case 'verticalRayLine':
|
|
26
|
+
case 'verticalSegment':
|
|
27
|
+
add(drawing.geometry.startValue, `${path}/geometry/startValue`);
|
|
28
|
+
add(drawing.geometry.endValue, `${path}/geometry/endValue`);
|
|
29
|
+
break;
|
|
30
|
+
case 'rayLine':
|
|
31
|
+
case 'segment':
|
|
32
|
+
case 'straightLine':
|
|
33
|
+
case 'fibonacciLine':
|
|
34
|
+
case 'priceChannelLine':
|
|
35
|
+
case 'parallelStraightLine':
|
|
36
|
+
case 'brush':
|
|
37
|
+
for (let index = 0; index < drawing.geometry.points.length; index++) {
|
|
38
|
+
add(drawing.geometry.points[index].value, `${path}/geometry/points/${index}/value`);
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
case 'simpleAnnotation':
|
|
42
|
+
case 'callout':
|
|
43
|
+
case 'text':
|
|
44
|
+
case 'crossLine':
|
|
45
|
+
add(drawing.geometry.point.value, `${path}/geometry/point/value`);
|
|
46
|
+
break;
|
|
47
|
+
case 'rectangle':
|
|
48
|
+
case 'arrow':
|
|
49
|
+
case 'priceMeasurement':
|
|
50
|
+
add(drawing.geometry.start.value, `${path}/geometry/start/value`);
|
|
51
|
+
add(drawing.geometry.end.value, `${path}/geometry/end/value`);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
function validateValueAxes(document, issues) {
|
|
57
|
+
const axes = document.coordinateSystem.valueAxes;
|
|
58
|
+
for (let index = 0; index < axes.length; index++) {
|
|
59
|
+
const axis = axes[index];
|
|
60
|
+
if (axis === undefined) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const path = `/coordinateSystem/valueAxes/${index}`;
|
|
64
|
+
if (axis.yAxisRole !== 'primary') {
|
|
65
|
+
issues.push(issue('DRAWING_TARGET_INVALID', `${path}/yAxisRole`, 'v1 Drawing value axes only allow the primary role.'));
|
|
66
|
+
}
|
|
67
|
+
const previous = axes[index - 1];
|
|
68
|
+
if (previous !== undefined &&
|
|
69
|
+
(previous.paneRole > axis.paneRole ||
|
|
70
|
+
(previous.paneRole === axis.paneRole && previous.yAxisRole >= axis.yAxisRole))) {
|
|
71
|
+
issues.push(issue('DRAWING_DOCUMENT_SEMANTIC_INVALID', path, 'valueAxes must be lexically ascending by paneRole and yAxisRole without duplicates.'));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function validateDrawing(document, drawing, index, issues) {
|
|
76
|
+
const path = `/drawings/${index}`;
|
|
77
|
+
const axis = document.coordinateSystem.valueAxes.find((candidate) => candidate.paneRole === drawing.target.paneRole &&
|
|
78
|
+
candidate.yAxisRole === drawing.target.yAxisRole);
|
|
79
|
+
if (axis === undefined) {
|
|
80
|
+
issues.push(issue('DRAWING_TARGET_INVALID', `${path}/target`, 'Drawing target must exactly match one coordinateSystem.valueAxes entry.'));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (drawing.target.yAxisRole !== 'primary') {
|
|
84
|
+
issues.push(issue('DRAWING_TARGET_INVALID', `${path}/target/yAxisRole`, 'v1 Drawing targets only allow the primary y-axis role.'));
|
|
85
|
+
}
|
|
86
|
+
for (const coordinate of drawingGeometryValues(drawing, path)) {
|
|
87
|
+
let normalized;
|
|
88
|
+
try {
|
|
89
|
+
normalized = normalizeDecimalValue(coordinate.value, axis.valuePrecision);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
issues.push(issue('DRAWING_GEOMETRY_INVALID', coordinate.path, error instanceof Error ? error.message : 'Value normalization failed.'));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (normalized !== coordinate.value || Object.is(coordinate.value, -0)) {
|
|
96
|
+
issues.push(issue('DRAWING_GEOMETRY_INVALID', coordinate.path, `Value must already be normalized to precision ${axis.valuePrecision}.`));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function collectDrawingSemanticIssues(document) {
|
|
101
|
+
const issues = [];
|
|
102
|
+
validateValueAxes(document, issues);
|
|
103
|
+
const ids = new Set();
|
|
104
|
+
for (let index = 0; index < document.drawings.length; index++) {
|
|
105
|
+
const drawing = document.drawings[index];
|
|
106
|
+
if (drawing === undefined) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (ids.has(drawing.id)) {
|
|
110
|
+
issues.push(issue('DRAWING_DUPLICATE_ID', `/drawings/${index}/id`, `Duplicate drawing ID: ${drawing.id}.`));
|
|
111
|
+
}
|
|
112
|
+
ids.add(drawing.id);
|
|
113
|
+
validateDrawing(document, drawing, index, issues);
|
|
114
|
+
}
|
|
115
|
+
return issues;
|
|
116
|
+
}
|
|
117
|
+
export function assertSemanticDrawingDocument(document) {
|
|
118
|
+
const issues = collectDrawingSemanticIssues(document);
|
|
119
|
+
const first = issues[0];
|
|
120
|
+
if (first !== undefined) {
|
|
121
|
+
throw new DrawingDocumentError(first.code, first.path, first.message, issues);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DrawingDocument } from './generated/drawing-document.js';
|
|
2
|
+
export declare function parseStructuralDrawingDocument(value: unknown): DrawingDocument;
|
|
3
|
+
export declare function parseDrawingDocument(value: unknown): DrawingDocument;
|
|
4
|
+
//# sourceMappingURL=drawing-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawing-validator.d.ts","sourceRoot":"","sources":["../src/drawing-validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AA2EvE,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CA8B9E;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAIpE"}
|