@senad-d/observme 0.1.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.
Files changed (111) hide show
  1. package/.env.example +57 -0
  2. package/CHANGELOG.md +52 -0
  3. package/LICENSE +21 -0
  4. package/ObservMe-Production-Docs/00-README.md +79 -0
  5. package/ObservMe-Production-Docs/01-requirements-and-scope.md +207 -0
  6. package/ObservMe-Production-Docs/02-reference-architecture.md +306 -0
  7. package/ObservMe-Production-Docs/03-pi-event-and-session-model.md +266 -0
  8. package/ObservMe-Production-Docs/04-telemetry-semantic-conventions.md +722 -0
  9. package/ObservMe-Production-Docs/05-otel-pipeline-and-collector.md +355 -0
  10. package/ObservMe-Production-Docs/06-security-privacy-redaction.md +294 -0
  11. package/ObservMe-Production-Docs/07-extension-implementation-blueprint.md +447 -0
  12. package/ObservMe-Production-Docs/08-query-grafana-integration.md +276 -0
  13. package/ObservMe-Production-Docs/09-dashboards-alerts-slos.md +640 -0
  14. package/ObservMe-Production-Docs/10-testing-release-operations.md +319 -0
  15. package/ObservMe-Production-Docs/11-deployment-runbooks.md +203 -0
  16. package/ObservMe-Production-Docs/12-configuration-reference.md +337 -0
  17. package/ObservMe-Production-Docs/13-source-notes.md +33 -0
  18. package/ObservMe-Production-Docs/pi-session-format.md +427 -0
  19. package/README.md +356 -0
  20. package/SECURITY.md +45 -0
  21. package/dashboards/observme-agent-node-graphs.json +250 -0
  22. package/dashboards/observme-agents.json +1880 -0
  23. package/dashboards/observme-alerts.yaml +113 -0
  24. package/dashboards/observme-branches-compactions.json +1042 -0
  25. package/dashboards/observme-cost.json +1254 -0
  26. package/dashboards/observme-errors.json +1659 -0
  27. package/dashboards/observme-export-health.json +1802 -0
  28. package/dashboards/observme-latency.json +1494 -0
  29. package/dashboards/observme-llm-conversations.json +730 -0
  30. package/dashboards/observme-logs-llm.json +644 -0
  31. package/dashboards/observme-models.json +933 -0
  32. package/dashboards/observme-overview.json +2129 -0
  33. package/dashboards/observme-slo-health.json +737 -0
  34. package/dashboards/observme-slos.yaml +56 -0
  35. package/dashboards/observme-tools.json +902 -0
  36. package/dashboards/observme-trace-journey.json +1793 -0
  37. package/docs/STRUCTURE.md +49 -0
  38. package/docs/agent-subagent-observability-requirements.md +997 -0
  39. package/docs/compatibility-matrix.md +39 -0
  40. package/docs/configuration-tui-design-standard.md +767 -0
  41. package/docs/configuration.md +38 -0
  42. package/docs/review-validation.md +115 -0
  43. package/docs/validation-flow.md +117 -0
  44. package/examples/collector.yaml +123 -0
  45. package/examples/observme.yaml +131 -0
  46. package/img/demo.gif +0 -0
  47. package/img/icon.svg +47 -0
  48. package/package.json +103 -0
  49. package/src/commands/obs-agents-runtime.ts +150 -0
  50. package/src/commands/obs-agents.ts +491 -0
  51. package/src/commands/obs-args.ts +63 -0
  52. package/src/commands/obs-backfill.ts +1334 -0
  53. package/src/commands/obs-command-support.ts +43 -0
  54. package/src/commands/obs-cost.ts +228 -0
  55. package/src/commands/obs-diagnostics.ts +22 -0
  56. package/src/commands/obs-errors.ts +156 -0
  57. package/src/commands/obs-health.ts +301 -0
  58. package/src/commands/obs-link.ts +90 -0
  59. package/src/commands/obs-logs.ts +194 -0
  60. package/src/commands/obs-loki-summary.ts +184 -0
  61. package/src/commands/obs-session.ts +259 -0
  62. package/src/commands/obs-status.ts +359 -0
  63. package/src/commands/obs-tools.ts +274 -0
  64. package/src/commands/obs-trace.ts +411 -0
  65. package/src/commands/obs.ts +211 -0
  66. package/src/config/bootstrap-project-config.ts +300 -0
  67. package/src/config/defaults.ts +143 -0
  68. package/src/config/load-config.ts +631 -0
  69. package/src/config/project-paths.ts +61 -0
  70. package/src/config/schema.ts +405 -0
  71. package/src/config/validate.ts +456 -0
  72. package/src/constants.ts +4 -0
  73. package/src/diagnostics/sanitize.ts +6 -0
  74. package/src/extension.ts +38 -0
  75. package/src/otel/logs.ts +160 -0
  76. package/src/otel/metrics.ts +165 -0
  77. package/src/otel/otlp-endpoint.ts +10 -0
  78. package/src/otel/sdk.ts +114 -0
  79. package/src/otel/shutdown.ts +102 -0
  80. package/src/otel/traces.ts +166 -0
  81. package/src/pi/agent-lineage.ts +378 -0
  82. package/src/pi/agent-tree-tracker.ts +258 -0
  83. package/src/pi/event-handlers/agent-turn.ts +155 -0
  84. package/src/pi/event-handlers/lifecycle.ts +642 -0
  85. package/src/pi/event-handlers/llm.ts +115 -0
  86. package/src/pi/event-handlers/session-events.ts +159 -0
  87. package/src/pi/event-handlers/tool-bash.ts +275 -0
  88. package/src/pi/handler-internals.ts +2154 -0
  89. package/src/pi/handler-runtime.ts +633 -0
  90. package/src/pi/handler-types.ts +261 -0
  91. package/src/pi/handlers.ts +75 -0
  92. package/src/pi/subagent-spawn.ts +975 -0
  93. package/src/pi/subagent-types.ts +29 -0
  94. package/src/privacy/content-capture.ts +104 -0
  95. package/src/privacy/hash.ts +93 -0
  96. package/src/privacy/redact.ts +619 -0
  97. package/src/privacy/secret-patterns.ts +185 -0
  98. package/src/privacy/truncate.ts +69 -0
  99. package/src/query/grafana-readiness.ts +164 -0
  100. package/src/query/grafana-transport.ts +481 -0
  101. package/src/query/grafana.ts +371 -0
  102. package/src/query/loki.ts +332 -0
  103. package/src/query/prometheus.ts +388 -0
  104. package/src/query/tempo.ts +332 -0
  105. package/src/safety/sensitive-input.ts +208 -0
  106. package/src/semconv/attributes.ts +279 -0
  107. package/src/semconv/metrics.ts +146 -0
  108. package/src/semconv/spans.ts +19 -0
  109. package/src/semconv/values.ts +13 -0
  110. package/src/util/bounded-map.ts +97 -0
  111. package/tsconfig.json +15 -0
@@ -0,0 +1,388 @@
1
+ import type { ObservMeConfig } from "../config/schema.ts";
2
+ import { LOG_ATTRIBUTES } from "../semconv/attributes.ts";
3
+ import type { GrafanaFetch, GrafanaTransportClient } from "./grafana-transport.ts";
4
+ import { createGrafanaTransport } from "./grafana-transport.ts";
5
+ import { assertNoSensitiveQueryInput } from "../safety/sensitive-input.ts";
6
+ import { assertGrafanaQueryReady } from "./grafana-readiness.ts";
7
+
8
+ export type PrometheusFetch = GrafanaFetch;
9
+ export type PrometheusResultLimit = "metricSeries" | "agents";
10
+ export type PrometheusResultType = "vector" | "matrix" | "scalar" | "string" | "unknown";
11
+ type SuccessfulPrometheusResultType = Exclude<PrometheusResultType, "unknown">;
12
+
13
+ export interface PrometheusSample {
14
+ readonly timestampUnixSeconds: string;
15
+ readonly value: string;
16
+ }
17
+
18
+ export interface PrometheusMetricSeries {
19
+ readonly metric: Record<string, string>;
20
+ readonly value?: PrometheusSample;
21
+ readonly values?: readonly PrometheusSample[];
22
+ }
23
+
24
+ export interface QueryResult {
25
+ readonly resultType: PrometheusResultType;
26
+ readonly series: readonly PrometheusMetricSeries[];
27
+ readonly scalar?: PrometheusSample;
28
+ readonly stringValue?: PrometheusSample;
29
+ }
30
+
31
+ export interface PrometheusQueryClientOptions {
32
+ readonly fetch?: PrometheusFetch;
33
+ }
34
+
35
+ export interface PrometheusQueryExecutionOptions {
36
+ readonly resultLimit?: PrometheusResultLimit | number;
37
+ }
38
+
39
+ export interface PrometheusQueryOptions extends PrometheusQueryClientOptions, PrometheusQueryExecutionOptions {}
40
+
41
+ export const FORBIDDEN_HIGH_CARDINALITY_PROMETHEUS_LABELS = [
42
+ "agent_id",
43
+ "agent_run_id",
44
+ "child_agent_id",
45
+ "entry_id",
46
+ "parent_agent_id",
47
+ "raw_command",
48
+ "raw_error_message",
49
+ "raw_path",
50
+ "raw_prompt",
51
+ "session_id",
52
+ LOG_ATTRIBUTES.SPAN_ID,
53
+ "spawn_id",
54
+ "spawn_tool_call_id",
55
+ "tool_call_id",
56
+ LOG_ATTRIBUTES.TRACE_ID,
57
+ "workflow_id",
58
+ "workflow_root_agent_id",
59
+ ] as const;
60
+
61
+ const forbiddenHighCardinalityPrometheusLabelAliases = [
62
+ "pi_agent_child_id",
63
+ "pi_agent_id",
64
+ "pi_agent_parent_id",
65
+ "pi_agent_root_id",
66
+ "pi_agent_run_id",
67
+ "pi_agent_spawn_id",
68
+ "pi_agent_spawn_tool_call_id",
69
+ "pi_entry_id",
70
+ "pi_session_id",
71
+ "pi_tool_call_id",
72
+ "pi_workflow_id",
73
+ "pi_workflow_root_agent_id",
74
+ ] as const;
75
+ const allForbiddenHighCardinalityPrometheusLabels = [
76
+ ...FORBIDDEN_HIGH_CARDINALITY_PROMETHEUS_LABELS,
77
+ ...forbiddenHighCardinalityPrometheusLabelAliases,
78
+ ] as const;
79
+ const minimumMaxMetricSeries = 1;
80
+ const minimumMaxAgents = 1;
81
+ const maxPromQlQueryLength = 4096;
82
+
83
+ export class PrometheusQueryClient {
84
+ readonly #config: ObservMeConfig;
85
+ readonly #transport: GrafanaTransportClient;
86
+
87
+ constructor(config: ObservMeConfig, options: PrometheusQueryClientOptions = {}) {
88
+ this.#config = config;
89
+ this.#transport = createGrafanaTransport(config, options);
90
+ }
91
+
92
+ async queryPrometheus(
93
+ query: string,
94
+ time?: Date,
95
+ options: PrometheusQueryExecutionOptions = {},
96
+ ): Promise<QueryResult> {
97
+ return queryPrometheusWithTransport(this.#config, this.#transport, query, time, options);
98
+ }
99
+ }
100
+
101
+ export function createPrometheusQueryClient(
102
+ config: ObservMeConfig,
103
+ options: PrometheusQueryClientOptions = {},
104
+ ): PrometheusQueryClient {
105
+ return new PrometheusQueryClient(config, options);
106
+ }
107
+
108
+ export async function queryPrometheus(
109
+ config: ObservMeConfig,
110
+ query: string,
111
+ time?: Date,
112
+ options: PrometheusQueryOptions = {},
113
+ ): Promise<QueryResult> {
114
+ return queryPrometheusWithTransport(config, createGrafanaTransport(config, options), query, time, options);
115
+ }
116
+
117
+ async function queryPrometheusWithTransport(
118
+ config: ObservMeConfig,
119
+ transport: GrafanaTransportClient,
120
+ query: string,
121
+ time: Date | undefined,
122
+ options: PrometheusQueryExecutionOptions,
123
+ ): Promise<QueryResult> {
124
+ if (!config.query.enabled) return emptyPrometheusQueryResult();
125
+
126
+ assertGrafanaQueryReady(config, "prometheus");
127
+ const normalizedQuery = normalizePrometheusQuery(query);
128
+ const queryTime = normalizeQueryTime(time);
129
+ const resultLimit = resolveResultLimit(config, options.resultLimit);
130
+ const url = createPrometheusQueryUrl(config, transport, normalizedQuery, queryTime, resultLimit);
131
+ const response = await transport.fetch(url, { timeoutMessage: "Prometheus query timed out." });
132
+
133
+ return readPrometheusQueryResult(response, transport, resultLimit);
134
+ }
135
+
136
+ export function findForbiddenPrometheusLabels(query: string): string[] {
137
+ return allForbiddenHighCardinalityPrometheusLabels.filter(label => containsPrometheusIdentifier(query, label));
138
+ }
139
+
140
+ function normalizePrometheusQuery(query: string): string {
141
+ const trimmed = query.trim();
142
+ if (!trimmed) throw new Error("Prometheus query requires a non-empty PromQL query.");
143
+ if (trimmed.length > maxPromQlQueryLength) throw new Error("Prometheus query is bounded to 4096 characters.");
144
+
145
+ assertNoSensitiveQueryInput(trimmed, "Prometheus query");
146
+ assertNoForbiddenHighCardinalityLabels(trimmed);
147
+ return trimmed;
148
+ }
149
+
150
+ function normalizeQueryTime(time: Date | undefined): Date | undefined {
151
+ if (time === undefined) return undefined;
152
+ if (!(time instanceof Date) || !Number.isFinite(time.getTime())) {
153
+ throw new TypeError("Prometheus query time must be a valid Date when provided.");
154
+ }
155
+
156
+ return time;
157
+ }
158
+
159
+ function assertNoForbiddenHighCardinalityLabels(query: string): void {
160
+ const labels = findForbiddenPrometheusLabels(query);
161
+ if (labels.length === 0) return;
162
+
163
+ throw new Error(`Unsafe Prometheus query: forbidden high-cardinality metric labels are not allowed: ${labels.join(", ")}.`);
164
+ }
165
+
166
+ function containsPrometheusIdentifier(query: string, identifier: string): boolean {
167
+ return createPrometheusIdentifierPattern(identifier).test(query);
168
+ }
169
+
170
+ function createPrometheusIdentifierPattern(identifier: string): RegExp {
171
+ return new RegExp(`(?<![A-Za-z0-9_])${escapeRegExp(identifier)}(?![A-Za-z0-9_])`, "u");
172
+ }
173
+
174
+ function escapeRegExp(value: string): string {
175
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
176
+ }
177
+
178
+ function createPrometheusQueryUrl(
179
+ config: ObservMeConfig,
180
+ transport: GrafanaTransportClient,
181
+ query: string,
182
+ time: Date | undefined,
183
+ resultLimit: number,
184
+ ): URL {
185
+ const url = transport.datasourceProxyUrl(config.query.grafana.datasourceUids.prometheus, "/api/v1/query");
186
+
187
+ url.searchParams.set("query", query);
188
+ url.searchParams.set("limit", String(resultLimit));
189
+ url.searchParams.set("timeout", formatPrometheusDuration(transport.timeoutMs));
190
+ if (time) url.searchParams.set("time", formatPrometheusTimestamp(time));
191
+ return url;
192
+ }
193
+
194
+ async function readPrometheusQueryResult(
195
+ response: Response,
196
+ transport: GrafanaTransportClient,
197
+ resultLimit: number,
198
+ ): Promise<QueryResult> {
199
+ if (!response.ok) throw new Error(`Prometheus query failed: ${transport.formatHttpFailure(response)}`);
200
+
201
+ const payload = (await response.json()) as unknown;
202
+ const apiError = readPrometheusApiError(payload);
203
+ if (apiError) throw new Error(`Prometheus query failed: ${apiError}`);
204
+ return extractPrometheusQueryResult(payload, resultLimit);
205
+ }
206
+
207
+ function readPrometheusApiError(payload: unknown): string | undefined {
208
+ if (!isRecord(payload) || payload.status !== "error") return undefined;
209
+
210
+ const errorType = readOptionalString(payload, "errorType");
211
+ const errorMessage = readOptionalString(payload, "error");
212
+ return [errorType, errorMessage].filter(isNonEmptyString).join(": ") || "unknown Prometheus API error";
213
+ }
214
+
215
+ function createPrometheusSchemaError(reason: string): Error {
216
+ return new Error(`Prometheus query failed: backend schema error: ${reason}.`);
217
+ }
218
+
219
+ function extractPrometheusQueryResult(payload: unknown, resultLimit: number): QueryResult {
220
+ const data = readPrometheusSuccessData(payload);
221
+ const resultType = readPrometheusResultType(data.resultType);
222
+
223
+ if (resultType === "scalar") return extractPrometheusScalarResult(data.result, resultType);
224
+ if (resultType === "string") return extractPrometheusStringResult(data.result, resultType);
225
+
226
+ const series = extractPrometheusMetricSeries(data.result, resultType).slice(0, resultLimit);
227
+ return { resultType, series };
228
+ }
229
+
230
+ function readPrometheusSuccessData(payload: unknown): Record<string, unknown> {
231
+ if (!isRecord(payload)) throw createPrometheusSchemaError("response must be a JSON object");
232
+ if (payload.status !== "success") throw createPrometheusSchemaError("status must be success");
233
+ if (!isRecord(payload.data)) throw createPrometheusSchemaError("data must be an object");
234
+ return payload.data;
235
+ }
236
+
237
+ function readPrometheusResultType(value: unknown): SuccessfulPrometheusResultType {
238
+ if (value === "vector" || value === "matrix" || value === "scalar" || value === "string") return value;
239
+ throw createPrometheusSchemaError("data.resultType must be vector, matrix, scalar, or string");
240
+ }
241
+
242
+ function extractPrometheusScalarResult(result: unknown, resultType: "scalar"): QueryResult {
243
+ const scalar = toPrometheusSample(result);
244
+ if (!scalar) throw createPrometheusSchemaError("data.result must be a valid scalar sample");
245
+ return { resultType, series: [], scalar };
246
+ }
247
+
248
+ function extractPrometheusStringResult(result: unknown, resultType: "string"): QueryResult {
249
+ const stringValue = toPrometheusSample(result);
250
+ if (!stringValue) throw createPrometheusSchemaError("data.result must be a valid string sample");
251
+ return { resultType, series: [], stringValue };
252
+ }
253
+
254
+ function extractPrometheusMetricSeries(
255
+ result: unknown,
256
+ resultType: "vector" | "matrix",
257
+ ): PrometheusMetricSeries[] {
258
+ if (!Array.isArray(result)) throw createPrometheusSchemaError("data.result must be an array for vector or matrix results");
259
+ return result.map(item => toPrometheusMetricSeries(item, resultType));
260
+ }
261
+
262
+ function toPrometheusMetricSeries(item: unknown, resultType: "vector" | "matrix"): PrometheusMetricSeries {
263
+ if (!isRecord(item)) throw createPrometheusSchemaError("each data.result item must be an object");
264
+
265
+ const metric = readPrometheusMetricLabels(item.metric);
266
+ if (resultType === "vector") return createPrometheusVectorSeries(metric, item.value);
267
+ return createPrometheusMatrixSeries(metric, item.values);
268
+ }
269
+
270
+ function readPrometheusMetricLabels(value: unknown): Record<string, string> {
271
+ if (!isRecord(value)) throw createPrometheusSchemaError("each data.result item metric must be an object");
272
+ return readStringRecord(value);
273
+ }
274
+
275
+ function createPrometheusVectorSeries(metric: Record<string, string>, value: unknown): PrometheusMetricSeries {
276
+ const sample = toPrometheusSample(value);
277
+ if (!sample) throw createPrometheusSchemaError("each vector data.result item must include a valid value sample");
278
+ return { metric, value: sample };
279
+ }
280
+
281
+ function createPrometheusMatrixSeries(metric: Record<string, string>, values: unknown): PrometheusMetricSeries {
282
+ const samples = toPrometheusSamples(values);
283
+ if (samples.length === 0) throw createPrometheusSchemaError("each matrix data.result item must include valid values samples");
284
+ return { metric, values: samples };
285
+ }
286
+
287
+ function toPrometheusSamples(values: unknown): PrometheusSample[] {
288
+ if (!Array.isArray(values)) throw createPrometheusSchemaError("matrix data.result item values must be an array");
289
+ return values.map(toPrometheusSampleStrict);
290
+ }
291
+
292
+ function toPrometheusSampleStrict(value: unknown): PrometheusSample {
293
+ const sample = toPrometheusSample(value);
294
+ if (!sample) throw createPrometheusSchemaError("Prometheus samples must be [timestamp, value] pairs");
295
+ return sample;
296
+ }
297
+
298
+ function toPrometheusSample(value: unknown): PrometheusSample | undefined {
299
+ if (!Array.isArray(value) || value.length < 2) return undefined;
300
+
301
+ const timestampUnixSeconds = readStringOrNumber(value[0]);
302
+ const sampleValue = readStringOrNumber(value[1]);
303
+ if (timestampUnixSeconds === undefined || sampleValue === undefined) return undefined;
304
+
305
+ return { timestampUnixSeconds, value: sampleValue };
306
+ }
307
+
308
+ function readStringRecord(value: unknown): Record<string, string> {
309
+ const record: Record<string, string> = {};
310
+ if (!isRecord(value)) return record;
311
+
312
+ for (const [key, item] of Object.entries(value)) {
313
+ const text = readStringOrNumber(item);
314
+ if (text !== undefined) record[key] = text;
315
+ }
316
+
317
+ return record;
318
+ }
319
+
320
+ function readOptionalString(item: Record<string, unknown>, key: string): string | undefined {
321
+ const value = item[key];
322
+ if (typeof value !== "string") return undefined;
323
+
324
+ const trimmed = value.trim();
325
+ return trimmed || undefined;
326
+ }
327
+
328
+ function readStringOrNumber(value: unknown): string | undefined {
329
+ if (typeof value === "string") return value;
330
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
331
+ return undefined;
332
+ }
333
+
334
+ function isNonEmptyString(value: string | undefined): value is string {
335
+ return value !== undefined;
336
+ }
337
+
338
+ function isRecord(value: unknown): value is Record<string, unknown> {
339
+ return typeof value === "object" && value !== null && !Array.isArray(value);
340
+ }
341
+
342
+ function emptyPrometheusQueryResult(): QueryResult {
343
+ return { resultType: "vector", series: [] };
344
+ }
345
+
346
+ function resolveResultLimit(config: ObservMeConfig, limit: PrometheusResultLimit | number | undefined): number {
347
+ if (limit === "agents") return resolveMaxAgents(config);
348
+ if (typeof limit === "number") return normalizeExplicitResultLimit(limit);
349
+ return resolveMaxMetricSeries(config);
350
+ }
351
+
352
+ function normalizeExplicitResultLimit(limit: number): number {
353
+ if (!Number.isFinite(limit) || limit < minimumMaxMetricSeries) {
354
+ throw new Error("Prometheus result limit must be a positive finite number.");
355
+ }
356
+
357
+ return Math.trunc(limit);
358
+ }
359
+
360
+ function resolveMaxMetricSeries(config: ObservMeConfig): number {
361
+ const maxMetricSeries = config.query.maxMetricSeries;
362
+ if (!Number.isFinite(maxMetricSeries) || maxMetricSeries < minimumMaxMetricSeries) return minimumMaxMetricSeries;
363
+ return Math.trunc(maxMetricSeries);
364
+ }
365
+
366
+ function resolveMaxAgents(config: ObservMeConfig): number {
367
+ const maxAgents = config.query.maxAgents;
368
+ if (!Number.isFinite(maxAgents) || maxAgents < minimumMaxAgents) return minimumMaxAgents;
369
+ return Math.trunc(maxAgents);
370
+ }
371
+
372
+ function formatPrometheusTimestamp(date: Date): string {
373
+ return trimTrailingFractionZeros((date.getTime() / 1000).toFixed(3));
374
+ }
375
+
376
+ function formatPrometheusDuration(milliseconds: number): string {
377
+ return `${trimTrailingFractionZeros((milliseconds / 1000).toFixed(3))}s`;
378
+ }
379
+
380
+ function trimTrailingFractionZeros(value: string): string {
381
+ if (!value.includes(".")) return value;
382
+
383
+ let end = value.length;
384
+ while (end > 0 && value[end - 1] === "0") end -= 1;
385
+
386
+ const withoutTrailingZeros = value.slice(0, end);
387
+ return withoutTrailingZeros.endsWith(".") ? withoutTrailingZeros.slice(0, -1) : withoutTrailingZeros;
388
+ }