agent-first-data 0.15.0 → 0.16.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.
@@ -0,0 +1,260 @@
1
+ {
2
+ "$schema": "https://agentfirstkit.org/schemas/agent-first-data/registry-v1.schema.json",
3
+ "schema_version": 1,
4
+ "name": "agent-first-data",
5
+ "description": "Machine-readable registry for Agent-First Data suffix semantics.",
6
+ "safe_integer": {
7
+ "minimum": -9007199254740991,
8
+ "maximum": 9007199254740991
9
+ },
10
+ "suffixes": [
11
+ {
12
+ "suffix": "_ns",
13
+ "category": "duration",
14
+ "json_types": ["number"],
15
+ "unit": "nanosecond",
16
+ "formatting": {"strip_key": true, "plain_yaml": "append_unit"},
17
+ "redaction": {"mode": "none"},
18
+ "constraints": {"number": true}
19
+ },
20
+ {
21
+ "suffix": "_us",
22
+ "category": "duration",
23
+ "json_types": ["number"],
24
+ "unit": "microsecond",
25
+ "formatting": {"strip_key": true, "plain_yaml": "append_unit"},
26
+ "redaction": {"mode": "none"},
27
+ "constraints": {"number": true}
28
+ },
29
+ {
30
+ "suffix": "_ms",
31
+ "category": "duration",
32
+ "json_types": ["number"],
33
+ "unit": "millisecond",
34
+ "formatting": {"strip_key": true, "plain_yaml": "duration_ms"},
35
+ "redaction": {"mode": "none"},
36
+ "constraints": {"number": true}
37
+ },
38
+ {
39
+ "suffix": "_s",
40
+ "category": "duration",
41
+ "json_types": ["number"],
42
+ "unit": "second",
43
+ "formatting": {"strip_key": true, "plain_yaml": "append_unit"},
44
+ "redaction": {"mode": "none"},
45
+ "constraints": {"number": true}
46
+ },
47
+ {
48
+ "suffix": "_minutes",
49
+ "category": "duration",
50
+ "json_types": ["number"],
51
+ "unit": "minute",
52
+ "formatting": {"strip_key": true, "plain_yaml": "append_unit_word"},
53
+ "redaction": {"mode": "none"},
54
+ "constraints": {"number": true}
55
+ },
56
+ {
57
+ "suffix": "_hours",
58
+ "category": "duration",
59
+ "json_types": ["number"],
60
+ "unit": "hour",
61
+ "formatting": {"strip_key": true, "plain_yaml": "append_unit_word"},
62
+ "redaction": {"mode": "none"},
63
+ "constraints": {"number": true}
64
+ },
65
+ {
66
+ "suffix": "_days",
67
+ "category": "duration",
68
+ "json_types": ["number"],
69
+ "unit": "day",
70
+ "formatting": {"strip_key": true, "plain_yaml": "append_unit_word"},
71
+ "redaction": {"mode": "none"},
72
+ "constraints": {"number": true}
73
+ },
74
+ {
75
+ "suffix": "_epoch_s",
76
+ "category": "timestamp",
77
+ "json_types": ["integer"],
78
+ "unit": "unix_epoch_second",
79
+ "formatting": {"strip_key": true, "plain_yaml": "rfc3339_datetime"},
80
+ "redaction": {"mode": "none"},
81
+ "constraints": {"integer": true}
82
+ },
83
+ {
84
+ "suffix": "_epoch_ms",
85
+ "category": "timestamp",
86
+ "json_types": ["integer"],
87
+ "unit": "unix_epoch_millisecond",
88
+ "formatting": {"strip_key": true, "plain_yaml": "rfc3339_datetime"},
89
+ "redaction": {"mode": "none"},
90
+ "constraints": {"integer": true}
91
+ },
92
+ {
93
+ "suffix": "_epoch_ns",
94
+ "category": "timestamp",
95
+ "json_types": ["string"],
96
+ "unit": "unix_epoch_nanosecond",
97
+ "formatting": {"strip_key": true, "plain_yaml": "rfc3339_datetime"},
98
+ "redaction": {"mode": "none"},
99
+ "constraints": {"decimal_integer_string": true}
100
+ },
101
+ {
102
+ "suffix": "_rfc3339",
103
+ "category": "timestamp",
104
+ "json_types": ["string"],
105
+ "unit": "rfc3339_datetime",
106
+ "formatting": {"strip_key": true, "plain_yaml": "pass_through"},
107
+ "redaction": {"mode": "none"},
108
+ "constraints": {"rfc3339_datetime": true}
109
+ },
110
+ {
111
+ "suffix": "_rfc3339_date",
112
+ "category": "strict_string",
113
+ "json_types": ["string"],
114
+ "unit": "rfc3339_full_date",
115
+ "formatting": {"strip_key": false, "plain_yaml": "pass_through"},
116
+ "redaction": {"mode": "none"},
117
+ "constraints": {"rfc3339_date": true}
118
+ },
119
+ {
120
+ "suffix": "_rfc3339_time",
121
+ "category": "strict_string",
122
+ "json_types": ["string"],
123
+ "unit": "rfc3339_partial_time",
124
+ "formatting": {"strip_key": false, "plain_yaml": "pass_through"},
125
+ "redaction": {"mode": "none"},
126
+ "constraints": {"rfc3339_time": true}
127
+ },
128
+ {
129
+ "suffix": "_bcp47",
130
+ "category": "strict_string",
131
+ "json_types": ["string"],
132
+ "unit": "bcp47_language_tag",
133
+ "formatting": {"strip_key": false, "plain_yaml": "pass_through"},
134
+ "redaction": {"mode": "none"},
135
+ "constraints": {"string": true}
136
+ },
137
+ {
138
+ "suffix": "_utc_offset",
139
+ "category": "strict_string",
140
+ "json_types": ["string"],
141
+ "unit": "fixed_utc_offset",
142
+ "formatting": {"strip_key": false, "plain_yaml": "pass_through"},
143
+ "redaction": {"mode": "none"},
144
+ "constraints": {"utc_offset": true}
145
+ },
146
+ {
147
+ "suffix": "_bytes",
148
+ "category": "size",
149
+ "json_types": ["integer"],
150
+ "unit": "byte",
151
+ "formatting": {"strip_key": true, "plain_yaml": "binary_size"},
152
+ "redaction": {"mode": "none"},
153
+ "constraints": {"integer": true, "minimum": 0}
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
+ {
165
+ "suffix": "_percent",
166
+ "category": "ratio",
167
+ "json_types": ["number"],
168
+ "unit": "percent",
169
+ "formatting": {"strip_key": true, "plain_yaml": "append_percent"},
170
+ "redaction": {"mode": "none"},
171
+ "constraints": {"number": true}
172
+ },
173
+ {
174
+ "suffix": "_msats",
175
+ "category": "currency",
176
+ "json_types": ["integer", "string"],
177
+ "unit": "millisatoshi",
178
+ "formatting": {"strip_key": true, "plain_yaml": "append_unit"},
179
+ "redaction": {"mode": "none"},
180
+ "constraints": {"integer_or_decimal_integer_string": true}
181
+ },
182
+ {
183
+ "suffix": "_sats",
184
+ "category": "currency",
185
+ "json_types": ["integer", "string"],
186
+ "unit": "satoshi",
187
+ "formatting": {"strip_key": true, "plain_yaml": "append_unit"},
188
+ "redaction": {"mode": "none"},
189
+ "constraints": {"integer_or_decimal_integer_string": true}
190
+ },
191
+ {
192
+ "suffix": "_usd_cents",
193
+ "category": "currency",
194
+ "json_types": ["integer"],
195
+ "unit": "usd_cent",
196
+ "formatting": {"strip_key": true, "plain_yaml": "currency_minor_unit"},
197
+ "redaction": {"mode": "none"},
198
+ "constraints": {"integer": true}
199
+ },
200
+ {
201
+ "suffix": "_eur_cents",
202
+ "category": "currency",
203
+ "json_types": ["integer"],
204
+ "unit": "eur_cent",
205
+ "formatting": {"strip_key": true, "plain_yaml": "currency_minor_unit"},
206
+ "redaction": {"mode": "none"},
207
+ "constraints": {"integer": true}
208
+ },
209
+ {
210
+ "suffix": "_jpy",
211
+ "category": "currency",
212
+ "json_types": ["integer"],
213
+ "unit": "jpy",
214
+ "formatting": {"strip_key": true, "plain_yaml": "currency_major_unit"},
215
+ "redaction": {"mode": "none"},
216
+ "constraints": {"integer": true}
217
+ },
218
+ {
219
+ "suffix": "_{code}_cents",
220
+ "category": "currency",
221
+ "json_types": ["integer"],
222
+ "unit": "currency_minor_unit",
223
+ "formatting": {"strip_key": true, "plain_yaml": "currency_code_minor_unit"},
224
+ "redaction": {"mode": "none"},
225
+ "constraints": {"integer": true, "code_pattern": "^[A-Za-z]{3,4}$"}
226
+ },
227
+ {
228
+ "suffix": "_{code}_micro",
229
+ "category": "currency",
230
+ "json_types": ["integer"],
231
+ "unit": "currency_micro_unit",
232
+ "formatting": {"strip_key": true, "plain_yaml": "currency_code_micro_unit"},
233
+ "redaction": {"mode": "none"},
234
+ "constraints": {"integer": true, "code_pattern": "^[A-Za-z]{3,4}$"}
235
+ },
236
+ {
237
+ "suffix": "_secret",
238
+ "category": "sensitive",
239
+ "json_types": ["any"],
240
+ "unit": null,
241
+ "formatting": {"strip_key": true, "plain_yaml": "redacted"},
242
+ "redaction": {"mode": "whole_value", "marker": "***"},
243
+ "constraints": {"free_text_detection": false}
244
+ },
245
+ {
246
+ "suffix": "_url",
247
+ "category": "sensitive",
248
+ "json_types": ["string"],
249
+ "unit": "url",
250
+ "formatting": {"strip_key": false, "plain_yaml": "scrub_url"},
251
+ "redaction": {
252
+ "mode": "url_components",
253
+ "userinfo_password": true,
254
+ "query_param_secret_suffix": "_secret",
255
+ "marker": "***"
256
+ },
257
+ "constraints": {"single_url_string": true, "free_text_detection": false}
258
+ }
259
+ ]
260
+ }
@@ -0,0 +1,347 @@
1
+ <!-- Canonical source: skills/agent-first-data/references/rules.md.
2
+ Mirrored byte-for-byte into go/python/typescript assets/ by scripts/sync_offline_assets.py.
3
+ Edit only here; run that script (checked by scripts/test.sh) to propagate. -->
4
+
5
+ # Agent-First Data detailed rules
6
+
7
+ Three parts:
8
+
9
+ 1. **Naming** — encode units and semantics in field names so agents parse structured data without external schemas
10
+ 2. **Output** — suffix-driven formatting with key stripping, value formatting, and automatic secret redaction
11
+ 3. **Protocol** — optional JSONL protocol with `code` (required) and `trace` (recommended)
12
+
13
+ ---
14
+
15
+ ## Part 1: Naming Convention
16
+
17
+ The field name is the schema. Always encode units and semantics in the field name.
18
+
19
+ ### Duration
20
+
21
+ | Suffix | Unit | Example |
22
+ |:-------|:-----|:--------|
23
+ | `_ns` | nanoseconds | `gc_pause_ns: 450000` |
24
+ | `_us` | microseconds | `query_us: 830` |
25
+ | `_ms` | milliseconds | `latency_ms: 142` |
26
+ | `_s` | seconds | `dns_ttl_s: 3600` |
27
+ | `_minutes` | minutes | `session_timeout_minutes: 30` |
28
+ | `_hours` | hours | `token_validity_hours: 24` |
29
+ | `_days` | days | `cert_validity_days: 365` |
30
+
31
+ ### Timestamps
32
+
33
+ | Suffix | Format | Example |
34
+ |:-------|:-------|:--------|
35
+ | `_epoch_ms` | milliseconds since Unix epoch | `created_at_epoch_ms: 1707868800000` |
36
+ | `_epoch_s` | seconds since Unix epoch | `cached_epoch_s: 1707868800` |
37
+ | `_epoch_ns` | nanoseconds since Unix epoch as a decimal string | `created_epoch_ns: "1707868800000000000"` |
38
+ | `_rfc3339` | RFC 3339 date-time string | `expires_rfc3339: "2026-02-14T10:30:00Z"` |
39
+
40
+ ### Strict string formats
41
+
42
+ | Suffix | Format | Example |
43
+ |:-------|:-------|:--------|
44
+ | `_bcp47` | BCP-47 language tag string | `language_bcp47: "zh-CN"` |
45
+ | `_utc_offset` | fixed UTC offset string | `timezone_utc_offset: "+08:00"` |
46
+ | `_rfc3339_date` | RFC 3339 full-date string | `invoice_due_rfc3339_date: "2026-06-13"` |
47
+ | `_rfc3339_time` | RFC 3339 partial-time string | `market_open_rfc3339_time: "09:30:00"` |
48
+
49
+ `*_bcp47` identifies a BCP-47 language tag string. AFDATA does not implement the full BCP-47 registry; tools may validate tags when needed.
50
+
51
+ `*_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
+
53
+ `*_rfc3339_date` identifies an RFC 3339 `full-date` string (`YYYY-MM-DD`). It is a calendar date, not an instant, and has no time, offset, or timezone.
54
+
55
+ `*_rfc3339_time` identifies an RFC 3339 `partial-time` string (`HH:MM:SS[.fraction]`). It is a time-of-day, not an instant, and MUST NOT include `Z`, `±HH:MM`, IANA timezone names, or other timezone annotations. Use `_rfc3339` or `_epoch_*` for instants.
56
+
57
+ Do not create companion timezone-name fields as an AFDATA core pattern. If a future tool needs IANA timezone semantics with a timestamp, prefer a self-contained standard value such as RFC 9557.
58
+
59
+ Avoid magic string sentinels such as `"auto"` inside strict-format fields. If a tool needs auto/default behavior, define it in that tool's own config semantics, not as an AFDATA-wide rule.
60
+
61
+ ### Size
62
+
63
+ | Suffix | Example |
64
+ |:-------|:--------|
65
+ | `_bytes` | `payload_bytes: 456789` (non-negative integer) |
66
+ | `_size` | `buffer_size: "10MiB"` or `"10MB"` (config files only, human-readable) |
67
+
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
+
72
+ ### Percentage
73
+
74
+ | Suffix | Example |
75
+ |:-------|:--------|
76
+ | `_percent` | `cpu_percent: 85` |
77
+
78
+ ### Currency
79
+
80
+ Bitcoin:
81
+
82
+ | Suffix | Example |
83
+ |:-------|:--------|
84
+ | `_msats` | `balance_msats: 97900` |
85
+ | `_sats` | `withdrawn_sats: 1234` |
86
+
87
+ Do not use a floating `_btc` suffix. Use integer `_sats` or `_msats` instead; values outside the JSON safe-integer range should be decimal strings.
88
+
89
+ Fiat — `_{iso4217}_cents` for currencies with 1/100 subdivision, `_{iso4217}` for currencies without. Generic `_{code}_cents` matches only 3-4 ASCII letters:
90
+
91
+ | Suffix | Example |
92
+ |:-------|:--------|
93
+ | `_usd_cents` | `price_usd_cents: 999` |
94
+ | `_eur_cents` | `price_eur_cents: 850` |
95
+ | `_jpy` | `price_jpy: 1500` |
96
+ | `_usdt_cents` | `deposit_usdt_cents: 1000` |
97
+
98
+ Sub-cent precision — `_{code}_micro`, integer millionths (10⁻⁶) of the major unit (`cost_usd_micro: 170000` = $0.17). The fiat analog of `_msats`: when cents are too coarse (per-token pricing, metered costs), use a smaller integer unit, never decimal cents. `_{code}_cents` for user-facing amounts, `_{code}_micro` for high-precision internal accounting.
99
+
100
+ ### Sensitive
101
+
102
+ | Suffix | Handling | Example |
103
+ |:-------|:---------|:--------|
104
+ | `_secret` | redact the entire value/subtree to `***` | `api_key_secret: "sk-or-v1-abc..."` |
105
+ | `_url` | scrub secrets *inside* the URL value, keep the rest | `callback_url: "https://h/cb?code_secret=..."` |
106
+
107
+ All CLI output formats (JSON, YAML, Plain) automatically redact `_secret` fields. Matching recognizes `_secret` and `_SECRET` only — no mixed case. The entire `_secret` value/subtree becomes `***`, including objects and arrays. For legacy fields that cannot be renamed, configure `OutputOptions.redaction` with `secret_names`/`secretNames` such as `["api_key", "authorization"]`; names match exact field names at any nesting level; no trim, case folding, hyphen/underscore normalization, globs, regex, or substring matching. AFDATA does not define named redaction profiles; use the default, `secret_names`, `RedactionTraceOnly`, or `RedactionNone` deliberately at the serialization boundary. Callers that need schema-preserving YAML/plain rendering can pass `OutputOptions` with the `Raw` output style.
108
+
109
+ `***` means only one thing: AFDATA redacted a sensitive value. Do not use it for serialization failure, truncation, unsupported types, or “maybe secret” guesses.
110
+
111
+ Name URL-valued fields `_url` so the userinfo password and any `_secret`/`secret_names` query parameter inside them are scrubbed automatically (the rest of the URL is preserved; the suffix is not stripped). For a URL inside a free-form message, redact it with `redact_url_secrets` before interpolating — `_url` only fires on whole-URL field values, never on prose. AFDATA does not scan arbitrary free text for secrets.
112
+
113
+ **`_url` scrubs the userinfo password and suffix-named params only — not arbitrary credential params.** Common parameters like `?access_token=`, `?api_key=`, `?code=`, `?sig=` are NOT redacted unless their name ends in `_secret` or is passed in `secret_names`. Rename params you own to the suffix (`?access_token_secret=`); list the rest in `secret_names`. A `_url` value that is not a clean scheme-prefixed URL but carries internal whitespace or an `@` credential sigil (e.g. a schemeless `user:pass@host/db`) is redacted wholesale to `***` (fail-closed).
114
+
115
+ ### Environment variables
116
+
117
+ Same suffixes, `UPPER_SNAKE_CASE`:
118
+
119
+ ```
120
+ DATABASE_URL_SECRET=postgres://user:pass@host/db
121
+ CACHE_TTL_S=3600
122
+ TOKEN_VALIDITY_HOURS=24
123
+ ```
124
+
125
+ ### No suffix needed
126
+
127
+ Fields whose meaning is obvious: `redb_path`, `proof_count`, `search_enabled`, `method`, `domain`, `model`. (URL fields are the exception — use `_url` so embedded secrets are scrubbed.)
128
+
129
+ ### Database columns
130
+
131
+ Use suffixes on generic types (`INTEGER`, `BIGINT`, `TEXT`). Native types that carry semantics (`TIMESTAMPTZ`, `INTERVAL`) don't need suffixes.
132
+
133
+ | Column | Type | Suffix? | Why |
134
+ |:-------|:-----|:--------|:----|
135
+ | `created_at` | `TIMESTAMPTZ` | no | type says timestamp |
136
+ | `duration_ms` | `INTEGER` | yes | integer is ambiguous |
137
+ | `api_key_secret` | `TEXT` | yes | enables auto-redaction |
138
+ | `retry_count` | `INTEGER` | no | meaning obvious |
139
+
140
+ ORM struct fields preserve the suffix: `duration_ms: i64`, not `duration: i64`.
141
+
142
+ ### Common mistakes
143
+
144
+ | Bad | Good | Why |
145
+ |:----|:-----|:----|
146
+ | `timeout: 30` | `timeout_s: 30` | 30 what? seconds? ms? |
147
+ | `timestamp: 1707868800` | `cached_epoch_s: 1707868800` | what unit? what event? |
148
+ | `size: 456789` | `payload_bytes: 456789` | bytes? KiB? |
149
+ | `price: 999` | `price_usd_cents: 999` | what currency? what unit? |
150
+ | `latency: 142` | `latency_ms: 142` | seconds? milliseconds? |
151
+ | `api_key: "sk-..."` | `api_key_secret: "sk-..."` | won't be auto-redacted |
152
+ | `cpu: 85` | `cpu_percent: 85` | 85 what? |
153
+ | `buffer: "10MiB"` | `buffer_size: "10MiB"` | only `_size` gets parsed |
154
+
155
+ ---
156
+
157
+ ## Part 2: Output Processing
158
+
159
+ Three output formats via `output_json()`, `output_yaml()`, `output_plain()`. Default YAML and Plain apply key stripping + value formatting. Optional `options` (Python/TS keyword arg; Rust/Go `_with_options` variant).
160
+
161
+ ### Formats
162
+
163
+ - **JSON** — single-line, original keys, raw values, no sorting (machine-readable), secrets redacted
164
+ - **YAML** — multi-line, formatting suffixes stripped, values formatted, secrets redacted by default
165
+ - **Plain** — single-line logfmt, formatting suffixes stripped, values formatted, secrets redacted by default
166
+
167
+ ### Key stripping (YAML and Plain)
168
+
169
+ Remove recognized formatting suffix from key. Longest match first, exact lowercase or uppercase only:
170
+
171
+ 1. `_epoch_ms`, `_epoch_s`, `_epoch_ns`
172
+ 2. `_usd_cents`, `_eur_cents`, `_{code}_cents`, `_{code}_micro` (`code` is 3-4 ASCII letters)
173
+ 3. `_rfc3339`, `_minutes`, `_hours`, `_days`
174
+ 4. `_msats`, `_sats`, `_bytes`, `_percent`, `_secret`
175
+ 5. `_jpy`, `_ns`, `_us`, `_ms`, `_s`
176
+
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.
178
+
179
+ ### Value formatting (YAML and Plain)
180
+
181
+ - `_ms` with absolute value < 1000 → `{n}ms`; absolute value ≥ 1000 → seconds (`1280` → `1.28s`, `-1500` → `-1.5s`)
182
+ - `_s`, `_ns`, `_us` → append unit (`3600s`, `450000ns`, `830μs`)
183
+ - `_minutes`, `_hours`, `_days` → append unit (`30 minutes`)
184
+ - `_epoch_ms`/`_epoch_s`/decimal-string `_epoch_ns` → RFC 3339 (negative = pre-1970)
185
+ - `_rfc3339` → pass through
186
+ - `_bytes` → human-readable (`456789` → `446.1KiB`); negative and fractional byte values fall through raw
187
+ - `_size` → pass through
188
+ - `_percent` → append `%`
189
+ - `_msats` → `{n}msats`, `_sats` → `{n}sats`
190
+ - `_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
191
+ - `_secret` → `***` (the redaction phase already replaced the subtree)
192
+ - `_bcp47`, `_utc_offset`, `_rfc3339_date`, `_rfc3339_time` → pass through unchanged
193
+
194
+ **Type constraints**: `_bytes`/`_epoch_*` require integer. `_usd_cents`/`_eur_cents`/`_jpy`/`_{code}_cents`/`_{code}_micro` require non-negative integer. Duration/Bitcoin/`_percent` accept any number. Wrong type → raw value + original key.
195
+
196
+ ### Plain logfmt details
197
+
198
+ - Nested keys use dot notation: `trace.duration=1.28s`
199
+ - Keys and values with ASCII space, tab/newline, VT, FF, NBSP, `=`, `"`, or `\` are quoted/escaped so each record stays one physical line
200
+ - Arrays comma-joined: `fields=email,age`
201
+ - Null → empty value: `RUST_LOG=`
202
+ - Sort by full dot path (JCS / UTF-16 code unit order)
203
+
204
+ ### Key ordering
205
+
206
+ YAML and Plain sort keys (after stripping) by UTF-16 code unit order (JCS, RFC 8785). For ASCII keys this equals byte-order sorting.
207
+
208
+ ---
209
+
210
+ ## Part 3: Protocol Template (Optional)
211
+
212
+ Every protocol event uses `kind` plus one same-named payload:
213
+
214
+ | `kind` | When |
215
+ |:-------|:-----|
216
+ | `"log"` | Diagnostic event; `log.event` may identify startup/request/retry/redirect |
217
+ | `"progress"` | Non-terminal status/progress |
218
+ | `"result"` | Terminal success |
219
+ | `"error"` | Terminal error with non-empty `error.code` and `error.message` |
220
+
221
+ Channel policy:
222
+ - `stdout` is the only protocol/log stream for machine-readable events
223
+ - runtime protocol events MUST NOT be emitted on `stderr`
224
+ - `stderr` is reserved for unrecoverable pre-protocol startup failures only
225
+
226
+ Optional stream redirection:
227
+ - use `--stdout-file <PATH>` to redirect stdout bytes to a file
228
+ - use `--stderr-file <PATH>` to redirect stderr bytes to a file
229
+ - keep `--output` reserved for stdout format selection (`json`, `yaml`, `plain`, and help-specific `markdown`)
230
+ - do not convert stderr diagnostics to JSON; Rust panics, Python tracebacks, and runtime errors stay native stderr bytes
231
+ - treat this as stream destination control, not a second protocol stream and not stream copying
232
+ - do not implement application-level rotation
233
+
234
+ Recommended enforcement:
235
+ - Rust: enable clippy `print_stderr = "deny"` and disallow `std::eprintln` / `std::io::stderr`
236
+ - Go/Python/TypeScript: add source-policy tests that fail if runtime sources reference stderr APIs
237
+
238
+ ### Templates
239
+
240
+ ```json
241
+ {"kind":"log","log":{"message":"startup","level":"info","event":"startup","config":{...}},"trace":{}}
242
+ {"kind":"progress","progress":{"message":"syncing","current":3,"total":10},"trace":{"duration_ms":5}}
243
+ {"kind":"result","result":{...},"trace":{"duration_ms":12,"source":"redb"}}
244
+ {"kind":"error","error":{"code":"not_found","message":"user not found","resource":"user","id":123,"retryable":false},"trace":{"duration_ms":8}}
245
+ ```
246
+
247
+ Always include `trace` for execution context: duration, token counts, cost, data source.
248
+ Startup payload fields are tool-defined; `config` is recommended, while `version`/`argv`/`args`/`env` are optional.
249
+
250
+ ### Same structure, any transport
251
+
252
+ | Transport | Format |
253
+ |:----------|:-------|
254
+ | CLI stdout | JSONL |
255
+ | REST API | JSON body |
256
+ | MCP tool | JSON |
257
+ | SSE stream | JSONL |
258
+
259
+ All use `kind` plus the same-named payload and optional `trace`. Do not split protocol events across `stdout` and `stderr`.
260
+
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
+
263
+ ---
264
+
265
+ ## Library Usage
266
+
267
+ Use the local language README for installation and the full overview/spec for API reference. Keep this skill focused on naming, output, protocol, logging, and review rules rather than duplicating import snippets that drift across languages.
268
+
269
+ Required cross-language behavior to rely on:
270
+
271
+ - Output helpers redact before formatting: `output_json()`, `output_yaml()`, `output_plain()` (with optional `options` in Python/TS).
272
+ - Use `redacted_value()` for raw HTTP/MCP/SSE serialization paths that bypass `output_json()`.
273
+ - Use `redact_url_secrets()` for URLs embedded in log messages or prose before interpolating them into output.
274
+ - Use `cli_parse_output()`, `cli_parse_log_filters()`, `cli_output()`, `build_cli_error()`, and the version helper for CLI tools instead of custom parsing/error envelopes.
275
+ - `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
276
+ - Rust CLIs should call `cli_handle_version_or_continue()` before clap parsing so `--version --output json|yaml|plain` emits a structured `kind:"result"` event with `result.version` instead of clap's plain text. Use `VersionConfig::conventional_default()` so bare `--version` stays human text while explicit `--output` remains structured.
277
+ - Use the protocol reader `decode_protocol_event(text)` to parse and strict-validate a single JSON text line, receiving a typed decoded event.
278
+
279
+ ## AFDATA Logging
280
+
281
+ Long-running services and processes depending on structured logging (tonic, sqlx, hyper, etc. via tracing) use a logging init function to capture the full process. One-time CLI output uses `json_log()` + `cli_output()` or `CliEmitter`.
282
+
283
+ ### Init (Rust only; pick one format per process)
284
+
285
+ | Format | Rust |
286
+ |:-------|:-----|
287
+ | **Unified** | `afdata_tracing::try_init(filter, format, redactor)` |
288
+
289
+ Go, Python, and TypeScript have no built-in logging integration; emit log events via `json_log()` + output helpers or construct them explicitly.
290
+
291
+ Rust requires `cargo add agent-first-data --features tracing`.
292
+
293
+ ### Spans (Rust only; add fields to all log events in scope)
294
+
295
+ ```rust
296
+ // Rust — tracing spans
297
+ let span = info_span!("request", request_id = %uuid);
298
+ let _guard = span.enter();
299
+ ```
300
+
301
+ For Go, Python, and TypeScript: emit span fields explicitly in each log event via `.field()` or equivalent event construction.
302
+
303
+ ### Output fields
304
+
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
+
307
+ 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
+ ## CLI Flags
310
+
311
+ CLI tools that use AFDATA should support output and logging flags:
312
+
313
+ ```
314
+ --output json|yaml|plain # default is tool-defined (interactive → yaml, scripting/logging → json)
315
+ --log startup,request,progress,retry,redirect
316
+ --verbose # shorthand for all log categories
317
+ ```
318
+
319
+ - Protocol output (`build_json_*` + `output_*`) follows `--output`
320
+ - Log format follows `--output` or a separate `--log-format` flag if independent control is needed
321
+ - Help scope and format are orthogonal: `--recursive` decides one-level vs recursive, `--output` decides plain/json/yaml/markdown. Human help is one-level `--help` (and scoped `myapp sub --help`); agents/docs use `--help --recursive` and add `--output json|yaml|markdown` for a recursive export. A bare `--recursive` without `--help` falls through to the app's own parser. `markdown` is help-only. Rust: use `cli_handle_help_or_continue()` / `cli_render_help_with_options()` from the `cli-help` feature; wrappers `cli_render_help()` and `cli_render_help_markdown()` remain available for recursive output.
322
+
323
+ ## Review Checklist
324
+
325
+ When reviewing code that produces structured output:
326
+
327
+ 1. Every numeric field with a unit has the correct suffix (`_ms`, `_bytes`, `_sats`, `_percent`, etc.)
328
+ 2. Timestamps use `_epoch_ms` / `_epoch_s` / `_rfc3339`; date-only/time-only strings use `_rfc3339_date` / `_rfc3339_time`
329
+ 3. Sensitive values end in `_secret` and are redacted in all output paths
330
+ 4. Protocol transport payloads and CLI events use `kind`, the same-named payload (`event[kind]`), and top-level `trace`; tool-defined codes live inside the payload
331
+ 5. Config files use the same suffixes as output
332
+ 6. No unit-less ambiguous fields (`timeout: 30` — 30 what?)
333
+
334
+ ## Skill Evaluation Checklist
335
+
336
+ Use this checklist when the skill is evaluating a change, not just documenting conventions:
337
+
338
+ 1. Trigger on any change to structured fields, configs, logs, CLI output, protocol envelopes, MCP/HTTP/SSE payloads, database columns, wire fields, or persisted JSON.
339
+ 2. Use `registry.json` for suffix semantics and `protocol-v1.schema.json` for envelope validity. Do not invent substitute suffix rules or shrink the schema to fit a local implementation.
340
+ 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
+ 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
+ 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
+ 7. Environment variables follow `UPPER_SNAKE_CASE` with the same suffixes
345
+ 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
+ 9. Database columns use AFDATA suffixes on generic types (`duration_ms INTEGER`, not `duration INTEGER`); native types like `TIMESTAMPTZ` don't need suffixes
347
+ 10. CLI flag parsing uses `cli_parse_output()`/`cli_parse_log_filters()`/`build_cli_error()`/version helpers — not custom reimplementations; uses `try_parse()` not `parse()` in Rust so clap errors go to stdout as JSONL