agent-first-data 0.16.0 → 0.17.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/README.md CHANGED
@@ -17,7 +17,7 @@ console.log(outputJson(event.toJSON()));
17
17
  console.log(outputPlain(event.toJSON()));
18
18
  ```
19
19
 
20
- Useful names use TypeScript casing: `outputJson`, `outputYaml`, `outputPlain`, `outputOptionsForPolicy`, `redactedValue`, `redactUrlSecrets`, `parseSize`, `normalizeUtcOffset`, `isValidRfc3339Date`, `isValidRfc3339Time`, `decodeProtocolEvent`, `cliParseOutput`, `cliParseLogFilters`, `cliOutput`, `buildCliError`, `buildCliVersion`, and `cliHandleVersionOrContinue`.
20
+ Useful names use TypeScript casing: `outputJson`, `outputYaml`, `outputPlain`, `outputOptionsForPolicy`, `redactedValue`, `redactUrlSecrets`, `normalizeUtcOffset`, `isValidRfc3339Date`, `isValidRfc3339Time`, `decodeProtocolEvent`, `cliParseOutput`, `cliParseLogFilters`, `cliOutput`, `buildCliError`, `buildCliVersion`, and `cliHandleVersionOrContinue`.
21
21
 
22
22
  Skill admin and stream redirection are not re-exported from the package root; import them from their own subpaths:
23
23
 
@@ -47,13 +47,7 @@
47
47
  "type": "object",
48
48
  "properties": {
49
49
  "kind": {"const": "progress"},
50
- "progress": {
51
- "type": "object",
52
- "properties": {
53
- "message": {"type": "string", "minLength": 1}
54
- },
55
- "additionalProperties": true
56
- },
50
+ "progress": true,
57
51
  "trace": {"$ref": "#/$defs/trace"}
58
52
  },
59
53
  "required": ["kind", "progress"],
@@ -63,18 +57,7 @@
63
57
  "type": "object",
64
58
  "properties": {
65
59
  "kind": {"const": "log"},
66
- "log": {
67
- "type": "object",
68
- "properties": {
69
- "message": {"type": "string", "minLength": 1},
70
- "level": {"enum": ["debug", "info", "warn", "error"]},
71
- "code": false
72
- },
73
- "not": {
74
- "required": ["code"]
75
- },
76
- "additionalProperties": true
77
- },
60
+ "log": true,
78
61
  "trace": {"$ref": "#/$defs/trace"}
79
62
  },
80
63
  "required": ["kind", "log"],
@@ -132,7 +132,7 @@
132
132
  "unit": "bcp47_language_tag",
133
133
  "formatting": {"strip_key": false, "plain_yaml": "pass_through"},
134
134
  "redaction": {"mode": "none"},
135
- "constraints": {"string": true}
135
+ "constraints": {"bcp47": true}
136
136
  },
137
137
  {
138
138
  "suffix": "_utc_offset",
@@ -152,18 +152,9 @@
152
152
  "redaction": {"mode": "none"},
153
153
  "constraints": {"integer": true, "minimum": 0}
154
154
  },
155
- {
156
- "suffix": "_size",
157
- "category": "size",
158
- "json_types": ["string"],
159
- "unit": "byte_size_input",
160
- "formatting": {"strip_key": false, "plain_yaml": "pass_through"},
161
- "redaction": {"mode": "none"},
162
- "constraints": {"parse_size_units": ["B", "kB", "MB", "GB", "TB", "KiB", "MiB", "GiB", "TiB"]}
163
- },
164
155
  {
165
156
  "suffix": "_percent",
166
- "category": "ratio",
157
+ "category": "percentage",
167
158
  "json_types": ["number"],
168
159
  "unit": "percent",
169
160
  "formatting": {"strip_key": true, "plain_yaml": "append_percent"},
@@ -132,7 +132,7 @@
132
132
  "unit": "bcp47_language_tag",
133
133
  "formatting": {"strip_key": false, "plain_yaml": "pass_through"},
134
134
  "redaction": {"mode": "none"},
135
- "constraints": {"string": true}
135
+ "constraints": {"bcp47": true}
136
136
  },
137
137
  {
138
138
  "suffix": "_utc_offset",
@@ -152,18 +152,9 @@
152
152
  "redaction": {"mode": "none"},
153
153
  "constraints": {"integer": true, "minimum": 0}
154
154
  },
155
- {
156
- "suffix": "_size",
157
- "category": "size",
158
- "json_types": ["string"],
159
- "unit": "byte_size_input",
160
- "formatting": {"strip_key": false, "plain_yaml": "pass_through"},
161
- "redaction": {"mode": "none"},
162
- "constraints": {"parse_size_units": ["B", "kB", "MB", "GB", "TB", "KiB", "MiB", "GiB", "TiB"]}
163
- },
164
155
  {
165
156
  "suffix": "_percent",
166
- "category": "ratio",
157
+ "category": "percentage",
167
158
  "json_types": ["number"],
168
159
  "unit": "percent",
169
160
  "formatting": {"strip_key": true, "plain_yaml": "append_percent"},
@@ -37,6 +37,10 @@ The field name is the schema. Always encode units and semantics in the field nam
37
37
  | `_epoch_ns` | nanoseconds since Unix epoch as a decimal string | `created_epoch_ns: "1707868800000000000"` |
38
38
  | `_rfc3339` | RFC 3339 date-time string | `expires_rfc3339: "2026-02-14T10:30:00Z"` |
39
39
 
40
+ Integer vs string is not uniform across siblings: `_epoch_s` and `_epoch_ms` are JSON integers, but `_epoch_ns` is a decimal **string** because nanoseconds exceed the 2⁵³−1 safe-integer range. The same rule applies to large `_sats`/`_msats` (integer when safe, decimal string when not). Writing `_epoch_ns` as a bare JSON number silently loses precision — `afdata lint` rejects it (`suffix_type_mismatch`).
41
+
42
+ `_rfc3339` needs a **mandatory** offset: `YYYY-MM-DDThh:mm:ss[.fff]` followed by `Z` or `±HH:MM`. A bare `2026-02-14T10:30:00` (no offset), a space instead of `T`, or a trailing IANA name (`...Asia/Shanghai`) is rejected by `afdata lint`. `afdata lint` also type-checks the numeric suffixes (durations `_s`/`_ms`/…, currency `_cents`/`_micro`/`_jpy`) and rejects a `_url` with internal whitespace or bare `user:pass@host` credentials.
43
+
40
44
  ### Strict string formats
41
45
 
42
46
  | Suffix | Format | Example |
@@ -46,7 +50,7 @@ The field name is the schema. Always encode units and semantics in the field nam
46
50
  | `_rfc3339_date` | RFC 3339 full-date string | `invoice_due_rfc3339_date: "2026-06-13"` |
47
51
  | `_rfc3339_time` | RFC 3339 partial-time string | `market_open_rfc3339_time: "09:30:00"` |
48
52
 
49
- `*_bcp47` identifies a BCP-47 language tag string. AFDATA does not implement the full BCP-47 registry; tools may validate tags when needed.
53
+ `*_bcp47` identifies a BCP-47 language tag string. AFDATA validates structure (hyphen-separated subtags, 2–3 letter primary language), so the POSIX form `zh_CN` and an over-long primary like `chinese` are rejected; use `zh-CN`. It does not check the IANA registry, so a well-formed-but-unregistered tag like `zz-ZZ` still passes. `is_valid_bcp47` and `afdata lint` apply this check.
50
54
 
51
55
  `*_utc_offset` identifies a fixed UTC offset. Canonical persisted and structured output values are `"UTC"` or `±HH:MM`, with `HH` in `00..23` and `MM` in `00..59`; zero offsets normalize to `"UTC"`. This is not an IANA timezone name, DST rule, or timezone database field.
52
56
 
@@ -63,11 +67,8 @@ Avoid magic string sentinels such as `"auto"` inside strict-format fields. If a
63
67
  | Suffix | Example |
64
68
  |:-------|:--------|
65
69
  | `_bytes` | `payload_bytes: 456789` (non-negative integer) |
66
- | `_size` | `buffer_size: "10MiB"` or `"10MB"` (config files only, human-readable) |
67
70
 
68
- `_size` parsing requires explicit units: `B`, decimal `kB/MB/GB/TB`, or binary `KiB/MiB/GiB/TiB`. Bare numbers and ambiguous `K/M/G/T` are invalid.
69
-
70
- `parse_size("10MiB")` → `10485760`; `parse_size("10MB")` → `10000000`. Returns null for invalid or negative input.
71
+ Byte sizes are always integer `_bytes`, in config and output alike. There is no unit-in-value size string: never write `buffer_size: "10MiB"`. Encode the unit in the key (`buffer_bytes: 10485760`), the same way durations use `timeout_s: 30`.
71
72
 
72
73
  ### Percentage
73
74
 
@@ -75,6 +76,8 @@ Avoid magic string sentinels such as `"auto"` inside strict-format fields. If a
75
76
  |:-------|:--------|
76
77
  | `_percent` | `cpu_percent: 85` |
77
78
 
79
+ Value is in units of percent: `1` = 1%, `0.2` = 0.2%, `85` = 85%. Decimals, negatives, and values >100 are all valid; no fixed range. `%` is exactly 0.01, so `_percent` is never a 0–1 fraction — if the underlying ratio is `0.999`, multiply by 100 and write `success_percent: 99.9`. Writing `0.85` for 85% is a producer-side conversion bug, not a convention ambiguity.
80
+
78
81
  ### Currency
79
82
 
80
83
  Bitcoin:
@@ -150,7 +153,6 @@ ORM struct fields preserve the suffix: `duration_ms: i64`, not `duration: i64`.
150
153
  | `latency: 142` | `latency_ms: 142` | seconds? milliseconds? |
151
154
  | `api_key: "sk-..."` | `api_key_secret: "sk-..."` | won't be auto-redacted |
152
155
  | `cpu: 85` | `cpu_percent: 85` | 85 what? |
153
- | `buffer: "10MiB"` | `buffer_size: "10MiB"` | only `_size` gets parsed |
154
156
 
155
157
  ---
156
158
 
@@ -174,7 +176,7 @@ Remove recognized formatting suffix from key. Longest match first, exact lowerca
174
176
  4. `_msats`, `_sats`, `_bytes`, `_percent`, `_secret`
175
177
  5. `_jpy`, `_ns`, `_us`, `_ms`, `_s`
176
178
 
177
- `_size`, `_bcp47`, `_utc_offset`, `_rfc3339_date`, and `_rfc3339_time` are NOT stripped (pass through). If two keys collide after stripping, both revert to original key AND raw value (no formatting). Redaction runs before collision handling, so fallback never restores a secret.
179
+ `_bcp47`, `_utc_offset`, `_rfc3339_date`, and `_rfc3339_time` are NOT stripped (pass through). If two keys collide after stripping, both revert to original key AND raw value (no formatting). Redaction runs before collision handling, so fallback never restores a secret.
178
180
 
179
181
  ### Value formatting (YAML and Plain)
180
182
 
@@ -184,7 +186,6 @@ Remove recognized formatting suffix from key. Longest match first, exact lowerca
184
186
  - `_epoch_ms`/`_epoch_s`/decimal-string `_epoch_ns` → RFC 3339 (negative = pre-1970)
185
187
  - `_rfc3339` → pass through
186
188
  - `_bytes` → human-readable (`456789` → `446.1KiB`); negative and fractional byte values fall through raw
187
- - `_size` → pass through
188
189
  - `_percent` → append `%`
189
190
  - `_msats` → `{n}msats`, `_sats` → `{n}sats`
190
191
  - `_usd_cents` → `$X.XX`, `_eur_cents` → `€X.XX`, `_jpy` → `¥X,XXX`, `_{code}_cents` → `X.XX CODE`, `_{code}_micro` → `X.XXXXXX CODE` where `code` is 3-4 ASCII letters
@@ -258,7 +259,7 @@ Startup payload fields are tool-defined; `config` is recommended, while `version
258
259
 
259
260
  All use `kind` plus the same-named payload and optional `trace`. Do not split protocol events across `stdout` and `stderr`.
260
261
 
261
- Base validators enforce mandatory envelope/error/lifecycle rules. Strict validators additionally require object-valued `trace` on every event, complete log fields (`message`, `level`; `code` field MUST NOT appear in log payload), `retryable` on every error, and a non-empty progress `message`. Use strict validation before claiming compliance with all recommendations.
262
+ Base validators enforce mandatory envelope/error/lifecycle rules. Strict validators additionally require object-valued `trace` on every event and `retryable` on every error. Log and progress payloads remain tool-defined in both profiles. Use strict validation before claiming compliance with all recommendations.
262
263
 
263
264
  ---
264
265
 
@@ -298,11 +299,11 @@ let span = info_span!("request", request_id = %uuid);
298
299
  let _guard = span.enter();
299
300
  ```
300
301
 
301
- For Go, Python, and TypeScript: emit span fields explicitly in each log event via `.field()` or equivalent event construction.
302
+ For Go, Python, and TypeScript: emit span fields explicitly in each tool-defined log payload.
302
303
 
303
304
  ### Output fields
304
305
 
305
- Every log line contains `message`, `level` (debug/info/warn/error), plus span fields and event fields. Log payloads MUST NOT contain a `code` field. Do not use the log level as `code`; `code:"error"` is reserved for terminal protocol errors. Projects that need timestamps add them as extension fields (`timestamp_epoch_ms`) via emitter defaults or per-call `.field()`.
306
+ Log payloads have no required or reserved fields. Traditional logging adapters commonly add `message` and `level` (debug/info/warn/error), plus span and event fields, but those names and meanings are adapter conventions rather than AFDATA protocol requirements. Projects that need timestamps add `timestamp_epoch_ms` explicitly.
306
307
 
307
308
  Log redaction is **by field name** (the same `_secret`/`_url` rule as all output), applied when the line is emitted. Name the secret field — `info!(api_key_secret = %key)` — rather than logging a whole object by its `Debug`/string rendering, which hides the inner field names from redaction. For structured/nested secret-bearing data, build a value, apply redaction through output formatters or redacted_value(), then emit via the standard formatting helpers — do not pass the struct to a `?`/`%`-rendered log field.
308
309
 
@@ -340,7 +341,7 @@ Use this checklist when the skill is evaluating a change, not just documenting c
340
341
  3. Require secret-bearing values to be named with `_secret` or configured exact secret names before serialization. Do not rely on arbitrary free-text scanning or `Debug`/string-rendered objects.
341
342
  4. Run `afdata lint` for JSON/JSONL samples, JSON Schema, MCP schemas, or serialized outputs when available; run `afdata validate` for finite protocol events/streams. If unavailable, report the skipped check.
342
343
  5. Before changing an external project's public API, wire format, database schema, or persisted field names, report the compatibility impact and request approval before editing.
343
- 6. Config size values use `_size` suffix (`buffer_size: "10MiB"`, not `buffer: "10MiB"`)
344
+ 6. Byte sizes use integer `_bytes` everywhere, config included (`buffer_bytes: 10485760`, never `buffer_size: "10MiB"`)
344
345
  7. Environment variables follow `UPPER_SNAKE_CASE` with the same suffixes
345
346
  8. One-time CLI output uses `json_log()`/`CliEmitter` + output helpers; long-running services use `afdata_tracing::try_init()` (Rust) to capture via tracing; other languages emit log events explicitly via builders
346
347
  9. Database columns use AFDATA suffixes on generic types (`duration_ms INTEGER`, not `duration INTEGER`); native types like `TIMESTAMPTZ` don't need suffixes
package/dist/format.d.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  * AFDATA output formatting and protocol templates.
3
3
  *
4
4
  * Public APIs include protocol event builders, redactedValue (covers _secret
5
- * and _url fields), output formatters, redactUrlSecrets, parseSize,
6
- * normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time,
5
+ * and _url fields), output formatters, redactUrlSecrets,
6
+ * normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time, isValidRfc3339, isValidBcp47,
7
7
  * RedactionPolicy, OutputStyle, and OutputOptions.
8
8
  */
9
9
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
@@ -54,30 +54,19 @@ export declare class JsonErrorBuilder {
54
54
  build(): Event;
55
55
  }
56
56
  export declare class JsonProgressBuilder {
57
- private readonly message;
58
- private extensionFields;
57
+ private readonly payload;
59
58
  private traceValue;
60
59
  private issues;
61
- constructor(message: string);
62
- field(name: string, value: JsonValue): this;
63
- fields(obj: unknown): this;
64
- extend(obj: unknown): this;
60
+ constructor(payload: JsonValue);
65
61
  trace(value: Record<string, JsonValue> | undefined): this;
66
- private isReservedProgressField;
67
62
  build(): Event;
68
63
  }
69
64
  export declare class JsonLogBuilder {
70
- private readonly level;
71
- private readonly message;
72
- private extensionFields;
65
+ private readonly payload;
73
66
  private traceValue;
74
67
  private issues;
75
- constructor(level: LogLevel, message: string);
76
- field(name: string, value: JsonValue): this;
77
- fields(obj: unknown): this;
78
- extend(obj: unknown): this;
68
+ constructor(payload: JsonValue);
79
69
  trace(value: Record<string, JsonValue> | undefined): this;
80
- private isReservedLogField;
81
70
  build(): Event;
82
71
  }
83
72
  /** Fluent builder for result events. */
@@ -85,9 +74,9 @@ export declare function jsonResult(payload: JsonValue): JsonResultBuilder;
85
74
  /** Fluent builder for error events. */
86
75
  export declare function jsonError(code: string, message: string): JsonErrorBuilder;
87
76
  /** Fluent builder for progress events. */
88
- export declare function jsonProgress(message: string): JsonProgressBuilder;
77
+ export declare function jsonProgress(payload: JsonValue): JsonProgressBuilder;
89
78
  /** Fluent builder for log events. */
90
- export declare function jsonLog(level: LogLevel, message: string): JsonLogBuilder;
79
+ export declare function jsonLog(payload: JsonValue): JsonLogBuilder;
91
80
  /** Validate one protocol event envelope. `strict` also enforces the recommended strict protocol profile (default true). */
92
81
  export declare function validateProtocolEvent(event: unknown, strict?: boolean): void;
93
82
  /** Validate finite CLI lifecycle: (log | progress)* -> exactly one terminal. `strict` also enforces the recommended strict protocol profile (default true). */
@@ -108,15 +97,12 @@ export type DecodedError = {
108
97
  };
109
98
  export type DecodedProgress = {
110
99
  kind: "progress";
111
- message: string;
112
- fields: Record<string, JsonValue>;
100
+ progress: JsonValue;
113
101
  trace?: Record<string, JsonValue>;
114
102
  };
115
103
  export type DecodedLog = {
116
104
  kind: "log";
117
- level: LogLevel;
118
- message: string;
119
- fields: Record<string, JsonValue>;
105
+ log: JsonValue;
120
106
  trace?: Record<string, JsonValue>;
121
107
  };
122
108
  /** Discriminated union of decoded protocol events, narrow on `kind`. */
@@ -127,8 +113,7 @@ export declare class EventDecodeError extends Error {
127
113
  }
128
114
  /**
129
115
  * Parse one protocol line (a single JSON text value), strict-validate it, and
130
- * return a typed decoded event. Extension fields are every payload key beyond
131
- * the required ones, collected into `fields`.
116
+ * return a typed decoded event.
132
117
  */
133
118
  export declare function decodeProtocolEvent(text: string): DecodedEvent;
134
119
  export declare enum RedactionPolicy {
@@ -179,13 +164,6 @@ export declare function redactedValue(value: unknown, options?: {
179
164
  export declare function redactUrlSecrets(url: string, options?: {
180
165
  secretNames?: readonly string[];
181
166
  }): string;
182
- /**
183
- * Parse a human-readable size string into bytes.
184
- * Accepts numbers followed by explicit units. Decimal units are
185
- * B/kB/MB/GB/TB; binary units are KiB/MiB/GiB/TiB.
186
- * Trims whitespace and rejects ambiguous K/M/G/T units.
187
- */
188
- export declare function parseSize(s: string): number | null;
189
167
  /**
190
168
  * Normalize a fixed UTC offset string to "UTC" or ±HH:MM.
191
169
  * This helper handles fixed offsets only; IANA timezone names and DST rules
@@ -196,3 +174,22 @@ export declare function normalizeUtcOffset(value: string): string | null;
196
174
  export declare function isValidRfc3339Date(value: string): boolean;
197
175
  /** Return true when value is an RFC 3339 partial-time (HH:MM:SS[.fraction]). */
198
176
  export declare function isValidRfc3339Time(value: string): boolean;
177
+ /**
178
+ * Return true when value is a complete RFC 3339 date-time, such as
179
+ * 2026-02-14T10:30:00Z or 2026-02-14T10:30:00.5+08:00. Composed from
180
+ * isValidRfc3339Date and isValidRfc3339Time: a full-date, a T/t separator, a
181
+ * partial-time (with optional fractional seconds), and a mandatory time-offset
182
+ * (Z/z or ±HH:MM with HH in 00..23 and MM in 00..59). The offset is required, so
183
+ * a bare 2026-02-14T10:30:00 is rejected; a space separator is rejected; and a
184
+ * leap second (:60) is rejected, matching isValidRfc3339Time. Non-ASCII is rejected.
185
+ */
186
+ export declare function isValidRfc3339(value: string): boolean;
187
+ /**
188
+ * Return true when value is a structurally well-formed BCP 47 (RFC 5646) language tag.
189
+ * A grammar-level check, not a registry lookup: hyphen-separated ASCII-alphanumeric
190
+ * subtags (each 1-8 chars) whose primary subtag is a 2-3 letter language code or the
191
+ * x/i privateuse/grandfathered lead. Rejects the POSIX underscore form (zh_CN), empty
192
+ * or misplaced hyphens, non-ASCII, and out-of-range primaries such as chinese. Does not
193
+ * verify that subtags are registered with IANA.
194
+ */
195
+ export declare function isValidBcp47(value: string): boolean;
package/dist/format.js CHANGED
@@ -2,8 +2,8 @@
2
2
  * AFDATA output formatting and protocol templates.
3
3
  *
4
4
  * Public APIs include protocol event builders, redactedValue (covers _secret
5
- * and _url fields), output formatters, redactUrlSecrets, parseSize,
6
- * normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time,
5
+ * and _url fields), output formatters, redactUrlSecrets,
6
+ * normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time, isValidRfc3339, isValidBcp47,
7
7
  * RedactionPolicy, OutputStyle, and OutputOptions.
8
8
  */
9
9
  // ═══════════════════════════════════════════
@@ -176,53 +176,11 @@ export class JsonErrorBuilder {
176
176
  }
177
177
  }
178
178
  export class JsonProgressBuilder {
179
- message;
180
- extensionFields = {};
179
+ payload;
181
180
  traceValue = {};
182
181
  issues = [];
183
- constructor(message) {
184
- this.message = message;
185
- if (!message || message === "")
186
- this.issues.push("message must be a non-empty string");
187
- }
188
- field(name, value) {
189
- if (this.isReservedProgressField(name)) {
190
- this.issues.push(`cannot write reserved progress field ${JSON.stringify(name)}`);
191
- }
192
- else {
193
- this.extensionFields[name] = value;
194
- }
195
- return this;
196
- }
197
- fields(obj) {
198
- if (!isObject(obj)) {
199
- this.issues.push("fields must be a JSON object");
200
- return this;
201
- }
202
- for (const [key, val] of Object.entries(obj)) {
203
- if (this.isReservedProgressField(key)) {
204
- this.issues.push(`cannot write reserved progress field ${JSON.stringify(key)}`);
205
- }
206
- else {
207
- this.extensionFields[key] = val;
208
- }
209
- }
210
- return this;
211
- }
212
- extend(obj) {
213
- if (!isObject(obj)) {
214
- this.issues.push("extend must be a JSON object");
215
- return this;
216
- }
217
- for (const [key, val] of Object.entries(obj)) {
218
- if (this.isReservedProgressField(key)) {
219
- this.issues.push(`cannot write reserved progress field ${JSON.stringify(key)}`);
220
- }
221
- else {
222
- this.extensionFields[key] = val;
223
- }
224
- }
225
- return this;
182
+ constructor(payload) {
183
+ this.payload = payload;
226
184
  }
227
185
  trace(value) {
228
186
  if (value !== undefined) {
@@ -235,71 +193,20 @@ export class JsonProgressBuilder {
235
193
  }
236
194
  return this;
237
195
  }
238
- isReservedProgressField(name) {
239
- return name === "message";
240
- }
241
196
  build() {
242
197
  if (this.issues.length > 0) {
243
198
  throw new EventBuildError(`Failed to build progress event: ${this.issues.join("; ")}`, this.issues);
244
199
  }
245
- const progress = { ...this.extensionFields };
246
- progress.message = this.message;
247
- const m = { kind: "progress", progress, trace: this.traceValue };
200
+ const m = { kind: "progress", progress: this.payload, trace: this.traceValue };
248
201
  return new Event(m);
249
202
  }
250
203
  }
251
204
  export class JsonLogBuilder {
252
- level;
253
- message;
254
- extensionFields = {};
205
+ payload;
255
206
  traceValue = {};
256
207
  issues = [];
257
- constructor(level, message) {
258
- this.level = level;
259
- this.message = message;
260
- if (!message || message === "")
261
- this.issues.push("message must be a non-empty string");
262
- if (!isValidLogLevel(level))
263
- this.issues.push(`level must be one of debug, info, warn, error`);
264
- }
265
- field(name, value) {
266
- if (this.isReservedLogField(name)) {
267
- this.issues.push(`cannot write reserved log field ${JSON.stringify(name)}`);
268
- }
269
- else {
270
- this.extensionFields[name] = value;
271
- }
272
- return this;
273
- }
274
- fields(obj) {
275
- if (!isObject(obj)) {
276
- this.issues.push("fields must be a JSON object");
277
- return this;
278
- }
279
- for (const [key, val] of Object.entries(obj)) {
280
- if (this.isReservedLogField(key)) {
281
- this.issues.push(`cannot write reserved log field ${JSON.stringify(key)}`);
282
- }
283
- else {
284
- this.extensionFields[key] = val;
285
- }
286
- }
287
- return this;
288
- }
289
- extend(obj) {
290
- if (!isObject(obj)) {
291
- this.issues.push("extend must be a JSON object");
292
- return this;
293
- }
294
- for (const [key, val] of Object.entries(obj)) {
295
- if (this.isReservedLogField(key)) {
296
- this.issues.push(`cannot write reserved log field ${JSON.stringify(key)}`);
297
- }
298
- else {
299
- this.extensionFields[key] = val;
300
- }
301
- }
302
- return this;
208
+ constructor(payload) {
209
+ this.payload = payload;
303
210
  }
304
211
  trace(value) {
305
212
  if (value !== undefined) {
@@ -312,17 +219,11 @@ export class JsonLogBuilder {
312
219
  }
313
220
  return this;
314
221
  }
315
- isReservedLogField(name) {
316
- return name === "message" || name === "level" || name === "code";
317
- }
318
222
  build() {
319
223
  if (this.issues.length > 0) {
320
224
  throw new EventBuildError(`Failed to build log event: ${this.issues.join("; ")}`, this.issues);
321
225
  }
322
- const log = { ...this.extensionFields };
323
- log.level = this.level;
324
- log.message = this.message;
325
- const m = { kind: "log", log, trace: this.traceValue };
226
+ const m = { kind: "log", log: this.payload, trace: this.traceValue };
326
227
  return new Event(m);
327
228
  }
328
229
  }
@@ -335,15 +236,12 @@ export function jsonError(code, message) {
335
236
  return new JsonErrorBuilder(code, message);
336
237
  }
337
238
  /** Fluent builder for progress events. */
338
- export function jsonProgress(message) {
339
- return new JsonProgressBuilder(message);
239
+ export function jsonProgress(payload) {
240
+ return new JsonProgressBuilder(payload);
340
241
  }
341
242
  /** Fluent builder for log events. */
342
- export function jsonLog(level, message) {
343
- return new JsonLogBuilder(level, message);
344
- }
345
- function isValidLogLevel(level) {
346
- return level === "debug" || level === "info" || level === "warn" || level === "error";
243
+ export function jsonLog(payload) {
244
+ return new JsonLogBuilder(payload);
347
245
  }
348
246
  /** Validate one protocol event envelope. `strict` also enforces the recommended strict protocol profile (default true). */
349
247
  export function validateProtocolEvent(event, strict = true) {
@@ -372,10 +270,6 @@ export function validateProtocolEvent(event, strict = true) {
372
270
  return;
373
271
  if (!isObject(event.trace))
374
272
  throw new Error("event.trace is required in strict mode");
375
- if (kind === "log")
376
- validateStrictLog(event.log);
377
- if (kind === "progress")
378
- validateStrictProgress(event.progress);
379
273
  if (kind === "error")
380
274
  validateStrictError(event.error);
381
275
  }
@@ -418,25 +312,6 @@ export function validateProtocolStream(events, strict = true) {
418
312
  throw new Error("event stream must contain exactly one terminal result or error");
419
313
  }
420
314
  }
421
- function validateStrictLog(payload) {
422
- if (!isObject(payload))
423
- throw new Error("event.log must be a JSON object in strict mode");
424
- // 0.16: log payload MUST NOT contain code field
425
- if ("code" in payload) {
426
- throw new Error("event.log.code is not allowed in strict mode");
427
- }
428
- if (typeof payload.message !== "string" || payload.message === "") {
429
- throw new Error("event.log.message must be a non-empty string in strict mode");
430
- }
431
- if (!['debug', 'info', 'warn', 'error'].includes(payload.level)) {
432
- throw new Error("event.log.level must be one of debug, info, warn, error in strict mode");
433
- }
434
- }
435
- function validateStrictProgress(payload) {
436
- if (!isObject(payload) || typeof payload.message !== "string" || payload.message === "") {
437
- throw new Error("event.progress.message must be a non-empty string in strict mode");
438
- }
439
- }
440
315
  function validateStrictError(error) {
441
316
  if (!isObject(error))
442
317
  throw new Error("event.error must be a JSON object in strict mode");
@@ -461,12 +336,9 @@ export class EventDecodeError extends Error {
461
336
  }
462
337
  }
463
338
  const ERROR_RESERVED_FIELDS = new Set(["code", "message", "hint", "retryable"]);
464
- const PROGRESS_RESERVED_FIELDS = new Set(["message"]);
465
- const LOG_RESERVED_FIELDS = new Set(["level", "message"]);
466
339
  /**
467
340
  * Parse one protocol line (a single JSON text value), strict-validate it, and
468
- * return a typed decoded event. Extension fields are every payload key beyond
469
- * the required ones, collected into `fields`.
341
+ * return a typed decoded event.
470
342
  */
471
343
  export function decodeProtocolEvent(text) {
472
344
  let parsed;
@@ -506,24 +378,10 @@ export function decodeProtocolEvent(text) {
506
378
  decoded.hint = error.hint;
507
379
  return decoded;
508
380
  }
509
- case "progress": {
510
- const progress = event.progress;
511
- const fields = {};
512
- for (const [k, v] of Object.entries(progress)) {
513
- if (!PROGRESS_RESERVED_FIELDS.has(k))
514
- fields[k] = v;
515
- }
516
- return { kind: "progress", message: progress.message, fields, trace };
517
- }
518
- case "log": {
519
- const log = event.log;
520
- const fields = {};
521
- for (const [k, v] of Object.entries(log)) {
522
- if (!LOG_RESERVED_FIELDS.has(k))
523
- fields[k] = v;
524
- }
525
- return { kind: "log", level: log.level, message: log.message, fields, trace };
526
- }
381
+ case "progress":
382
+ return { kind: "progress", progress: event.progress, trace };
383
+ case "log":
384
+ return { kind: "log", log: event.log, trace };
527
385
  default:
528
386
  // Unreachable: validateProtocolEvent already constrains event.kind.
529
387
  throw new EventDecodeError(`unsupported event kind ${JSON.stringify(event.kind)}`);
@@ -603,42 +461,6 @@ export function redactUrlSecrets(url, options) {
603
461
  const redacted = redactUrlInStr(url, secretNameSet(options ?? {}));
604
462
  return redacted ?? url;
605
463
  }
606
- /**
607
- * Parse a human-readable size string into bytes.
608
- * Accepts numbers followed by explicit units. Decimal units are
609
- * B/kB/MB/GB/TB; binary units are KiB/MiB/GiB/TiB.
610
- * Trims whitespace and rejects ambiguous K/M/G/T units.
611
- */
612
- export function parseSize(s) {
613
- s = s.trim();
614
- if (!s)
615
- return null;
616
- const multipliers = {
617
- KiB: 1024,
618
- MiB: 1024 ** 2,
619
- GiB: 1024 ** 3,
620
- TiB: 1024 ** 4,
621
- kB: 1000,
622
- MB: 1000 ** 2,
623
- GB: 1000 ** 3,
624
- TB: 1000 ** 4,
625
- B: 1,
626
- };
627
- const unit = Object.keys(multipliers).find((candidate) => s.endsWith(candidate));
628
- if (unit === undefined)
629
- return null;
630
- const numStr = s.slice(0, -unit.length);
631
- const mult = multipliers[unit];
632
- if (!numStr || !/^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(numStr))
633
- return null;
634
- const n = Number(numStr);
635
- if (isNaN(n) || n < 0 || !isFinite(n))
636
- return null;
637
- const result = Math.trunc(n * mult);
638
- if (!Number.isSafeInteger(result))
639
- return null;
640
- return result;
641
- }
642
464
  /**
643
465
  * Normalize a fixed UTC offset string to "UTC" or ±HH:MM.
644
466
  * This helper handles fixed offsets only; IANA timezone names and DST rules
@@ -686,6 +508,71 @@ export function isValidRfc3339Time(value) {
686
508
  return true;
687
509
  return value[8] === "." && value.length > 9 && /^[0-9]+$/.test(value.slice(9));
688
510
  }
511
+ /**
512
+ * Return true when value is a complete RFC 3339 date-time, such as
513
+ * 2026-02-14T10:30:00Z or 2026-02-14T10:30:00.5+08:00. Composed from
514
+ * isValidRfc3339Date and isValidRfc3339Time: a full-date, a T/t separator, a
515
+ * partial-time (with optional fractional seconds), and a mandatory time-offset
516
+ * (Z/z or ±HH:MM with HH in 00..23 and MM in 00..59). The offset is required, so
517
+ * a bare 2026-02-14T10:30:00 is rejected; a space separator is rejected; and a
518
+ * leap second (:60) is rejected, matching isValidRfc3339Time. Non-ASCII is rejected.
519
+ */
520
+ export function isValidRfc3339(value) {
521
+ if (value.length < 20 || !/^[\x00-\x7F]*$/.test(value))
522
+ return false;
523
+ if (!isValidRfc3339Date(value.slice(0, 10)))
524
+ return false;
525
+ if (value[10] !== "T" && value[10] !== "t")
526
+ return false;
527
+ const rest = value.slice(11);
528
+ const last = rest[rest.length - 1];
529
+ let partial;
530
+ if (last === "Z" || last === "z") {
531
+ partial = rest.slice(0, -1);
532
+ }
533
+ else {
534
+ if (rest.length < 6 || !isRfc3339NumOffset(rest.slice(-6)))
535
+ return false;
536
+ partial = rest.slice(0, -6);
537
+ }
538
+ return isValidRfc3339Time(partial);
539
+ }
540
+ function isRfc3339NumOffset(offset) {
541
+ if (offset.length !== 6 || (offset[0] !== "+" && offset[0] !== "-") || offset[3] !== ":") {
542
+ return false;
543
+ }
544
+ const hours = parseFixedDigits(offset.slice(1, 3));
545
+ const minutes = parseFixedDigits(offset.slice(4, 6));
546
+ if (hours === null || minutes === null)
547
+ return false;
548
+ return hours <= 23 && minutes <= 59;
549
+ }
550
+ /**
551
+ * Return true when value is a structurally well-formed BCP 47 (RFC 5646) language tag.
552
+ * A grammar-level check, not a registry lookup: hyphen-separated ASCII-alphanumeric
553
+ * subtags (each 1-8 chars) whose primary subtag is a 2-3 letter language code or the
554
+ * x/i privateuse/grandfathered lead. Rejects the POSIX underscore form (zh_CN), empty
555
+ * or misplaced hyphens, non-ASCII, and out-of-range primaries such as chinese. Does not
556
+ * verify that subtags are registered with IANA.
557
+ */
558
+ export function isValidBcp47(value) {
559
+ if (!value)
560
+ return false;
561
+ const subtags = value.split("-");
562
+ for (let index = 0; index < subtags.length; index++) {
563
+ const subtag = subtags[index];
564
+ if (subtag.length < 1 || subtag.length > 8 || !/^[A-Za-z0-9]+$/.test(subtag)) {
565
+ return false;
566
+ }
567
+ if (index === 0) {
568
+ const isLanguage = subtag.length >= 2 && subtag.length <= 3 && /^[A-Za-z]+$/.test(subtag);
569
+ const isSpecial = subtag === "x" || subtag === "i";
570
+ if (!isLanguage && !isSpecial)
571
+ return false;
572
+ }
573
+ }
574
+ return true;
575
+ }
689
576
  function parseUtcOffsetBody(body) {
690
577
  if (!body)
691
578
  return null;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates.
3
3
  */
4
- export { jsonResult, jsonError, jsonProgress, jsonLog, Event, EventBuildError, JsonResultBuilder, JsonErrorBuilder, JsonProgressBuilder, JsonLogBuilder, type LogLevel, validateProtocolEvent, validateProtocolStream, decodeProtocolEvent, type DecodedEvent, type DecodedResult, type DecodedError, type DecodedProgress, type DecodedLog, EventDecodeError, type JsonValue, RedactionPolicy, OutputStyle, type OutputOptions, outputOptionsForPolicy, outputJson, outputYaml, outputPlain, redactedValue, redactUrlSecrets, parseSize, normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time, } from "./format.js";
4
+ export { jsonResult, jsonError, jsonProgress, jsonLog, Event, EventBuildError, JsonResultBuilder, JsonErrorBuilder, JsonProgressBuilder, JsonLogBuilder, type LogLevel, validateProtocolEvent, validateProtocolStream, decodeProtocolEvent, type DecodedEvent, type DecodedResult, type DecodedError, type DecodedProgress, type DecodedLog, EventDecodeError, type JsonValue, RedactionPolicy, OutputStyle, type OutputOptions, outputOptionsForPolicy, outputJson, outputYaml, outputPlain, redactedValue, redactUrlSecrets, normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time, isValidRfc3339, isValidBcp47, } from "./format.js";
5
5
  export { type OutputFormat, type CliEventWriter, CliEmitter, LogFilters, cliParseOutput, cliParseLogFilters, cliOutput, buildCliError, buildCliVersion, cliRenderVersion, cliHandleVersionOrContinue, } from "./cli.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates.
3
3
  */
4
- export { jsonResult, jsonError, jsonProgress, jsonLog, Event, EventBuildError, JsonResultBuilder, JsonErrorBuilder, JsonProgressBuilder, JsonLogBuilder, validateProtocolEvent, validateProtocolStream, decodeProtocolEvent, EventDecodeError, RedactionPolicy, OutputStyle, outputOptionsForPolicy, outputJson, outputYaml, outputPlain, redactedValue, redactUrlSecrets, parseSize, normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time, } from "./format.js";
4
+ export { jsonResult, jsonError, jsonProgress, jsonLog, Event, EventBuildError, JsonResultBuilder, JsonErrorBuilder, JsonProgressBuilder, JsonLogBuilder, validateProtocolEvent, validateProtocolStream, decodeProtocolEvent, EventDecodeError, RedactionPolicy, OutputStyle, outputOptionsForPolicy, outputJson, outputYaml, outputPlain, redactedValue, redactUrlSecrets, normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time, isValidRfc3339, isValidBcp47, } from "./format.js";
5
5
  export { CliEmitter, LogFilters, cliParseOutput, cliParseLogFilters, cliOutput, buildCliError, buildCliVersion, cliRenderVersion, cliHandleVersionOrContinue, } from "./cli.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-first-data",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "A naming convention that lets AI agents understand your data without being told what it means.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^26.1.1",
39
- "tsx": "^4.23.0",
39
+ "tsx": "^4.23.1",
40
40
  "typescript": "^7.0.2"
41
41
  }
42
42
  }