agent-first-data 0.35.1 → 0.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/assets/cli-spec-v1.schema.json +5 -2
- package/dist/cli.d.ts +12 -2
- package/dist/cli.js +43 -4
- package/dist/format.d.ts +13 -7
- package/dist/format.js +308 -107
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -17,10 +17,12 @@ console.log(render(event.toJSON(), "json"));
|
|
|
17
17
|
console.log(render(event.toJSON(), "plain"));
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Useful names use TypeScript casing: `render`, `outputOptionsForPolicy`, `redactedValue`, `redactUrlSecrets`, `redactUrlsInText`, `redactArgv`, `normalizeUtcOffset`, `isValidRfc3339Date`, `isValidRfc3339Time`, `isValidRfc3339`, `isValidBcp47`, `decodeProtocolEvent`, `cliParseOutput`, `cliParseLogFilters`, `buildCliError`, and `
|
|
20
|
+
Useful names use TypeScript casing: `render`, `outputOptionsForPolicy`, `redactedValue`, `redactUrlSecrets`, `redactUrlsInText`, `redactArgv`, `normalizeUtcOffset`, `isValidRfc3339Date`, `isValidRfc3339Time`, `isValidRfc3339`, `isValidBcp47`, `decodeProtocolEvent`, `cliParseOutput`, `cliParseLogFilters`, `buildCliError`, `buildCliVersion`, and `CliEmitter`.
|
|
21
21
|
|
|
22
22
|
## Behavior Notes
|
|
23
23
|
|
|
24
|
+
- `OutputOptions.redaction.revealNames` reveals exact structured field names at the selected output boundary. Children still pass through redaction; URL parameters and argv receive no exemption.
|
|
25
|
+
- The protocol reader accepts base-profile events. An absent `error.retryable` remains nullable; strict validation is a separate opt-in.
|
|
24
26
|
- Default redaction replaces every `_secret` or configured secret-name subtree with `***`, including objects and arrays.
|
|
25
27
|
- `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed, and internal whitespace or a `://` that is not the start of a clean URL redacts the whole field, where whitespace is the explicit Unicode `White_Space` set the spec enumerates, not the language's native predicate.
|
|
26
28
|
- `OutputOptions.redaction.urlNames` applies URL treatment to exact legacy field names
|
|
@@ -29,7 +31,7 @@ Useful names use TypeScript casing: `render`, `outputOptionsForPolicy`, `redacte
|
|
|
29
31
|
- YAML keeps original keys and values (structure-preserving, like JSON), sorting keys by UTF-16 code unit order and quoting/escaping unsafe keys and string scalars. Plain strips formatting suffixes, formats values, sorts the same way, and renders nested objects/arrays as canonical JSON; it quotes a value only when it contains ASCII space, tab, newline, carriage return, form feed, vertical tab, NBSP, `=`, `"`, or `\`, and escapes `\`, `"`, newline, carriage return, tab, form feed, and vertical tab inside the quotes.
|
|
30
32
|
- Logging records use `kind:"log"` with a nested `log` payload and a separate `level` field, so error-level logs are not terminal protocol errors.
|
|
31
33
|
- `buildCliError(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
|
|
32
|
-
- Do not add a raw version/help pre-parser.
|
|
34
|
+
- Do not add a raw version/help pre-parser. This SDK provides emitters and readers; argv and lifecycle parsing belong to the application. Claim help-v2 or closed-world compatibility only when using a registry compiler.
|
|
33
35
|
|
|
34
36
|
## Reference
|
|
35
37
|
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"exit_codes": {
|
|
46
46
|
"type": "array",
|
|
47
|
-
"description": "Exit codes this CLI returns beyond AFDATA's 0/1/2, rendered into the reference's exit-code table.
|
|
47
|
+
"description": "Exit codes this CLI returns beyond AFDATA's 0/1/2/4, rendered into the reference's exit-code table. AFDATA's codes may not be redeclared.",
|
|
48
48
|
"items": {
|
|
49
49
|
"type": "object",
|
|
50
50
|
"additionalProperties": false,
|
|
@@ -56,7 +56,10 @@
|
|
|
56
56
|
"code": {
|
|
57
57
|
"type": "integer",
|
|
58
58
|
"minimum": 3,
|
|
59
|
-
"maximum": 255
|
|
59
|
+
"maximum": 255,
|
|
60
|
+
"not": {
|
|
61
|
+
"const": 4
|
|
62
|
+
}
|
|
60
63
|
},
|
|
61
64
|
"meaning": {
|
|
62
65
|
"$ref": "#/$defs/nonEmpty"
|
package/dist/cli.d.ts
CHANGED
|
@@ -108,6 +108,9 @@ export declare class CliEmitter {
|
|
|
108
108
|
private terminalEmitted;
|
|
109
109
|
private diagnostic?;
|
|
110
110
|
private strictProtocol;
|
|
111
|
+
private writeFailed;
|
|
112
|
+
/** Where an event-stream emitter reports a failed write; tests replace it. */
|
|
113
|
+
private processStderr;
|
|
111
114
|
/**
|
|
112
115
|
* Create an event-stream emitter: every event, including `error`, goes to the
|
|
113
116
|
* single `writer`. Use CliEmitter.finite for a one-shot command that should
|
|
@@ -154,10 +157,17 @@ export declare class CliEmitter {
|
|
|
154
157
|
* exit code, so a one-shot CLI need not hand-roll the emit-then-exit dance.
|
|
155
158
|
*
|
|
156
159
|
* A successful write returns `successCode`; a broken pipe (the reader hung up)
|
|
157
|
-
* returns `0
|
|
158
|
-
*
|
|
160
|
+
* returns `0` and says nothing. Any other write failure returns `4` after
|
|
161
|
+
* reporting `output_write_failed` as an `error` event: on the diagnostic
|
|
162
|
+
* writer in finite mode, on the process's stderr in event-stream mode, whose
|
|
163
|
+
* only writer is the one that just failed. That report is best effort. An
|
|
164
|
+
* event the emitter refuses (a protocol violation or a second terminal
|
|
165
|
+
* event) is the command's own failure, not an output one, and returns `1`.
|
|
166
|
+
* A library never calls process.exit itself — return this code and let the
|
|
167
|
+
* caller exit.
|
|
159
168
|
*/
|
|
160
169
|
finish(event: Event, successCode: number): number;
|
|
170
|
+
private reportOutputWriteFailed;
|
|
161
171
|
/**
|
|
162
172
|
* Convenience over finish: emit a `result` payload and return `0` on success.
|
|
163
173
|
*
|
package/dist/cli.js
CHANGED
|
@@ -156,6 +156,9 @@ export class CliEmitter {
|
|
|
156
156
|
terminalEmitted = false;
|
|
157
157
|
diagnostic;
|
|
158
158
|
strictProtocol = false;
|
|
159
|
+
writeFailed = false;
|
|
160
|
+
/** Where an event-stream emitter reports a failed write; tests replace it. */
|
|
161
|
+
processStderr = processStderrWriter;
|
|
159
162
|
/**
|
|
160
163
|
* Create an event-stream emitter: every event, including `error`, goes to the
|
|
161
164
|
* single `writer`. Use CliEmitter.finite for a one-shot command that should
|
|
@@ -217,6 +220,7 @@ export class CliEmitter {
|
|
|
217
220
|
return this;
|
|
218
221
|
}
|
|
219
222
|
emit(event) {
|
|
223
|
+
this.writeFailed = false;
|
|
220
224
|
const jsonValue = event.toJSON();
|
|
221
225
|
validateProtocolEvent(jsonValue, this.strictProtocol);
|
|
222
226
|
const kind = jsonValue.kind;
|
|
@@ -233,7 +237,15 @@ export class CliEmitter {
|
|
|
233
237
|
else {
|
|
234
238
|
throw new Error(`unsupported event kind ${JSON.stringify(kind)}`);
|
|
235
239
|
}
|
|
236
|
-
|
|
240
|
+
const line = `${render(jsonValue, this.format, this.outputOptions)}\n`;
|
|
241
|
+
try {
|
|
242
|
+
this.sinkFor(kind)(line);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
// Marks the failure as output's, not the event's, for finish().
|
|
246
|
+
this.writeFailed = true;
|
|
247
|
+
throw err;
|
|
248
|
+
}
|
|
237
249
|
if (kind === "result" || kind === "error")
|
|
238
250
|
this.terminalEmitted = true;
|
|
239
251
|
}
|
|
@@ -265,8 +277,14 @@ export class CliEmitter {
|
|
|
265
277
|
* exit code, so a one-shot CLI need not hand-roll the emit-then-exit dance.
|
|
266
278
|
*
|
|
267
279
|
* A successful write returns `successCode`; a broken pipe (the reader hung up)
|
|
268
|
-
* returns `0
|
|
269
|
-
*
|
|
280
|
+
* returns `0` and says nothing. Any other write failure returns `4` after
|
|
281
|
+
* reporting `output_write_failed` as an `error` event: on the diagnostic
|
|
282
|
+
* writer in finite mode, on the process's stderr in event-stream mode, whose
|
|
283
|
+
* only writer is the one that just failed. That report is best effort. An
|
|
284
|
+
* event the emitter refuses (a protocol violation or a second terminal
|
|
285
|
+
* event) is the command's own failure, not an output one, and returns `1`.
|
|
286
|
+
* A library never calls process.exit itself — return this code and let the
|
|
287
|
+
* caller exit.
|
|
270
288
|
*/
|
|
271
289
|
finish(event, successCode) {
|
|
272
290
|
try {
|
|
@@ -274,7 +292,28 @@ export class CliEmitter {
|
|
|
274
292
|
return successCode;
|
|
275
293
|
}
|
|
276
294
|
catch (err) {
|
|
277
|
-
|
|
295
|
+
if (CliEmitter.isBrokenPipe(err))
|
|
296
|
+
return 0;
|
|
297
|
+
if (!this.writeFailed)
|
|
298
|
+
return 1;
|
|
299
|
+
this.reportOutputWriteFailed();
|
|
300
|
+
return 4;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
reportOutputWriteFailed() {
|
|
304
|
+
try {
|
|
305
|
+
const event = jsonError("output_write_failed", "could not write command output").build();
|
|
306
|
+
if (this.diagnostic) {
|
|
307
|
+
this.emit(event);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
// Event-stream mode has no diagnostic writer, so the report goes to
|
|
311
|
+
// the process's stderr through an emitter of its own.
|
|
312
|
+
CliEmitter.stream(this.processStderr, this.format).withStrictProtocol().emit(event);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Best effort: an unwritable stderr leaves only the exit code.
|
|
278
317
|
}
|
|
279
318
|
}
|
|
280
319
|
/**
|
package/dist/format.d.ts
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* Number literal fidelity: decodeProtocolEvent parses with `lossless-json`
|
|
11
11
|
* instead of JSON.parse, so a number's exact source literal (arbitrary
|
|
12
12
|
* precision integers, high-precision floats, exponent forms, "-0") survives
|
|
13
|
-
* decode. Parsed numbers become LosslessNumber internally; every
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* decode. Parsed numbers become LosslessNumber internally; every rendering
|
|
14
|
+
* path here treats it as an opaque scalar leaf (never destructured as a plain
|
|
15
|
+
* object). JSON re-emits its exact literal text; YAML/plain render it in the
|
|
16
|
+
* spec's canonical number form without losing a digit. Native bigint values
|
|
16
17
|
* are also supported so callers can render integers beyond Number's safe
|
|
17
18
|
* range without first corrupting them.
|
|
18
19
|
*/
|
|
@@ -22,7 +23,8 @@ export type JsonValue = string | number | bigint | boolean | null | JsonValue[]
|
|
|
22
23
|
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
23
24
|
/**
|
|
24
25
|
* Event is an opaque type wrapping a protocol event envelope.
|
|
25
|
-
*
|
|
26
|
+
* Non-error builders retain caller-provided traces verbatim; validate the
|
|
27
|
+
* envelope before accepting externally supplied traces as protocol-valid.
|
|
26
28
|
*/
|
|
27
29
|
export declare class Event {
|
|
28
30
|
private readonly value;
|
|
@@ -100,7 +102,7 @@ export type DecodedError = {
|
|
|
100
102
|
kind: "error";
|
|
101
103
|
code: string;
|
|
102
104
|
message: string;
|
|
103
|
-
retryable: boolean;
|
|
105
|
+
retryable: boolean | null;
|
|
104
106
|
hint?: string;
|
|
105
107
|
fields: Record<string, JsonValue>;
|
|
106
108
|
trace?: Record<string, JsonValue>;
|
|
@@ -117,12 +119,12 @@ export type DecodedLog = {
|
|
|
117
119
|
};
|
|
118
120
|
/** Discriminated union of decoded protocol events, narrow on `kind`. */
|
|
119
121
|
export type DecodedEvent = DecodedResult | DecodedError | DecodedProgress | DecodedLog;
|
|
120
|
-
/** Thrown by decodeProtocolEvent for malformed JSON or a
|
|
122
|
+
/** Thrown by decodeProtocolEvent for malformed JSON or a base-invalid event. */
|
|
121
123
|
export declare class EventDecodeError extends Error {
|
|
122
124
|
constructor(message: string);
|
|
123
125
|
}
|
|
124
126
|
/**
|
|
125
|
-
* Parse one protocol line (a single JSON text value),
|
|
127
|
+
* Parse one protocol line (a single JSON text value), base-validate it, and
|
|
126
128
|
* return a typed decoded event.
|
|
127
129
|
*/
|
|
128
130
|
export declare function decodeProtocolEvent(text: string): DecodedEvent;
|
|
@@ -158,6 +160,8 @@ export type OutputOptions = {
|
|
|
158
160
|
* redaction in addition to _url/_URL fields.
|
|
159
161
|
*/
|
|
160
162
|
urlNames?: readonly string[];
|
|
163
|
+
/** Exact structured field names to reveal; URL parameters and argv stay redacted. */
|
|
164
|
+
revealNames?: readonly string[];
|
|
161
165
|
};
|
|
162
166
|
/** Rendering style for plain (logfmt) output only. Omitted means readable.
|
|
163
167
|
* JSON and YAML are structure-preserving and ignore this setting. */
|
|
@@ -191,6 +195,8 @@ export declare function redactedValue(value: unknown, options?: {
|
|
|
191
195
|
policy?: RedactionPolicy;
|
|
192
196
|
secretNames?: readonly string[];
|
|
193
197
|
urlNames?: readonly string[];
|
|
198
|
+
/** Exact structured field names to reveal; URL parameters and argv stay redacted. */
|
|
199
|
+
revealNames?: readonly string[];
|
|
194
200
|
}): JsonValue;
|
|
195
201
|
/**
|
|
196
202
|
* Redact secret components of a single URL string.
|
package/dist/format.js
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* Number literal fidelity: decodeProtocolEvent parses with `lossless-json`
|
|
11
11
|
* instead of JSON.parse, so a number's exact source literal (arbitrary
|
|
12
12
|
* precision integers, high-precision floats, exponent forms, "-0") survives
|
|
13
|
-
* decode. Parsed numbers become LosslessNumber internally; every
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* decode. Parsed numbers become LosslessNumber internally; every rendering
|
|
14
|
+
* path here treats it as an opaque scalar leaf (never destructured as a plain
|
|
15
|
+
* object). JSON re-emits its exact literal text; YAML/plain render it in the
|
|
16
|
+
* spec's canonical number form without losing a digit. Native bigint values
|
|
16
17
|
* are also supported so callers can render integers beyond Number's safe
|
|
17
18
|
* range without first corrupting them.
|
|
18
19
|
*/
|
|
@@ -22,7 +23,8 @@ import { parse as losslessParse, stringify as losslessStringify, isLosslessNumbe
|
|
|
22
23
|
// ═══════════════════════════════════════════
|
|
23
24
|
/**
|
|
24
25
|
* Event is an opaque type wrapping a protocol event envelope.
|
|
25
|
-
*
|
|
26
|
+
* Non-error builders retain caller-provided traces verbatim; validate the
|
|
27
|
+
* envelope before accepting externally supplied traces as protocol-valid.
|
|
26
28
|
*/
|
|
27
29
|
export class Event {
|
|
28
30
|
value;
|
|
@@ -113,7 +115,7 @@ export class JsonErrorBuilder {
|
|
|
113
115
|
this.issues.push(`cannot write reserved error field ${JSON.stringify(name)}`);
|
|
114
116
|
}
|
|
115
117
|
else {
|
|
116
|
-
this.extensionFields
|
|
118
|
+
setJsonField(this.extensionFields, name, value);
|
|
117
119
|
}
|
|
118
120
|
return this;
|
|
119
121
|
}
|
|
@@ -127,7 +129,7 @@ export class JsonErrorBuilder {
|
|
|
127
129
|
this.issues.push(`cannot write reserved error field ${JSON.stringify(key)}`);
|
|
128
130
|
}
|
|
129
131
|
else {
|
|
130
|
-
this.extensionFields
|
|
132
|
+
setJsonField(this.extensionFields, key, val);
|
|
131
133
|
}
|
|
132
134
|
}
|
|
133
135
|
return this;
|
|
@@ -142,7 +144,7 @@ export class JsonErrorBuilder {
|
|
|
142
144
|
this.issues.push(`cannot write reserved error field ${JSON.stringify(key)}`);
|
|
143
145
|
}
|
|
144
146
|
else {
|
|
145
|
-
this.extensionFields
|
|
147
|
+
setJsonField(this.extensionFields, key, val);
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
return this;
|
|
@@ -277,6 +279,9 @@ function validateErrorPayload(error) {
|
|
|
277
279
|
if ("hint" in error && typeof error.hint !== "string") {
|
|
278
280
|
throw new Error("event.error.hint must be a string when present");
|
|
279
281
|
}
|
|
282
|
+
if ("retryable" in error && typeof error.retryable !== "boolean") {
|
|
283
|
+
throw new Error("event.error.retryable must be a boolean when present");
|
|
284
|
+
}
|
|
280
285
|
}
|
|
281
286
|
/** Validate finite CLI lifecycle: (log | progress)* -> exactly one terminal. `strict` also enforces the recommended strict protocol profile (default true). */
|
|
282
287
|
export function validateProtocolStream(events, strict = true) {
|
|
@@ -319,7 +324,7 @@ function validateStrictError(error) {
|
|
|
319
324
|
throw new Error("event.error.hint must be a string when present in strict mode");
|
|
320
325
|
}
|
|
321
326
|
}
|
|
322
|
-
/** Thrown by decodeProtocolEvent for malformed JSON or a
|
|
327
|
+
/** Thrown by decodeProtocolEvent for malformed JSON or a base-invalid event. */
|
|
323
328
|
export class EventDecodeError extends Error {
|
|
324
329
|
constructor(message) {
|
|
325
330
|
super(message);
|
|
@@ -327,8 +332,70 @@ export class EventDecodeError extends Error {
|
|
|
327
332
|
}
|
|
328
333
|
}
|
|
329
334
|
const ERROR_RESERVED_FIELDS = new Set(["code", "message", "hint", "retryable"]);
|
|
335
|
+
/** How many arrays and objects a protocol line may nest, counting the envelope object. Every SDK enforces the same bound. */
|
|
336
|
+
const MAX_PROTOCOL_NESTING = 127;
|
|
337
|
+
function hexEscape(text, at) {
|
|
338
|
+
if (text.charCodeAt(at) !== 0x5c || text.charCodeAt(at + 1) !== 0x75)
|
|
339
|
+
return null;
|
|
340
|
+
const digits = text.slice(at + 2, at + 6);
|
|
341
|
+
return /^[0-9a-fA-F]{4}$/.test(digits) ? parseInt(digits, 16) : null;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* The decode rules lossless-json does not enforce: a nesting bound, and no
|
|
345
|
+
* lone UTF-16 surrogate, escaped or raw (lossless-json keeps one in the
|
|
346
|
+
* string). Only strings and escapes are tracked; the rest of the grammar is
|
|
347
|
+
* left to the parser.
|
|
348
|
+
*/
|
|
349
|
+
function checkProtocolLine(text) {
|
|
350
|
+
let depth = 0;
|
|
351
|
+
let inString = false;
|
|
352
|
+
for (let i = 0; i < text.length; i++) {
|
|
353
|
+
const c = text.charCodeAt(i);
|
|
354
|
+
if (inString) {
|
|
355
|
+
if (c === 0x22) {
|
|
356
|
+
inString = false;
|
|
357
|
+
}
|
|
358
|
+
else if (c === 0x5c) {
|
|
359
|
+
const unit = hexEscape(text, i);
|
|
360
|
+
if (unit !== null && unit >= 0xd800 && unit <= 0xdbff) {
|
|
361
|
+
const low = hexEscape(text, i + 6);
|
|
362
|
+
if (low === null || low < 0xdc00 || low > 0xdfff) {
|
|
363
|
+
throw new SyntaxError(`lone surrogate escape at position ${i}`);
|
|
364
|
+
}
|
|
365
|
+
i += 11;
|
|
366
|
+
}
|
|
367
|
+
else if (unit !== null && unit >= 0xdc00 && unit <= 0xdfff) {
|
|
368
|
+
throw new SyntaxError(`lone surrogate escape at position ${i}`);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
i++;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else if (c >= 0xd800 && c <= 0xdbff) {
|
|
375
|
+
const next = text.charCodeAt(i + 1);
|
|
376
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
377
|
+
throw new SyntaxError(`lone surrogate at position ${i}`);
|
|
378
|
+
i++;
|
|
379
|
+
}
|
|
380
|
+
else if (c >= 0xdc00 && c <= 0xdfff) {
|
|
381
|
+
throw new SyntaxError(`lone surrogate at position ${i}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else if (c === 0x22) {
|
|
385
|
+
inString = true;
|
|
386
|
+
}
|
|
387
|
+
else if (c === 0x5b || c === 0x7b) {
|
|
388
|
+
depth++;
|
|
389
|
+
if (depth > MAX_PROTOCOL_NESTING)
|
|
390
|
+
throw new SyntaxError(`nesting deeper than ${MAX_PROTOCOL_NESTING}`);
|
|
391
|
+
}
|
|
392
|
+
else if (c === 0x5d || c === 0x7d) {
|
|
393
|
+
depth--;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
330
397
|
/**
|
|
331
|
-
* Parse one protocol line (a single JSON text value),
|
|
398
|
+
* Parse one protocol line (a single JSON text value), base-validate it, and
|
|
332
399
|
* return a typed decoded event.
|
|
333
400
|
*/
|
|
334
401
|
export function decodeProtocolEvent(text) {
|
|
@@ -338,13 +405,16 @@ export function decodeProtocolEvent(text) {
|
|
|
338
405
|
// source literal (see module-level fidelity note) instead of collapsing
|
|
339
406
|
// it through float64. isSyntaxError-compatible: throws the same
|
|
340
407
|
// SyntaxError shape JSON.parse would.
|
|
341
|
-
|
|
408
|
+
checkProtocolLine(text);
|
|
409
|
+
// A repeated key keeps its last value, as JSON.parse and every other
|
|
410
|
+
// SDK's decoder do; lossless-json would otherwise throw.
|
|
411
|
+
parsed = losslessParse(text, null, { onDuplicateKey: ({ newValue }) => newValue });
|
|
342
412
|
}
|
|
343
413
|
catch (e) {
|
|
344
414
|
throw new EventDecodeError(`invalid JSON: ${e.message}`);
|
|
345
415
|
}
|
|
346
416
|
try {
|
|
347
|
-
validateProtocolEvent(parsed,
|
|
417
|
+
validateProtocolEvent(parsed, false);
|
|
348
418
|
}
|
|
349
419
|
catch (e) {
|
|
350
420
|
throw new EventDecodeError(e.message);
|
|
@@ -359,13 +429,13 @@ export function decodeProtocolEvent(text) {
|
|
|
359
429
|
const fields = {};
|
|
360
430
|
for (const [k, v] of Object.entries(error)) {
|
|
361
431
|
if (!ERROR_RESERVED_FIELDS.has(k))
|
|
362
|
-
fields
|
|
432
|
+
setJsonField(fields, k, v);
|
|
363
433
|
}
|
|
364
434
|
const decoded = {
|
|
365
435
|
kind: "error",
|
|
366
436
|
code: error.code,
|
|
367
437
|
message: error.message,
|
|
368
|
-
retryable: error.retryable,
|
|
438
|
+
retryable: typeof error.retryable === "boolean" ? error.retryable : null,
|
|
369
439
|
fields,
|
|
370
440
|
trace,
|
|
371
441
|
};
|
|
@@ -443,8 +513,12 @@ export function formatJsonValue(value, options = {}) {
|
|
|
443
513
|
* and formatting for everything else.
|
|
444
514
|
*/
|
|
445
515
|
function canonicalJsonString(value) {
|
|
446
|
-
return losslessStringify(value) ?? "null";
|
|
516
|
+
return losslessStringify(value, null, undefined, NUMBER_STRINGIFIERS) ?? "null";
|
|
447
517
|
}
|
|
518
|
+
// JSON.stringify spells negative zero "0"; the other SDKs keep its sign.
|
|
519
|
+
const NUMBER_STRINGIFIERS = [
|
|
520
|
+
{ test: (value) => Object.is(value, -0), stringify: () => "-0" },
|
|
521
|
+
];
|
|
448
522
|
/**
|
|
449
523
|
* Format as multi-line YAML. Structure-preserving: like JSON, original keys,
|
|
450
524
|
* scalar types, and numeric semantics are kept after redaction; secrets are
|
|
@@ -761,6 +835,7 @@ function isRedacted(value) {
|
|
|
761
835
|
const DEFAULT_CONTEXT = {
|
|
762
836
|
secretNames: new Set(),
|
|
763
837
|
urlNames: new Set(),
|
|
838
|
+
revealNames: new Set(),
|
|
764
839
|
};
|
|
765
840
|
function secretNameSet(redactionOptions) {
|
|
766
841
|
return new Set(redactionOptions.secretNames ?? []);
|
|
@@ -769,6 +844,7 @@ function contextFromOptions(redactionOptions) {
|
|
|
769
844
|
return {
|
|
770
845
|
secretNames: secretNameSet(redactionOptions),
|
|
771
846
|
urlNames: new Set(redactionOptions.urlNames ?? []),
|
|
847
|
+
revealNames: new Set(redactionOptions.revealNames ?? []),
|
|
772
848
|
};
|
|
773
849
|
}
|
|
774
850
|
function keyHasSecretSuffix(key) {
|
|
@@ -791,7 +867,7 @@ function redactSecrets(value, context = DEFAULT_CONTEXT, depth = 0) {
|
|
|
791
867
|
if (isObject(value)) {
|
|
792
868
|
for (const k of Object.keys(value)) {
|
|
793
869
|
const v = value[k];
|
|
794
|
-
if (isSecretKey(k, context.secretNames)) {
|
|
870
|
+
if (isSecretKey(k, context.secretNames) && !context.revealNames.has(k)) {
|
|
795
871
|
// A null secret is an absent secret. Masking it would manufacture the
|
|
796
872
|
// appearance of a configured credential — readers cannot tell
|
|
797
873
|
// "***"-because-set from "***"-because-null. Redaction hides a value
|
|
@@ -832,7 +908,7 @@ function redactUrlValue(value, context, depth) {
|
|
|
832
908
|
if (isObject(value)) {
|
|
833
909
|
for (const key of Object.keys(value)) {
|
|
834
910
|
const item = value[key];
|
|
835
|
-
if (isSecretKey(key, context.secretNames)) {
|
|
911
|
+
if (isSecretKey(key, context.secretNames) && !context.revealNames.has(key)) {
|
|
836
912
|
if (item !== null)
|
|
837
913
|
value[key] = REDACTED_MARKER;
|
|
838
914
|
}
|
|
@@ -1216,21 +1292,34 @@ function tryStripGenericMicro(key) {
|
|
|
1216
1292
|
return null;
|
|
1217
1293
|
return [stripped, code];
|
|
1218
1294
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1295
|
+
/**
|
|
1296
|
+
* The exact digits of an integer-valued number or decimal integer string, of
|
|
1297
|
+
* any magnitude. Bitcoin amounts and `_epoch_ns` only append a unit or divide,
|
|
1298
|
+
* so unlike money suffixes they are not bounded by i64: an integer literal
|
|
1299
|
+
* keeps its digits, and a float counts when its value is an integer below 1e21
|
|
1300
|
+
* (the canonical number form's integer range). A numeric -0 is zero.
|
|
1301
|
+
*/
|
|
1222
1302
|
function decimalIntText(value) {
|
|
1223
|
-
if (typeof value === "string"
|
|
1224
|
-
return value;
|
|
1303
|
+
if (typeof value === "string")
|
|
1304
|
+
return /^-?\d+$/.test(value) ? value : null;
|
|
1225
1305
|
if (typeof value === "bigint")
|
|
1226
1306
|
return value.toString();
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1307
|
+
let literal;
|
|
1308
|
+
if (typeof value === "number") {
|
|
1309
|
+
if (!Number.isFinite(value))
|
|
1310
|
+
return null;
|
|
1311
|
+
literal = value.toString();
|
|
1232
1312
|
}
|
|
1233
|
-
|
|
1313
|
+
else if (isLosslessNumber(value)) {
|
|
1314
|
+
literal = value.toString();
|
|
1315
|
+
}
|
|
1316
|
+
else {
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
const integer = /[.eE]/.test(literal) ? integralFloatText(literal) : literal;
|
|
1320
|
+
if (integer === null)
|
|
1321
|
+
return null;
|
|
1322
|
+
return integer === "-0" ? "0" : integer;
|
|
1234
1323
|
}
|
|
1235
1324
|
const MIN_I64 = -9223372036854775808n;
|
|
1236
1325
|
const MAX_I64 = 9223372036854775807n;
|
|
@@ -1240,14 +1329,23 @@ const MAX_U64 = 18446744073709551615n;
|
|
|
1240
1329
|
function integerValue(value) {
|
|
1241
1330
|
if (typeof value === "bigint")
|
|
1242
1331
|
return value;
|
|
1243
|
-
if (typeof value === "number")
|
|
1244
|
-
|
|
1332
|
+
if (typeof value === "number") {
|
|
1333
|
+
// Read through the shortest spelling, the text the canonical number form
|
|
1334
|
+
// prints, so 2**60 counts as 1152921504606847000 exactly as a decoded
|
|
1335
|
+
// literal of that spelling does in every SDK; BigInt(value) would give
|
|
1336
|
+
// ...846976 and disagree with Rust.
|
|
1337
|
+
if (!Number.isFinite(value))
|
|
1338
|
+
return null;
|
|
1339
|
+
const literal = value.toString();
|
|
1340
|
+
const integer = /[.eE]/.test(literal) ? integralFloatText(literal) : literal;
|
|
1341
|
+
return integer !== null ? BigInt(integer) : null;
|
|
1342
|
+
}
|
|
1245
1343
|
if (isLosslessNumber(value)) {
|
|
1246
1344
|
const literal = value.toString();
|
|
1247
1345
|
if (/^-?\d+$/.test(literal))
|
|
1248
1346
|
return BigInt(literal);
|
|
1249
|
-
const
|
|
1250
|
-
return
|
|
1347
|
+
const integer = integralFloatText(literal);
|
|
1348
|
+
return integer !== null ? BigInt(integer) : null;
|
|
1251
1349
|
}
|
|
1252
1350
|
return null;
|
|
1253
1351
|
}
|
|
@@ -1288,24 +1386,14 @@ function isNum(value) {
|
|
|
1288
1386
|
}
|
|
1289
1387
|
function tryProcessField(key, value) {
|
|
1290
1388
|
let stripped;
|
|
1291
|
-
//
|
|
1292
|
-
|
|
1293
|
-
// LosslessNumber is normalized to one here via Number() \u2014 which never
|
|
1294
|
-
// throws, unlike LosslessNumber's own valueOf() (documented to throw for
|
|
1295
|
-
// values it can't safely represent as number/bigint) \u2014 so an ordinary
|
|
1296
|
-
// decoded value (e.g. duration_ms: 42) keeps formatting exactly as before,
|
|
1297
|
-
// and an exotic decoded value degrades gracefully (loses precision here,
|
|
1298
|
-
// same as it always would for a >2^53 native number) rather than crashing.
|
|
1299
|
-
// This is Plain's documented lossy/human path; JSON/YAML stay exact via
|
|
1300
|
-
// yamlScalar/plainScalar/canonicalJsonString, and decimalIntText below
|
|
1301
|
-
// (msats/sats/epoch_ns) keeps exactness by reading `value` directly instead
|
|
1302
|
-
// of `numeric`.
|
|
1303
|
-
const numeric = isLosslessNumber(value) ? Number(value.toString()) : value;
|
|
1389
|
+
// Appending a unit does no arithmetic and must retain decoded digits.
|
|
1390
|
+
const isNumber = isNum(value) || isLosslessNumber(value);
|
|
1304
1391
|
// Group 1: compound timestamp suffixes
|
|
1305
1392
|
stripped = stripSuffixCI(key, "_epoch_ms");
|
|
1306
1393
|
if (stripped !== null) {
|
|
1307
|
-
|
|
1308
|
-
|
|
1394
|
+
const integer = signedI64(value);
|
|
1395
|
+
if (integer !== null) {
|
|
1396
|
+
const formatted = formatRfc3339Ms(Number(integer));
|
|
1309
1397
|
if (formatted !== null)
|
|
1310
1398
|
return [stripped, formatted];
|
|
1311
1399
|
}
|
|
@@ -1313,8 +1401,9 @@ function tryProcessField(key, value) {
|
|
|
1313
1401
|
}
|
|
1314
1402
|
stripped = stripSuffixCI(key, "_epoch_s");
|
|
1315
1403
|
if (stripped !== null) {
|
|
1316
|
-
|
|
1317
|
-
|
|
1404
|
+
const integer = signedI64(value);
|
|
1405
|
+
if (integer !== null) {
|
|
1406
|
+
const formatted = formatRfc3339Ms(Number(integer) * 1000);
|
|
1318
1407
|
if (formatted !== null)
|
|
1319
1408
|
return [stripped, formatted];
|
|
1320
1409
|
}
|
|
@@ -1376,20 +1465,20 @@ function tryProcessField(key, value) {
|
|
|
1376
1465
|
}
|
|
1377
1466
|
stripped = stripSuffixCI(key, "_minutes");
|
|
1378
1467
|
if (stripped !== null) {
|
|
1379
|
-
if (
|
|
1380
|
-
return [stripped, `${plainScalar(
|
|
1468
|
+
if (isNumber)
|
|
1469
|
+
return [stripped, `${plainScalar(value)} minutes`];
|
|
1381
1470
|
return null;
|
|
1382
1471
|
}
|
|
1383
1472
|
stripped = stripSuffixCI(key, "_hours");
|
|
1384
1473
|
if (stripped !== null) {
|
|
1385
|
-
if (
|
|
1386
|
-
return [stripped, `${plainScalar(
|
|
1474
|
+
if (isNumber)
|
|
1475
|
+
return [stripped, `${plainScalar(value)} hours`];
|
|
1387
1476
|
return null;
|
|
1388
1477
|
}
|
|
1389
1478
|
stripped = stripSuffixCI(key, "_days");
|
|
1390
1479
|
if (stripped !== null) {
|
|
1391
|
-
if (
|
|
1392
|
-
return [stripped, `${plainScalar(
|
|
1480
|
+
if (isNumber)
|
|
1481
|
+
return [stripped, `${plainScalar(value)} days`];
|
|
1393
1482
|
return null;
|
|
1394
1483
|
}
|
|
1395
1484
|
// Group 4: single-unit suffixes
|
|
@@ -1416,8 +1505,8 @@ function tryProcessField(key, value) {
|
|
|
1416
1505
|
}
|
|
1417
1506
|
stripped = stripSuffixCI(key, "_percent");
|
|
1418
1507
|
if (stripped !== null) {
|
|
1419
|
-
if (
|
|
1420
|
-
return [stripped, `${plainScalar(
|
|
1508
|
+
if (isNumber)
|
|
1509
|
+
return [stripped, `${plainScalar(value)}%`];
|
|
1421
1510
|
return null;
|
|
1422
1511
|
}
|
|
1423
1512
|
// Group 5: short suffixes (last to avoid false positives)
|
|
@@ -1433,27 +1522,27 @@ function tryProcessField(key, value) {
|
|
|
1433
1522
|
}
|
|
1434
1523
|
stripped = stripSuffixCI(key, "_ns");
|
|
1435
1524
|
if (stripped !== null) {
|
|
1436
|
-
if (
|
|
1437
|
-
return [stripped, `${plainScalar(
|
|
1525
|
+
if (isNumber)
|
|
1526
|
+
return [stripped, `${plainScalar(value)}ns`];
|
|
1438
1527
|
return null;
|
|
1439
1528
|
}
|
|
1440
1529
|
stripped = stripSuffixCI(key, "_us");
|
|
1441
1530
|
if (stripped !== null) {
|
|
1442
|
-
if (
|
|
1443
|
-
return [stripped, `${plainScalar(
|
|
1531
|
+
if (isNumber)
|
|
1532
|
+
return [stripped, `${plainScalar(value)}\u03bcs`];
|
|
1444
1533
|
return null;
|
|
1445
1534
|
}
|
|
1446
1535
|
stripped = stripSuffixCI(key, "_ms");
|
|
1447
1536
|
if (stripped !== null) {
|
|
1448
|
-
const fv = formatMsValue(
|
|
1537
|
+
const fv = formatMsValue(value);
|
|
1449
1538
|
if (fv !== null)
|
|
1450
1539
|
return [stripped, fv];
|
|
1451
1540
|
return null;
|
|
1452
1541
|
}
|
|
1453
1542
|
stripped = stripSuffixCI(key, "_s");
|
|
1454
1543
|
if (stripped !== null) {
|
|
1455
|
-
if (
|
|
1456
|
-
return [stripped, `${plainScalar(
|
|
1544
|
+
if (isNumber)
|
|
1545
|
+
return [stripped, `${plainScalar(value)}s`];
|
|
1457
1546
|
return null;
|
|
1458
1547
|
}
|
|
1459
1548
|
return null;
|
|
@@ -1500,17 +1589,18 @@ function processObjectFields(obj) {
|
|
|
1500
1589
|
// Formatting Helpers
|
|
1501
1590
|
// ═══════════════════════════════════════════
|
|
1502
1591
|
function formatMsAsSeconds(ms) {
|
|
1503
|
-
const formatted = (ms / 1000
|
|
1592
|
+
const formatted = toFixedHalfEven(ms / 1000, 3);
|
|
1504
1593
|
const trimmed = formatted.replace(/0+$/, "");
|
|
1505
1594
|
if (trimmed.endsWith("."))
|
|
1506
1595
|
return trimmed + "0s";
|
|
1507
1596
|
return trimmed + "s";
|
|
1508
1597
|
}
|
|
1509
1598
|
function formatMsValue(value) {
|
|
1510
|
-
|
|
1599
|
+
const numeric = isLosslessNumber(value) ? Number(value.toString()) : value;
|
|
1600
|
+
if (!isNum(numeric) || !Number.isFinite(numeric))
|
|
1511
1601
|
return null;
|
|
1512
|
-
if (Math.abs(
|
|
1513
|
-
return formatMsAsSeconds(
|
|
1602
|
+
if (Math.abs(numeric) >= 1000)
|
|
1603
|
+
return formatMsAsSeconds(numeric);
|
|
1514
1604
|
return `${plainScalar(value)}ms`;
|
|
1515
1605
|
}
|
|
1516
1606
|
const MIN_RFC3339_MS = -62135596800000;
|
|
@@ -1524,25 +1614,30 @@ function formatRfc3339Ms(ms) {
|
|
|
1524
1614
|
return d.toISOString().replace(/(\.\d{3})\d*Z$/, "$1Z");
|
|
1525
1615
|
}
|
|
1526
1616
|
/**
|
|
1527
|
-
*
|
|
1528
|
-
*
|
|
1529
|
-
*
|
|
1530
|
-
*
|
|
1617
|
+
* Format `x` with `digits` decimals the way printf `%.Nf` does in Rust, Go and
|
|
1618
|
+
* Python: round the double's exact binary value, half to even.
|
|
1619
|
+
*
|
|
1620
|
+
* `Number.toFixed` already rounds the exact value, but breaks a true tie
|
|
1621
|
+
* (1.0625 at 3 places) away from zero and switches to exponent form from
|
|
1622
|
+
* 1e21. So take the exact expansion — 100 places covers every double this
|
|
1623
|
+
* can tie on — and round the decimal string here. An epsilon test on
|
|
1624
|
+
* `x * 10 ** digits` is not a substitute: it calls near-ties ties.
|
|
1531
1625
|
*/
|
|
1532
1626
|
function toFixedHalfEven(x, digits) {
|
|
1533
|
-
const
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
const
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1627
|
+
const negative = x < 0;
|
|
1628
|
+
const a = Math.abs(x);
|
|
1629
|
+
// Every double from 1e21 up is an integer, which BigInt spells exactly.
|
|
1630
|
+
const exact = a >= 1e21 ? `${BigInt(a)}.${"0".repeat(100)}` : a.toFixed(100);
|
|
1631
|
+
const [whole, fraction] = exact.split(".");
|
|
1632
|
+
const next = fraction.charCodeAt(digits) - 48;
|
|
1633
|
+
let units = BigInt(whole + fraction.slice(0, digits));
|
|
1634
|
+
const tieBroken = /[1-9]/.test(fraction.slice(digits + 1)) || units % 2n === 1n;
|
|
1635
|
+
if (next > 5 || (next === 5 && tieBroken))
|
|
1636
|
+
units += 1n;
|
|
1637
|
+
const text = units.toString().padStart(digits + 1, "0");
|
|
1638
|
+
const point = text.length - digits;
|
|
1639
|
+
const body = digits === 0 ? text : `${text.slice(0, point)}.${text.slice(point)}`;
|
|
1640
|
+
return negative ? `-${body}` : body;
|
|
1546
1641
|
}
|
|
1547
1642
|
function formatBytesHuman(bytes) {
|
|
1548
1643
|
const KiB = 1024;
|
|
@@ -1696,6 +1791,100 @@ function isAmbiguousYamlKey(key) {
|
|
|
1696
1791
|
return ["inf", "infinity", "nan"].includes(key.replace(/^[+-]/, "").toLowerCase())
|
|
1697
1792
|
|| /^[-+]?(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][-+]?[0-9]+)?$/.test(key);
|
|
1698
1793
|
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Render a number's literal canonically for YAML/plain: a float literal whose
|
|
1796
|
+
* value is an integer below 1e21 in magnitude renders as that integer ("3.0"
|
|
1797
|
+
* -> "3", "-0.0" -> "-0"); anything else keeps its digits, placed by
|
|
1798
|
+
* placeDecimalPoint. Native numbers and decoded literals share it, and it
|
|
1799
|
+
* works on the text so a digit is never lost to a float64 round trip.
|
|
1800
|
+
*/
|
|
1801
|
+
function formatNumberLiteral(literal) {
|
|
1802
|
+
return integralFloatText(literal) ?? placeDecimalPoint(literal) ?? normalizeExponent(literal);
|
|
1803
|
+
}
|
|
1804
|
+
const I64_MIN = -(2n ** 63n);
|
|
1805
|
+
const I64_MAX = 2n ** 63n - 1n;
|
|
1806
|
+
/**
|
|
1807
|
+
* Spell a non-integral float literal with the notation `Number#toString`
|
|
1808
|
+
* picks: positional when the value lies in [1e-6, 1e21), exponent form
|
|
1809
|
+
* otherwise. A decoded literal carries its own spelling and the other SDKs'
|
|
1810
|
+
* native formatters switch elsewhere, so the choice is made here, on the
|
|
1811
|
+
* digits as written. Null for integer literals and exponents beyond i64,
|
|
1812
|
+
* which keep their spelling.
|
|
1813
|
+
*/
|
|
1814
|
+
function placeDecimalPoint(literal) {
|
|
1815
|
+
if (!/[.eE]/.test(literal))
|
|
1816
|
+
return null;
|
|
1817
|
+
const sign = literal.startsWith("-") ? "-" : "";
|
|
1818
|
+
const unsigned = sign ? literal.slice(1) : literal;
|
|
1819
|
+
const e = unsigned.search(/[eE]/);
|
|
1820
|
+
const mantissa = e >= 0 ? unsigned.slice(0, e) : unsigned;
|
|
1821
|
+
const [whole, fraction = ""] = mantissa.split(".");
|
|
1822
|
+
const digits = whole + fraction;
|
|
1823
|
+
const significant = digits.replace(/^0+/, "");
|
|
1824
|
+
if (!significant)
|
|
1825
|
+
return null;
|
|
1826
|
+
const shape = /^([-+]?)0*(\d{0,19})$/.exec(e >= 0 ? unsigned.slice(e + 1) : "0");
|
|
1827
|
+
if (!shape || (e >= 0 && unsigned.length === e + 1 + shape[1].length))
|
|
1828
|
+
return null;
|
|
1829
|
+
const exponent = BigInt(shape[1] + (shape[2] || "0"));
|
|
1830
|
+
// point: how many significant digits precede the decimal point.
|
|
1831
|
+
const point = BigInt(whole.length - (digits.length - significant.length)) + exponent;
|
|
1832
|
+
if (exponent < I64_MIN || exponent > I64_MAX || point - 1n < I64_MIN || point > I64_MAX) {
|
|
1833
|
+
return null;
|
|
1834
|
+
}
|
|
1835
|
+
let text;
|
|
1836
|
+
if (point >= 1n && point <= 21n) {
|
|
1837
|
+
const integer = significant.slice(0, Number(point));
|
|
1838
|
+
const rest = significant.slice(Number(point));
|
|
1839
|
+
if (!rest)
|
|
1840
|
+
return null;
|
|
1841
|
+
text = `${integer}.${rest}`;
|
|
1842
|
+
}
|
|
1843
|
+
else if (point >= -5n && point <= 0n) {
|
|
1844
|
+
text = `0.${"0".repeat(Number(-point))}${significant}`;
|
|
1845
|
+
}
|
|
1846
|
+
else {
|
|
1847
|
+
const scale = point - 1n;
|
|
1848
|
+
const rest = significant.length > 1 ? `.${significant.slice(1)}` : "";
|
|
1849
|
+
text = `${significant[0]}${rest}e${scale < 0n ? "-" : "+"}${scale < 0n ? -scale : scale}`;
|
|
1850
|
+
}
|
|
1851
|
+
return sign + text;
|
|
1852
|
+
}
|
|
1853
|
+
function integralFloatText(literal) {
|
|
1854
|
+
if (!/[.eE]/.test(literal))
|
|
1855
|
+
return null;
|
|
1856
|
+
const negative = literal.startsWith("-");
|
|
1857
|
+
const unsigned = negative ? literal.slice(1) : literal;
|
|
1858
|
+
const e = unsigned.search(/[eE]/);
|
|
1859
|
+
const mantissa = e >= 0 ? unsigned.slice(0, e) : unsigned;
|
|
1860
|
+
const [whole, fraction = ""] = mantissa.split(".");
|
|
1861
|
+
const digits = whole + fraction;
|
|
1862
|
+
if (!/[^0]/.test(digits))
|
|
1863
|
+
return negative ? "-0" : "0";
|
|
1864
|
+
const exponent = e >= 0 ? Number(unsigned.slice(e + 1)) : 0;
|
|
1865
|
+
if (!Number.isSafeInteger(exponent))
|
|
1866
|
+
return null;
|
|
1867
|
+
const point = whole.length + exponent;
|
|
1868
|
+
const split = Math.min(Math.max(point, 0), digits.length);
|
|
1869
|
+
if (/[^0]/.test(digits.slice(split)))
|
|
1870
|
+
return null;
|
|
1871
|
+
const padding = Math.min(Math.max(point - digits.length, 0), 22);
|
|
1872
|
+
const integer = (digits.slice(0, split) + "0".repeat(padding)).replace(/^0+/, "");
|
|
1873
|
+
if (integer.length > 21)
|
|
1874
|
+
return null;
|
|
1875
|
+
return (negative ? "-" : "") + (integer || "0");
|
|
1876
|
+
}
|
|
1877
|
+
function normalizeExponent(literal) {
|
|
1878
|
+
const match = /^(.*?)[eE]([-+]?)0*(\d*)$/.exec(literal);
|
|
1879
|
+
if (!match)
|
|
1880
|
+
return literal;
|
|
1881
|
+
// An unsigned exponent is positive; spell it like toString() does (e+23).
|
|
1882
|
+
return `${match[1]}e${match[2] || "+"}${match[3] || "0"}`;
|
|
1883
|
+
}
|
|
1884
|
+
function numberText(value) {
|
|
1885
|
+
// `toString` spells negative zero "0"; the canonical form keeps the sign.
|
|
1886
|
+
return Object.is(value, -0) ? "-0" : formatNumberLiteral(value.toString());
|
|
1887
|
+
}
|
|
1699
1888
|
function yamlScalar(value) {
|
|
1700
1889
|
if (typeof value === "string")
|
|
1701
1890
|
return `"${escapeYamlStr(value)}"`;
|
|
@@ -1704,14 +1893,14 @@ function yamlScalar(value) {
|
|
|
1704
1893
|
if (typeof value === "boolean")
|
|
1705
1894
|
return value.toString();
|
|
1706
1895
|
if (typeof value === "number")
|
|
1707
|
-
return value
|
|
1896
|
+
return numberText(value);
|
|
1708
1897
|
if (typeof value === "bigint")
|
|
1709
1898
|
return value.toString();
|
|
1710
|
-
// Decoded number:
|
|
1711
|
-
//
|
|
1899
|
+
// Decoded number: its exact digits, in the same canonical form a native
|
|
1900
|
+
// number gets.
|
|
1712
1901
|
if (isLosslessNumber(value))
|
|
1713
|
-
return value.toString();
|
|
1714
|
-
return `"${escapeYamlStr(
|
|
1902
|
+
return formatNumberLiteral(value.toString());
|
|
1903
|
+
return `"${escapeYamlStr(embeddedJson(value))}"`;
|
|
1715
1904
|
}
|
|
1716
1905
|
// ═══════════════════════════════════════════
|
|
1717
1906
|
// Plain Rendering (logfmt)
|
|
@@ -1772,19 +1961,19 @@ function plainScalar(value) {
|
|
|
1772
1961
|
if (typeof value === "boolean")
|
|
1773
1962
|
return value.toString();
|
|
1774
1963
|
if (typeof value === "number")
|
|
1775
|
-
return value
|
|
1964
|
+
return numberText(value);
|
|
1776
1965
|
if (typeof value === "bigint")
|
|
1777
1966
|
return value.toString();
|
|
1778
|
-
// Decoded number:
|
|
1779
|
-
//
|
|
1780
|
-
// gets
|
|
1967
|
+
// Decoded number: its exact digits (Plain is documented lossy only for
|
|
1968
|
+
// arithmetic-derived formatting), in the same canonical form a native
|
|
1969
|
+
// number gets.
|
|
1781
1970
|
if (isLosslessNumber(value))
|
|
1782
|
-
return value.toString();
|
|
1783
|
-
return
|
|
1971
|
+
return formatNumberLiteral(value.toString());
|
|
1972
|
+
return embeddedJson(value);
|
|
1784
1973
|
}
|
|
1785
1974
|
function plainScalarRaw(value) {
|
|
1786
1975
|
if (isObject(value) || Array.isArray(value)) {
|
|
1787
|
-
return
|
|
1976
|
+
return embeddedJson(value);
|
|
1788
1977
|
}
|
|
1789
1978
|
return plainScalar(value);
|
|
1790
1979
|
}
|
|
@@ -1809,6 +1998,11 @@ function quoteLogfmtKey(key) {
|
|
|
1809
1998
|
// ═══════════════════════════════════════════
|
|
1810
1999
|
// Utilities
|
|
1811
2000
|
// ═══════════════════════════════════════════
|
|
2001
|
+
// Arbitrary JSON names, including __proto__, are own data properties. A normal
|
|
2002
|
+
// bracket assignment would invoke Object.prototype's legacy prototype setter.
|
|
2003
|
+
function setJsonField(target, key, value) {
|
|
2004
|
+
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
2005
|
+
}
|
|
1812
2006
|
function sanitizeForJson(value, stack = new WeakSet(), depth = 0) {
|
|
1813
2007
|
if (depth >= MAX_DEPTH)
|
|
1814
2008
|
return MAX_DEPTH_MARKER;
|
|
@@ -1823,8 +2017,9 @@ function sanitizeForJson(value, stack = new WeakSet(), depth = 0) {
|
|
|
1823
2017
|
const n = value;
|
|
1824
2018
|
if (!Number.isFinite(n))
|
|
1825
2019
|
return "<unsupported:number>";
|
|
1826
|
-
|
|
1827
|
-
|
|
2020
|
+
// An integral number past 2^53 is still a float64, like every SDK's native
|
|
2021
|
+
// float, and renders by the same canonical rule. A caller that needs such
|
|
2022
|
+
// an integer exactly passes a bigint.
|
|
1828
2023
|
return n;
|
|
1829
2024
|
}
|
|
1830
2025
|
if (t === "bigint")
|
|
@@ -1859,7 +2054,7 @@ function sanitizeForJson(value, stack = new WeakSet(), depth = 0) {
|
|
|
1859
2054
|
stack.add(obj);
|
|
1860
2055
|
const out = {};
|
|
1861
2056
|
for (const [k, v] of Object.entries(obj)) {
|
|
1862
|
-
out
|
|
2057
|
+
setJsonField(out, k, sanitizeForJson(v, stack, depth + 1));
|
|
1863
2058
|
}
|
|
1864
2059
|
stack.delete(obj);
|
|
1865
2060
|
return out;
|
|
@@ -1878,17 +2073,23 @@ function isObject(value) {
|
|
|
1878
2073
|
function sortedObjectKeys(obj) {
|
|
1879
2074
|
return Object.keys(obj).sort(jcsCompare);
|
|
1880
2075
|
}
|
|
1881
|
-
|
|
2076
|
+
/**
|
|
2077
|
+
* A container nested inside a Plain array (or in a YAML scalar position) is
|
|
2078
|
+
* written as compact JSON with sorted keys. Its number leaves take the
|
|
2079
|
+
* canonical number form, the same text the value prints as a scalar, so a
|
|
2080
|
+
* nested `1.0` reads `1` and a decoded `1e5` reads `100000` in every SDK
|
|
2081
|
+
* rather than in each runtime's own JSON spelling.
|
|
2082
|
+
*/
|
|
2083
|
+
function embeddedJson(value) {
|
|
1882
2084
|
if (Array.isArray(value))
|
|
1883
|
-
return value.map(
|
|
2085
|
+
return `[${value.map(embeddedJson).join(",")}]`;
|
|
1884
2086
|
if (isObject(value)) {
|
|
1885
|
-
const
|
|
1886
|
-
|
|
1887
|
-
out[key] = sortJsonValue(value[key]);
|
|
1888
|
-
}
|
|
1889
|
-
return out;
|
|
2087
|
+
const members = sortedObjectKeys(value).map((key) => `${JSON.stringify(key)}:${embeddedJson(value[key])}`);
|
|
2088
|
+
return `{${members.join(",")}}`;
|
|
1890
2089
|
}
|
|
1891
|
-
|
|
2090
|
+
if (typeof value === "string")
|
|
2091
|
+
return JSON.stringify(value);
|
|
2092
|
+
return plainScalar(value);
|
|
1892
2093
|
}
|
|
1893
2094
|
function jcsCompare(a, b) {
|
|
1894
2095
|
const ua = Array.from(a).flatMap((c) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-first-data",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A naming convention that lets AI agents understand your data without being told what it means, plus
|
|
3
|
+
"version": "0.36.1",
|
|
4
|
+
"description": "A naming convention that lets AI agents understand your data without being told what it means, plus the afdata CLI and a Rust library for reading Markdown structure and safely editing structured JSON, TOML, YAML, dotenv, and INI documents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsc",
|
|
26
26
|
"prepack": "npm run build",
|
|
27
|
-
"test": "tsx --test src/*.test.ts"
|
|
27
|
+
"test": "node --import tsx --test src/*.test.ts"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@types/node": "^26.6.
|
|
30
|
+
"@types/node": "^26.6.6",
|
|
31
31
|
"tsx": "^4.23.15",
|
|
32
32
|
"typescript": "^7.0.2"
|
|
33
33
|
},
|