veaf-tools doctor — paste block format¶
Audience: developers of the lots that consume
doctor's output — log analysis (FEAT-SUPPORT-LOG-ANALYSIS), which embeds it in its own report block, and bug intake (FEAT-SUPPORT-BUG-INTAKE), which parses it back.Schema:
veaf-tools-doctor/1. — 🇫🇷diagnostic-block.md.
Why a format¶
The block travels through Discord or a GitHub issue, copied by hand by someone who will not reread it, and is read back by a machine at the other end. So it is neither a display nor a file: it is a contract, versioned, that neither producer nor consumers can change on their own.
The implementation lives in src/python/veaf-tools/veaf_libs/diagnostics.py, producer
(DiagnosticReport.to_block) and reader (parse_block) in the same module — so they cannot drift —
with the round-trip test in test/python/veaf_libs/test_diagnostics.py.
Structure¶
=== VEAF-TOOLS DOCTOR BEGIN ===
schema: veaf-tools-doctor/1
generated: 2026-09-05T09:39:29Z
tool.version: 6.19.0
...
--- recent-errors ---
2026-09-03 21:05:52,533 - veaf-tools - ERROR - Failed to evaluate time expression
Traceback (most recent call last):
ValueError: ...
--- recent-errors end ---
=== VEAF-TOOLS DOCTOR END ===
Three rules, and nothing else:
- The block is delimited by
=== VEAF-TOOLS DOCTOR BEGIN ===and=== VEAF-TOOLS DOCTOR END ===. It arrives inside something else (a message, code fences): a reader looks for those two lines, it does not assume the block starts at the first one. - Between the delimiters every line is
key: value. The split is on the first:— a value may contain more of them (a Windows path, a timestamp). A key and a value each hold to one line: the producer collapses any line break to a space, without which a multi-line value would come back as two fields, one of them written by nobody. - The optional section between
--- recent-errors ---and--- recent-errors end ---holds raw text. A record starts at a header line (YYYY-MM-DD HH:MM:SS,mmm - <logger> - <LEVEL> -); everything after it belongs to that record, which is how a stack trace survives whole.
The fields¶
The order is FIELD_ORDER in diagnostics.py, and it is stable.
| Key | Example | Reports unknown when |
|---|---|---|
schema |
veaf-tools-doctor/1 |
never |
generated |
2026-09-05T09:39:29Z |
never |
tool.version |
6.19.0 |
the package is not installed and the version file is absent |
tool.packaging |
frozen or source |
never |
tool.executable |
C:\Users\<user>\...\python.exe |
the interpreter does not name itself |
tool.python |
3.13.15 |
never |
machine.os |
Windows-11-10.0.26200-SP0 |
the platform does not answer |
machine.locale |
fr_FR / cp1252 |
the locale cannot be determined |
machine.free_space |
654.0 GB on D:\ |
the disk does not answer |
dcs.detected |
yes / no |
never |
dcs.version |
2.9.29.27278 |
DCS absent, or the banner is missing from dcs.log |
dcs.variant |
stable, openbeta |
DCS absent |
dcs.write_dir |
C:\Users\<user>\Saved Games\DCS |
DCS absent |
dcs.log_age |
3 d |
DCS absent |
veaf.home |
C:\Users\<user>\.veaf |
the folder cannot be created |
veaf.log |
present, 1.2 MB / absent |
access fails |
veaf.lua_modules |
37 |
the inventory is unreadable |
An unavailable field is the string unknown and never an empty string: "absent" and "not
collected" read the same to a human, and a consumer must be able to tell them apart.
Evolution rules¶
- Adding a field is compatible: a reader reads what it knows and ignores the rest. The producer already carries a field it does not itself know about.
- Removing or renaming one is not, and bumps the schema to
veaf-tools-doctor/2. - A reader checks
schemabefore parsing. A block with an unknown schema is a block it can assume nothing about.
A block you received is not a measurement¶
parse_block reads text that arrived through a public issue or a Discord message. Anyone can type
one by hand. The producer guarantees the shape (one field, one line), never the truth of a
value: a consumer acting on tool.version is acting on a claim, not on a reading taken from the
machine. That is a trust boundary, not a parsing detail — FEAT-SUPPORT-BUG-INTAKE is the first lot
to cross it.
Redaction¶
Everything doctor produces goes through veaf_libs.redaction.redact before it is displayed:
account name → <user>, routable IP address (v4 and v6) → <ip>, e-mail address → <email>, token
or password → <redacted>. Loopback addresses are kept: they carry nothing personal and do say
something useful.
Two asymmetric rules, and the asymmetry is deliberate.
Identity is over-redacted. The account name is replaced everywhere it appears, not only under
Users/: it resurfaces in a temporary directory (Temp\pytest-of-Firstname\), in a host name, in
an environment dump. Measured over 1489 real ERROR records it survived 56 times on lines whose
originating path had been redacted three segments earlier.
Identifiers are not redacted. There is deliberately no entropy rule here. A first version
replaced any run of 24 or more characters mixing letters and digits. Measured against the same log
it produced 74 substitutions and not one credential; measured against the repository's own data
files it matched 169 DCS GUIDs and 493 other identifiers, among them HVAR_USN_Mk28_Mod4_Corsair
and M261_INBOARD_DE_M151_C_M274. That is unknown payload <redacted>: keeping "it broke" and
throwing away "on what". A secret is therefore recognised by its context (an assignment to a
credential-shaped key — token, password, access_token, client_secret…) or by a known
shape (a GitHub token prefix, a JWT, a webhook URL, credentials inside a URL). An unlabelled blob
of randomness in an unknown shape goes through, on purpose.
Redaction happens at production, not at publication: by publication time it is a different program
on a different machine, and it is too late. A consumer may run redact again over what it receives
— the operation is idempotent — but must not rely on that to rescue an un-redacted source.