agent-first-data 0.17.3__tar.gz → 0.19.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/PKG-INFO +7 -7
  2. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/README.md +5 -5
  3. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/agent_first_data/__init__.py +6 -10
  4. agent_first_data-0.19.0/agent_first_data/assets/skills/agent-first-data/SKILL.md +280 -0
  5. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/agent_first_data/assets/skills/agent-first-data/references/rules.md +36 -26
  6. agent_first_data-0.19.0/agent_first_data/cli.py +512 -0
  7. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/agent_first_data/format.py +175 -107
  8. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/agent_first_data/skill.py +3 -3
  9. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/agent_first_data.egg-info/PKG-INFO +7 -7
  10. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/pyproject.toml +2 -2
  11. agent_first_data-0.19.0/tests/test_cli.py +588 -0
  12. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/tests/test_format.py +134 -50
  13. agent_first_data-0.19.0/tests/test_no_stderr_policy.py +48 -0
  14. agent_first_data-0.17.3/agent_first_data/assets/skills/agent-first-data/SKILL.md +0 -134
  15. agent_first_data-0.17.3/agent_first_data/cli.py +0 -330
  16. agent_first_data-0.17.3/tests/test_cli.py +0 -323
  17. agent_first_data-0.17.3/tests/test_no_stderr_policy.py +0 -25
  18. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/agent_first_data/assets/protocol-v1.schema.json +0 -0
  19. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/agent_first_data/assets/registry.json +0 -0
  20. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/agent_first_data/assets/skills/agent-first-data/references/protocol-v1.schema.json +0 -0
  21. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/agent_first_data/assets/skills/agent-first-data/references/registry.json +0 -0
  22. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/agent_first_data/stream_redirect.py +0 -0
  23. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/agent_first_data.egg-info/SOURCES.txt +0 -0
  24. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/agent_first_data.egg-info/dependency_links.txt +0 -0
  25. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/agent_first_data.egg-info/top_level.txt +0 -0
  26. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/setup.cfg +0 -0
  27. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/tests/test_skill.py +0 -0
  28. {agent_first_data-0.17.3 → agent_first_data-0.19.0}/tests/test_stream_redirect.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.17.3
4
- Summary: A naming convention that lets AI agents understand your data without being told what it means.
3
+ Version: 0.19.0
4
+ Summary: A naming convention that lets AI agents understand your data without being told what it means, plus a CLI and library for reading and safely editing structured JSON, TOML, YAML, dotenv, and INI documents.
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
7
7
  Requires-Python: >=3.9
@@ -14,7 +14,7 @@ pip install agent-first-data
14
14
  ```
15
15
 
16
16
  ```python
17
- from agent_first_data import json_result, output_json, output_plain
17
+ from agent_first_data import json_result, render, OutputFormat
18
18
 
19
19
  value = json_result(
20
20
  {
@@ -24,11 +24,11 @@ value = json_result(
24
24
  }
25
25
  ).build()
26
26
 
27
- print(output_json(value))
28
- print(output_plain(value))
27
+ print(render(value, OutputFormat.JSON))
28
+ print(render(value, OutputFormat.PLAIN))
29
29
  ```
30
30
 
31
- Useful names use Python casing: `output_json`, `output_yaml`, `output_plain` (each takes an optional keyword-only `options=`), `redacted_value`, `redact_secrets_in_place`, `redact_url_secrets`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `cli_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `decode_protocol_event`. Skill admin and stream-redirect helpers live in `agent_first_data.skill` and `agent_first_data.stream_redirect` (import the submodule directly).
31
+ Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `decode_protocol_event`. Skill admin and stream-redirect helpers live in `agent_first_data.skill` and `agent_first_data.stream_redirect` (import the submodule directly).
32
32
 
33
33
  Logging is available through `init_logging_json`, `init_logging_plain`, `init_logging_yaml`, `span`, and `get_logger`.
34
34
 
@@ -42,7 +42,7 @@ init_logging_json("INFO", secret_names=("authorization",))
42
42
 
43
43
  - Default redaction replaces every `_secret` or configured secret-name subtree with `***`, including objects and arrays.
44
44
  - `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed and internal whitespace redacts the whole field.
45
- - YAML/plain quote and escape keys as well as values, sort by UTF-16 code unit order, and render nested objects in arrays as canonical JSON.
45
+ - 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.
46
46
  - Logging records use `kind:"log"` with a nested `log` payload and a separate `level` field, so error-level logs are not terminal protocol errors.
47
47
  - `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
48
48
  - Use `cli_handle_version_or_continue()` before argument parsing so bare `--version` stays conventional and `--version --output json|yaml|plain` stays structured.
@@ -5,7 +5,7 @@ pip install agent-first-data
5
5
  ```
6
6
 
7
7
  ```python
8
- from agent_first_data import json_result, output_json, output_plain
8
+ from agent_first_data import json_result, render, OutputFormat
9
9
 
10
10
  value = json_result(
11
11
  {
@@ -15,11 +15,11 @@ value = json_result(
15
15
  }
16
16
  ).build()
17
17
 
18
- print(output_json(value))
19
- print(output_plain(value))
18
+ print(render(value, OutputFormat.JSON))
19
+ print(render(value, OutputFormat.PLAIN))
20
20
  ```
21
21
 
22
- Useful names use Python casing: `output_json`, `output_yaml`, `output_plain` (each takes an optional keyword-only `options=`), `redacted_value`, `redact_secrets_in_place`, `redact_url_secrets`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `cli_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `decode_protocol_event`. Skill admin and stream-redirect helpers live in `agent_first_data.skill` and `agent_first_data.stream_redirect` (import the submodule directly).
22
+ Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `decode_protocol_event`. Skill admin and stream-redirect helpers live in `agent_first_data.skill` and `agent_first_data.stream_redirect` (import the submodule directly).
23
23
 
24
24
  Logging is available through `init_logging_json`, `init_logging_plain`, `init_logging_yaml`, `span`, and `get_logger`.
25
25
 
@@ -33,7 +33,7 @@ init_logging_json("INFO", secret_names=("authorization",))
33
33
 
34
34
  - Default redaction replaces every `_secret` or configured secret-name subtree with `***`, including objects and arrays.
35
35
  - `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed and internal whitespace redacts the whole field.
36
- - YAML/plain quote and escape keys as well as values, sort by UTF-16 code unit order, and render nested objects in arrays as canonical JSON.
36
+ - 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.
37
37
  - Logging records use `kind:"log"` with a nested `log` payload and a separate `level` field, so error-level logs are not terminal protocol errors.
38
38
  - `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
39
39
  - Use `cli_handle_version_or_continue()` before argument parsing so bare `--version` stays conventional and `--version --output json|yaml|plain` stays structured.
@@ -21,11 +21,8 @@ from agent_first_data.format import (
21
21
  DecodedLog,
22
22
  decode_protocol_event,
23
23
  RedactionPolicy,
24
- OutputStyle,
24
+ PlainStyle,
25
25
  OutputOptions,
26
- output_json,
27
- output_yaml,
28
- output_plain,
29
26
  redacted_value,
30
27
  redact_url_secrets,
31
28
  normalize_utc_offset,
@@ -37,11 +34,12 @@ from agent_first_data.format import (
37
34
 
38
35
  from agent_first_data.cli import (
39
36
  OutputFormat,
37
+ OutputTo,
40
38
  LogFilters,
41
39
  CliEmitter,
42
40
  cli_parse_output,
43
41
  cli_parse_log_filters,
44
- cli_output,
42
+ render,
45
43
  build_cli_error,
46
44
  build_cli_version,
47
45
  cli_render_version,
@@ -69,11 +67,8 @@ __all__ = [
69
67
  "DecodedLog",
70
68
  "decode_protocol_event",
71
69
  "RedactionPolicy",
72
- "OutputStyle",
70
+ "PlainStyle",
73
71
  "OutputOptions",
74
- "output_json",
75
- "output_yaml",
76
- "output_plain",
77
72
  "redacted_value",
78
73
  "redact_url_secrets",
79
74
  "normalize_utc_offset",
@@ -82,11 +77,12 @@ __all__ = [
82
77
  "is_valid_rfc3339_date",
83
78
  "is_valid_rfc3339_time",
84
79
  "OutputFormat",
80
+ "OutputTo",
85
81
  "LogFilters",
86
82
  "CliEmitter",
87
83
  "cli_parse_output",
88
84
  "cli_parse_log_filters",
89
- "cli_output",
85
+ "render",
90
86
  "build_cli_error",
91
87
  "build_cli_version",
92
88
  "cli_render_version",
@@ -0,0 +1,280 @@
1
+ ---
2
+ name: agent-first-data
3
+ description: Apply and review the formal Agent-First Data specification using the Rust/Go/Python/TypeScript libraries or afdata CLI for naming, redaction, formatting, protocol envelopes, logging, linting, validation, and reading or safely editing structured JSON/TOML/YAML/dotenv/INI documents (and the +++/--- frontmatter block of a Markdown page) by dot-path. Use proactively when writing or reviewing structured data, configs, logs, transport payloads, database/wire fields, CLI output, compatibility-sensitive public and persistent field names, or when reading or editing a config/document/Markdown-frontmatter file from the shell.
4
+ ---
5
+
6
+ <!-- Canonical source: skills/agent-first-data/SKILL.md.
7
+ Mirrored byte-for-byte into go/python/typescript assets/ by scripts/sync_offline_assets.py.
8
+ Edit only here; run that script (checked by scripts/test.sh) to propagate. -->
9
+
10
+ # Agent-First Data
11
+
12
+ Use AFDATA when naming fields or reviewing structured output. The contract is naming-based: suffixes communicate units, formatting, and redaction semantics without relying on free-text inference.
13
+
14
+ ## Specification workflow
15
+
16
+ Treat the formal specification as normative, not the skill summary or an
17
+ example payload. In a repository checkout, read these spore-root-relative
18
+ sources as needed:
19
+
20
+ - `spec/agent-first-data.md` — complete cross-language naming, formatting,
21
+ redaction, protocol, logging, and CLI contract.
22
+ - `spec/registry.json` — exact suffix metadata.
23
+ - `spec/protocol-v1.schema.json` — protocol event schema.
24
+ - `spec/fixtures/` — cross-language conformance cases.
25
+
26
+ When only the installed skill is available, use the bundled equivalents:
27
+
28
+ - `references/rules.md` — detailed suffix rules, output formatting, protocol templates, logging, CLI flags, and review checklist.
29
+ - `references/registry.json` — machine-readable suffix registry; use this as the exact source for suffix metadata.
30
+ - `references/protocol-v1.schema.json` — protocol event schema for `kind:"result"`, `kind:"error"`, `kind:"progress"`, and `kind:"log"` envelopes.
31
+
32
+ If an example or README conflicts with the formal spec, follow the spec and
33
+ report the discrepancy. Do not invent suffix meanings or reduce the protocol
34
+ schema.
35
+
36
+ ## Library workflow
37
+
38
+ Use the library at serialization boundaries so redaction and formatting are
39
+ not reimplemented ad hoc. Rust applications can build a typed protocol event
40
+ and render it in the required output format:
41
+
42
+ ```bash
43
+ cargo add agent-first-data --no-default-features
44
+ pip install agent-first-data
45
+ npm install agent-first-data
46
+ go get github.com/agentfirstkit/agent-first-data/go
47
+ ```
48
+
49
+ ```rust
50
+ use agent_first_data::{json_result, render, OutputFormat, OutputOptions};
51
+ use serde_json::json;
52
+
53
+ let event = json_result(json!({
54
+ "latency_ms": 1280,
55
+ "api_key_secret": "sk-live-example",
56
+ })).build();
57
+ let rendered = render(event.as_value(), OutputFormat::Json, &OutputOptions::default());
58
+ assert!(rendered.contains("\"api_key_secret\":\"***\""));
59
+ ```
60
+
61
+ For a finite CLI execution implemented in Rust, create one `CliEmitter`, select
62
+ `OutputFormat` through `cli_parse_output`, enable strict protocol, and emit one
63
+ terminal result or error. Handle version before clap with
64
+ `cli_handle_version_or_continue(raw_args, name, version)`.
65
+ For non-CLI HTTP/MCP/SSE serialization, call `redacted_value()` or
66
+ `redact_url_secrets()` explicitly at the boundary.
67
+
68
+ The same library contract is available for Go, Python, and TypeScript. Use the
69
+ runtime's native names while preserving the same builders, output formats,
70
+ redaction behavior, registry, schema, and conformance fixtures; consult its
71
+ root-level `go/README.md`, `python/README.md`, or `typescript/README.md` when
72
+ working in that runtime.
73
+
74
+ ## Naming and redaction decisions
75
+
76
+ - Add unit suffixes to ambiguous numeric fields: `_ms`, `_s`, `_bytes`, `_percent`, `_sats`, `_msats`, currency suffixes, and timestamp suffixes.
77
+ - Use `_secret` for values/subtrees that must redact to `***` in every output format.
78
+ - Use `_url` only for whole URL values whose userinfo password or suffix-named secret query params should be scrubbed.
79
+ - Do not scan arbitrary free text for secrets. Rename fields or configure explicit secret names at the serialization boundary.
80
+ - Before changing public API, wire, database, or persistent field names, report the compatibility impact and get approval.
81
+
82
+ ## Evaluation rules
83
+
84
+ - Trigger this skill for field naming, structured output, configs, logs, CLI output, protocol events, MCP/HTTP/SSE payloads, database columns, wire fields, or persisted JSON. Do not wait for the user to say "AFDATA" when the work changes those surfaces.
85
+ - In a repository checkout, treat `spec/agent-first-data.md`,
86
+ `spec/registry.json`, and `spec/protocol-v1.schema.json` as authoritative; in
87
+ an installed skill, use their bundled `references/` equivalents. Do not
88
+ replace them with invented suffix meanings, a reduced schema, or free-text
89
+ interpretation.
90
+ - For secrets, fix the field name or serializer configuration. Use `_secret` or configured exact secret names; do not rely on scanning arbitrary strings or object debug output.
91
+ - Run `afdata lint` on JSON/JSONL examples, JSON Schema, MCP schemas, or serialized samples when the CLI is available. Run `afdata validate` on finite protocol events/streams. If a check cannot run, report that explicitly.
92
+ - Before modifying an external project's public API, wire format, database schema, or persisted field names, stop and report the compatibility impact; request approval before applying the change.
93
+
94
+ ## Redaction behavior contract
95
+
96
+ Fields named `_secret` or `_url` and values passing through AFDATA output rendering (`render`, in any format) are automatically redacted: secrets become `***`, URL userinfo passwords and suffix-named query parameters are scrubbed. When serializing outside output formatters (HTTP, MCP, SSE), call `redacted_value()` or `redact_url_secrets()` at the serialization boundary. PII redaction, domain-specific privacy policies (header allowlists, API scope sensitivity), and per-field secret naming are each spore's responsibility; the library provides field-name-based mechanics only.
97
+
98
+ ## Logging behavior contract
99
+
100
+ One-time CLI output uses `json_log()` or `CliEmitter` for a single event; serialization via `render()` applies output formatting. Construct the emitter in finite mode for a one-shot command (`result`→stdout, `error`/`progress`/`log`→stderr) or stream mode for an interleaved event stream (all events on one destination) — see the Output stream contract. Long-running services or processes depending on structured logging (tonic, sqlx, hyper, etc. via tracing) initialize with `afdata_tracing::try_init()` (Rust only) to wire process-wide logging; other languages emit log events explicitly via builders or integrate their own structured logging.
101
+
102
+ ## CLI workflow
103
+
104
+ Install `afdata` when a shell workflow needs validation, formatting, or skill
105
+ administration without embedding a library:
106
+
107
+ ```bash
108
+ cargo install agent-first-data
109
+ ```
110
+
111
+ Use it for deterministic checks:
112
+
113
+ ```bash
114
+ afdata lint payload.json
115
+ afdata validate events.jsonl
116
+ afdata render payload.json --output yaml
117
+ afdata skill validate skills/example-skill
118
+ ```
119
+
120
+ - Every command's first positional is its input; `-` reads stdin explicitly.
121
+ There is no implicit stdin fallback, and omitting the input is always a
122
+ usage error that names the fix — never a hang.
123
+ - Run `afdata lint` for ordinary JSON/JSONL, JSON Schema, MCP input/output
124
+ schemas, or a structured document (`--input-format`/extension detection
125
+ picks TOML/YAML/dotenv/INI; JSON/JSONL stays the default dual-mode input).
126
+ - Run `afdata validate` for AFDATA protocol events or finite event streams.
127
+ - Run `afdata render` to apply AFDATA redaction/formatting to arbitrary JSON
128
+ or JSONL. `--output` accepts `json`, `yaml`, or `plain`; JSON and YAML keep
129
+ original keys/values (structure-preserving), Plain is the lossy human
130
+ renderer.
131
+ - Treat findings as contract issues unless the owning tool intentionally
132
+ documents a non-AFDATA field.
133
+
134
+ ## Output stream contract
135
+
136
+ AFDATA CLIs route protocol events by consumption mode, not by event shape.
137
+
138
+ - A **finite one-shot command** (the default) puts `kind:"result"` on `stdout`
139
+ and `kind:"error"`/`progress`/`log` on `stderr`, so `x=$(tool …)` never
140
+ captures a failure as data, `tool … >/dev/null` never swallows a diagnostic,
141
+ and `tool … | next` never pipes an error envelope in as input. The exit
142
+ code — not the stream — is set by the terminal kind; routing follows `kind`,
143
+ not the exit code.
144
+ - An **event stream** (interleaved events consumed in order) keeps every event,
145
+ including `error`, on one stream so ordering survives.
146
+ - `afdata` and any CLI built on the library expose `--output-to
147
+ <split|stdout|stderr>` (default `split`). `stdout`/`stderr` collapse the whole
148
+ stream — errors included — onto one destination, for a consumer that would
149
+ rather read one stream and branch on `kind`. `--output` picks the format,
150
+ `--output-to` picks the destination. Send an event stream to a file by
151
+ collapsing it (`--output-to stdout`) and redirecting (`--stdout-file PATH`).
152
+ - The raw-scalar readers (`value`/`paths`/`keys`) are intrinsically split (raw
153
+ data on `stdout`, error envelope on `stderr`) and reject a non-default
154
+ `--output-to`.
155
+ - When building a CLI on the library, choose the mode by how the output is
156
+ consumed — do not reflexively pick finite. A plain one-shot shell command uses
157
+ **finite** mode (errors → stderr, so shell capture and pipelines stay safe). A
158
+ tool whose output is itself an event stream (interleaved `progress`/`log` then
159
+ a terminal `result`/`error` — e.g. streaming query rows), or that is
160
+ agent-facing and wants the caller to read a single structured channel and
161
+ branch on `kind`, uses **stream/unified** mode (every event, errors included,
162
+ on `stdout`). A stream's terminal error MUST stay on the stream, so such a tool
163
+ keeps even its usage errors on `stdout` for one consistent channel. Never write
164
+ to `stderr` ad hoc — route every diagnostic through the emitter so redaction
165
+ and framing still apply.
166
+ - Emit a terminal event with the emitter's `finish` / `finish_result` helpers,
167
+ which map the write outcome to a broken-pipe-safe process exit code (success →
168
+ the given code, a hung-up reader → 0, any other write failure → a nonzero
169
+ output-failure code) instead of hand-rolling that dance — they work in either
170
+ mode (the emitter's mode decides the stream; `finish` only maps the outcome).
171
+ Construct an error with the error builder (`json_error(code, message)` with
172
+ `hint`/`retryable`/extra fields as needed) — that builder *is* the error type —
173
+ and pass the built event to `finish`; there is no separate error-emitting
174
+ convenience.
175
+
176
+ ## Document workflow
177
+
178
+ Use `get`/`value`/`paths`/`keys` to read a JSON, TOML, YAML, dotenv, or INI
179
+ document and `set`/`add`/`remove`/`unset` to edit one in place, instead of
180
+ `sed`, regular expressions, or a generic reserializer — those can reorder
181
+ keys, drop comments, or change quoting/anchors in the rest of the file. Every
182
+ document command's first positional is `FILE` (or `-` for stdin on a read
183
+ command); mutation commands never read stdin and reject `-` as a usage error.
184
+ To edit only the metadata block of a Markdown page, pass `--input-format
185
+ toml-frontmatter` (a `+++` block) or `yaml-frontmatter` (a `---` block): the
186
+ same read/mutate commands then address dot-paths inside the frontmatter and
187
+ leave the Markdown body byte-for-byte untouched. Frontmatter is never
188
+ auto-detected — the format must be named explicitly, since extension detection
189
+ only ever resolves to a whole-file format — so reach for it deliberately
190
+ instead of `sed`-ing a page's `+++`/`---` header.
191
+
192
+ - `get FILE` returns the whole document as one AFDATA record
193
+ (`code:"document"`); `get FILE KEY` narrows it to one dot-path value in the
194
+ same shape. Pass every known sensitive legacy field name with
195
+ `--secret-name FIELD` (an exact field name, not a dot-path). A secret-named
196
+ leaf (`_secret`/`_SECRET`, or an exact `--secret-name` match) is still
197
+ redacted to `"***"` even when directly targeted — use `value
198
+ --reveal-secret` when the real value is genuinely needed.
199
+ - `value FILE KEY` writes only the raw scalar to stdout, with no AFDATA
200
+ envelope and no forced trailing newline, for direct use in shell
201
+ substitution: `value=$(afdata value config.toml server.host)`. Only scalars
202
+ are supported; a container errors (`document_not_scalar`) — use `get` for a
203
+ subtree. A secret-named leaf errors (`document_secret_redacted`) unless
204
+ `--reveal-secret` is passed. On any failure `value`'s stdout is always
205
+ empty — the error envelope goes to stderr instead — so
206
+ `port=$(afdata value config.toml server.port)` never captures a JSON error
207
+ as data. `--default VAL` prints `VAL` instead of erroring when the path is
208
+ absent or the value is `null` (an empty string is a real value and does not
209
+ trigger it), collapsing the common "read or fall back" shell idiom into one
210
+ call with no `2>/dev/null` guard needed.
211
+ - `paths FILE [KEY]` and `keys FILE [KEY]` enumerate a container's immediate
212
+ children, one per line, with the same empty-stdout-on-failure contract as
213
+ `value`. **`paths` output feeds back into afdata** (full grammar-escaped
214
+ dot-paths from the root — pipe straight into `value`/`get`/`unset` or
215
+ extend with `"$p.field"`); **`keys` output goes to external tools** (raw,
216
+ unescaped key names/array indices — what a package manager or another CLI
217
+ expects). Never use `paths`'s output as a literal name, or `keys`'s output
218
+ as a dot-path — they only diverge on a key containing a dot or space, so
219
+ the wrong one works until it silently doesn't. A scalar target errors
220
+ (`document_not_container`); `--missing-ok` turns a missing KEY specifically
221
+ into empty output + exit 0 (other errors still fail). Path grammar does not
222
+ escape spaces or other IFS characters, so the only correct consumption
223
+ shape is a `while IFS= read -r` loop, never `for x in $(...)`:
224
+
225
+ ```sh
226
+ while IFS= read -r p; do
227
+ afdata value config.toml "$p.slug"
228
+ done < <(afdata paths config.toml extra.tools)
229
+ ```
230
+ - Escape a literal dot as `\.` and a literal backslash as `\\` in a dot-path;
231
+ every command shares this grammar (`paths`'s escaped output re-parses
232
+ through the exact same rules it was emitted from), so an unrecognized
233
+ escape or empty segment is always an error, never a guess.
234
+ - `add`/`remove` operate on a keyed list — an array of objects addressed by a
235
+ slug — and always require an explicit `--slug-field FIELD`; the CLI never
236
+ infers an identity field. Keyed editing is implemented for JSON and YAML
237
+ only; TOML, dotenv, and INI return a structured "not implemented" error, so
238
+ edit their list entries by writing the whole value another way. Adding a
239
+ slug that already exists, or removing/unsetting one that does not, is
240
+ always an error (`document_slug_exists`/`document_slug_not_found`/
241
+ `document_path_not_found`) — never a silent no-op; wrap the call yourself
242
+ when idempotence is wanted.
243
+ - `set`'s bare VALUE (and `add`'s `FIELD=VALUE`) is always a plain string —
244
+ no shape-guessing, so a leading-zero ID or SHA never gets reinterpreted as
245
+ a number. Writing an exact type goes through `--value-type
246
+ string|number|bool|null|json` instead; `json` is the only entry point for
247
+ an array or object. Overwriting an *existing* scalar of a different kind
248
+ with a bare VALUE is a usage error that names both escape hatches (keep the
249
+ type with `--value-type <kind>`, or convert explicitly with `--value-type
250
+ string`) — a brand-new key never needs `--value-type`.
251
+ - For `set`, choose `--secret-from stdin|prompt|fd:<N>|env:<VAR>` instead of a
252
+ positional value when the value must not land in shell history or `ps`:
253
+ `stdin` (pipe), `prompt` (human, terminal echo off), `fd:N` (Unix
254
+ automation that needs stdin for something else), or `env:VAR` (already in
255
+ the process environment). There is no inline `--secret-from=VALUE`-style
256
+ argv source — that would defeat the point.
257
+ - Mutations preserve the rest of the source document — comments, key order,
258
+ unrelated formatting — and refuse to write through a symlink or (on Unix) a
259
+ multi-link hardlink target. Treat a failed mutation as leaving the original
260
+ file untouched rather than assuming a partial write, and reread before
261
+ retrying. A format backend that isn't compiled in, or input outside the
262
+ documented dotenv/INI dialect, is a structured error to report, not a cue to
263
+ hand-edit the file. A successful mutation's result carries the `path`
264
+ actually written, for confirming which file changed when scripting several.
265
+ - Branch on `error.code`, not the message text: document errors use stable
266
+ codes (`document_path_not_found`, `document_type_mismatch`,
267
+ `document_slug_not_found`, `document_slug_exists`, `document_parse_failed`,
268
+ `document_io_failed`, and a few narrower ones), while a malformed
269
+ invocation (`--input-format`, `--value-type`, `FIELD=VALUE`, a mutation
270
+ given `-`, …) is always `document_usage_error` at exit 2 — distinct from
271
+ every runtime document error, which exits 1.
272
+
273
+ ## Review checklist
274
+
275
+ 1. Numeric fields with units carry the right suffix and safe integer/string representation.
276
+ 2. Timestamps use `_epoch_s`, `_epoch_ms`, decimal-string `_epoch_ns`, or `_rfc3339`.
277
+ 3. Secrets use `_secret` or configured exact secret names; no fallback path leaks them.
278
+ 4. CLI output uses AFDATA helpers for JSON/YAML/plain formatting and structured errors, and routes by consumption mode (finite: `result`→stdout, `error`→stderr; stream: one destination) rather than writing to stderr ad hoc.
279
+ 5. Logs use AFDATA structured logging or equivalent field-name redaction.
280
+ 6. Transport payloads preserve the protocol envelope when AFDATA compliance is claimed.
@@ -41,6 +41,8 @@ Integer vs string is not uniform across siblings: `_epoch_s` and `_epoch_ms` are
41
41
 
42
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
43
 
44
+ A `null` value is exempt from every suffix type constraint above: `null` means the field is absent/unset, not present-with-the-wrong-type, so `law_repeal_at_epoch_s: null` is not a `suffix_type_mismatch`. Absence may be written as an omitted key or as an explicit `null` — both are valid. The constraint applies only once a value is present and non-null.
45
+
44
46
  ### Strict string formats
45
47
 
46
48
  | Suffix | Format | Example |
@@ -107,7 +109,7 @@ Sub-cent precision — `_{code}_micro`, integer millionths (10⁻⁶) of the maj
107
109
  | `_secret` | redact the entire value/subtree to `***` | `api_key_secret: "sk-or-v1-abc..."` |
108
110
  | `_url` | scrub secrets *inside* the URL value, keep the rest | `callback_url: "https://h/cb?code_secret=..."` |
109
111
 
110
- 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.
112
+ 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 policy `All`, `secret_names`, `TraceOnly`, or `Off` deliberately at the serialization boundary. YAML is always schema-preserving, like JSON. Callers that need schema-preserving Plain rendering can pass `OutputOptions` with the `Raw` output style.
111
113
 
112
114
  `***` means only one thing: AFDATA redacted a sensitive value. Do not use it for serialization failure, truncation, unsupported types, or “maybe secret” guesses.
113
115
 
@@ -158,15 +160,17 @@ ORM struct fields preserve the suffix: `duration_ms: i64`, not `duration: i64`.
158
160
 
159
161
  ## Part 2: Output Processing
160
162
 
161
- 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).
163
+ Three output formats via one entry point `render(value, format, options)` (`format` = json/yaml/plain). JSON and YAML are both **structure-preserving**: original keys, scalar types, and numeric semantics survive after redaction. Plain is the one **lossy/human** renderer — it strips formatting suffixes and reformats values for scanning. `options` is an optional keyword arg in Python/TS; Rust/Go pass `OutputOptions` explicitly.
162
164
 
163
165
  ### Formats
164
166
 
165
167
  - **JSON** — single-line, original keys, raw values, no sorting (machine-readable), secrets redacted
166
- - **YAML** — multi-line, formatting suffixes stripped, values formatted, secrets redacted by default
168
+ - **YAML** — multi-line, original keys, raw values (same semantics as JSON), keys sorted, secrets redacted by default
167
169
  - **Plain** — single-line logfmt, formatting suffixes stripped, values formatted, secrets redacted by default
168
170
 
169
- ### Key stripping (YAML and Plain)
171
+ Rust gates YAML output behind the `yaml` Cargo feature (on by default, along with the rest of the default feature set). With `default-features = false` and `yaml` not re-enabled, `OutputFormat::Yaml` does not exist and `--output yaml` is rejected with a feature-requirement error instead of silently falling back to another format.
172
+
173
+ ### Key stripping (Plain only)
170
174
 
171
175
  Remove recognized formatting suffix from key. Longest match first, exact lowercase or uppercase only:
172
176
 
@@ -176,9 +180,9 @@ Remove recognized formatting suffix from key. Longest match first, exact lowerca
176
180
  4. `_msats`, `_sats`, `_bytes`, `_percent`, `_secret`
177
181
  5. `_jpy`, `_ns`, `_us`, `_ms`, `_s`
178
182
 
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.
183
+ `_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. JSON and YAML never strip suffixes — every key renders exactly as written.
180
184
 
181
- ### Value formatting (YAML and Plain)
185
+ ### Value formatting (Plain only)
182
186
 
183
187
  - `_ms` with absolute value < 1000 → `{n}ms`; absolute value ≥ 1000 → seconds (`1280` → `1.28s`, `-1500` → `-1.5s`)
184
188
  - `_s`, `_ns`, `_us` → append unit (`3600s`, `450000ns`, `830μs`)
@@ -192,7 +196,7 @@ Remove recognized formatting suffix from key. Longest match first, exact lowerca
192
196
  - `_secret` → `***` (the redaction phase already replaced the subtree)
193
197
  - `_bcp47`, `_utc_offset`, `_rfc3339_date`, `_rfc3339_time` → pass through unchanged
194
198
 
195
- **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.
199
+ **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. JSON and YAML never format values — a value renders exactly as its JSON type (numbers stay numbers, strings stay quoted strings, no unit/date/currency formatting).
196
200
 
197
201
  ### Plain logfmt details
198
202
 
@@ -204,7 +208,11 @@ Remove recognized formatting suffix from key. Longest match first, exact lowerca
204
208
 
205
209
  ### Key ordering
206
210
 
207
- YAML and Plain sort keys (after stripping) by UTF-16 code unit order (JCS, RFC 8785). For ASCII keys this equals byte-order sorting.
211
+ YAML sorts keys by UTF-16 code unit order (JCS, RFC 8785) without stripping suffixes. Plain sorts keys the same way, but after stripping. For ASCII keys this equals byte-order sorting.
212
+
213
+ ### `PlainStyle` (Plain only)
214
+
215
+ `PlainStyle` (`Readable`/`Raw`) only affects Plain: `Readable` (default) strips suffixes and formats values; `Raw` keeps original keys/values (still redacted). YAML ignores `PlainStyle` entirely — it is always structure-preserving, matching JSON.
208
216
 
209
217
  ---
210
218
 
@@ -219,22 +227,24 @@ Every protocol event uses `kind` plus one same-named payload:
219
227
  | `"result"` | Terminal success |
220
228
  | `"error"` | Terminal error with non-empty `error.code` and `error.message` |
221
229
 
222
- Channel policy:
223
- - `stdout` is the only protocol/log stream for machine-readable events
224
- - runtime protocol events MUST NOT be emitted on `stderr`
225
- - `stderr` is reserved for unrecoverable pre-protocol startup failures only
230
+ Channel policy — the stream follows the consumption mode, not the event shape:
231
+ - **Finite one-shot command (default):** `result` → `stdout`; `error`/`progress`/`log` → `stderr`. Routing follows `kind`, not the exit code, so `stdout` carries only successful payloads (`x=$(tool …)`, `tool | next`, and `tool >/dev/null` all stay safe).
232
+ - **Event stream (interleaved events consumed in order):** every event, including `error`, stays on one stream so ordering survives; do not split it across `stdout`/`stderr`.
233
+ - `--output-to <split|stdout|stderr>` (default `split`) selects the mode: `stdout`/`stderr` collapse the whole stream onto one destination. `--output` selects format, `--output-to` selects destination.
234
+ - Raw-scalar readers (`value`/`paths`/`keys`) are intrinsically split and reject a non-default `--output-to`.
235
+ - Choose the mode by how the output is consumed, not reflexively: a plain one-shot shell command uses **finite**; a tool whose output is itself an event stream (streaming rows, interleaved progress/log) or that is agent-facing and wants one structured channel uses **stream/unified** (all on `stdout`, errors included — a stream's terminal error must stay on the stream). When building a CLI on the library, emit terminal events with the emitter's `finish`/`finish_result` helpers (success → the given code, broken pipe → 0, other write failure → a nonzero output-failure code); build errors with the `json_error` builder (the error *type*, carrying hint/retryable/fields) and pass the event to `finish` — the helpers work in either mode.
226
236
 
227
237
  Optional stream redirection:
228
- - use `--stdout-file <PATH>` to redirect stdout bytes to a file
229
- - use `--stderr-file <PATH>` to redirect stderr bytes to a file
230
- - keep `--output` reserved for stdout format selection (`json`, `yaml`, `plain`, and help-specific `markdown`)
231
- - do not convert stderr diagnostics to JSON; Rust panics, Python tracebacks, and runtime errors stay native stderr bytes
238
+ - use `--stdout-file <PATH>` / `--stderr-file <PATH>` to redirect a stream's bytes to a file (process-level, beneath the channel routing above)
239
+ - send an event stream to a file by collapsing it (`--output-to stdout`) and redirecting (`--stdout-file <PATH>`); there is no separate events-file flag
240
+ - native panics/tracebacks stay raw `stderr` bytes; in finite mode `stderr` also carries the formatted AFDATA `error`/`progress`/`log` envelopes, so a reader parses JSON lines and tolerates interleaved native bytes
232
241
  - treat this as stream destination control, not a second protocol stream and not stream copying
233
242
  - do not implement application-level rotation
234
243
 
235
244
  Recommended enforcement:
236
- - Rust: enable clippy `print_stderr = "deny"` and disallow `std::eprintln` / `std::io::stderr`
237
- - Go/Python/TypeScript: add source-policy tests that fail if runtime sources reference stderr APIs
245
+ - route every `stderr` write through the emitter's diagnostic/error path; no ad-hoc stderr that bypasses the formatter in runtime code
246
+ - Rust: keep native panic output as the only unstructured `stderr` (no stray `eprintln!` / `std::io::stderr`)
247
+ - Go/Python/TypeScript: source-policy tests that fail on ad-hoc stderr APIs in runtime code, with the emitter's own sink as the sanctioned exception
238
248
 
239
249
  ### Templates
240
250
 
@@ -257,7 +267,7 @@ Startup payload fields are tool-defined; `config` is recommended, while `version
257
267
  | MCP tool | JSON |
258
268
  | SSE stream | JSONL |
259
269
 
260
- All use `kind` plus the same-named payload and optional `trace`. Do not split protocol events across `stdout` and `stderr`.
270
+ All use `kind` plus the same-named payload and optional `trace`. An event stream keeps its events on one stream (never split); a finite one-shot command splits by `kind` (`result` → stdout, `error` → stderr) — see Channel policy.
261
271
 
262
272
  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.
263
273
 
@@ -269,17 +279,17 @@ Use the local language README for installation and the full overview/spec for AP
269
279
 
270
280
  Required cross-language behavior to rely on:
271
281
 
272
- - Output helpers redact before formatting: `output_json()`, `output_yaml()`, `output_plain()` (with optional `options` in Python/TS).
273
- - Use `redacted_value()` for raw HTTP/MCP/SSE serialization paths that bypass `output_json()`.
282
+ - The single output entry point `render(value, format, options)` redacts before formatting (with optional `options` in Python/TS).
283
+ - Use `redacted_value()` for raw HTTP/MCP/SSE serialization paths that bypass `render()`.
274
284
  - Use `redact_url_secrets()` for URLs embedded in log messages or prose before interpolating them into output.
275
- - 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.
285
+ - Use `cli_parse_output()`, `cli_parse_log_filters()`, `render()`, `build_cli_error()`, and the version helper for CLI tools instead of custom parsing/error envelopes.
276
286
  - `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
277
- - 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.
287
+ - Rust CLIs should call `cli_handle_version_or_continue(raw_args, name, version)` before clap parsing so `--version --output json|yaml|plain` emits a structured `kind:"result"` event with `result.code == "version"` and `result.version` instead of clap's plain text. Bare `--version` always stays human `<name> <version>` text; explicit `--output` is always structured — there is no configuration.
278
288
  - Use the protocol reader `decode_protocol_event(text)` to parse and strict-validate a single JSON text line, receiving a typed decoded event.
279
289
 
280
290
  ## AFDATA Logging
281
291
 
282
- 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`.
292
+ 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()` + `render()` or `CliEmitter`.
283
293
 
284
294
  ### Init (Rust only; pick one format per process)
285
295
 
@@ -312,7 +322,7 @@ Log redaction is **by field name** (the same `_secret`/`_url` rule as all output
312
322
  CLI tools that use AFDATA should support output and logging flags:
313
323
 
314
324
  ```
315
- --output json|yaml|plain # default is tool-defined (interactive → yaml, scripting/logging → json)
325
+ --output json|yaml|plain # default is tool-defined (structured/scripting → json or yaml, human scanning → plain)
316
326
  --log startup,request,progress,retry,redirect
317
327
  --verbose # shorthand for all log categories
318
328
  ```
@@ -345,4 +355,4 @@ Use this checklist when the skill is evaluating a change, not just documenting c
345
355
  7. Environment variables follow `UPPER_SNAKE_CASE` with the same suffixes
346
356
  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
347
357
  9. Database columns use AFDATA suffixes on generic types (`duration_ms INTEGER`, not `duration INTEGER`); native types like `TIMESTAMPTZ` don't need suffixes
348
- 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
358
+ 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 a clap/usage error becomes a structured `kind:"error"` envelope (routed to stderr under the default split, exit 2), never raw clap text