Vitals — Capsule Stream Specification

schema: vitals/v0.1 · spec_revision: 0.1-draft.5 · status: DRAFT (not yet frozen) transport: NDJSON / UTF-8 · media type: application/x-vitals-stream (.ndjson) · reference producer: vitals-maven ≥ 0.1.0

The normative artifact is the JSON Schema at stream.schema.json. When this prose and the schema disagree, the schema wins. RFC 2119 / RFC 8174 keywords (MUST, SHOULD, MAY…) are normative only when capitalized.


1. What Vitals is

Vitals is a tool-neutral format for build lifecycle and diagnostics, streamed as NDJSON so AI agents, CI systems, and humans can consume a build without scraping console text. A stream is a sequence of capsules — one JSON object per line, each an envelope plus a body chosen by its kind. Exactly one capsule, the Pulse, is the small, deduped, authoritative roll-up a consumer reads first; everything else is drill-down.

The data model is a hierarchy — Build ⊃ Unit ⊃ Step — that fits Maven, Gradle, and npm/pnpm workspaces losslessly. It is deliberately narrower than a universal build protocol: arbitrary action DAGs, dependency graphs, remote-execution topology, and watch/daemon sessions are explicit non-goals (one stream = one build invocation).

2. Design principles

These five break every tie:

  1. Facts, not rendering. The stream records what happened, in time order, losslessly. Token-efficient renderings (tables, summaries) are consumer-built projections, never the system of record.
  2. Line independence. One capsule per line, with identity, attribution, severity, timestamp, and message all inline — a grepped line is interpretable with zero cross-line lookups. Invocation constants live once in the StreamHeader.
  3. Spend tokens on semantics, not plumbing. Semantic fields keep descriptive names; per-line repetition of invocation constants is eliminated.
  4. Open-world consumers, closed-world producers. Consumers MUST ignore what they don’t know; producers MUST NOT redefine what the schema owns. This is what makes additive evolution real (§10).
  5. Never break the build. A producer failure yields a Meta capsule and a possibly incomplete stream — never a failed or altered build. Observation is side-effect-free.

3. Stream model (transport, normative)

A stream is a sequence of capsules, one per line, each a complete valid JSON object, UTF-8, LF-terminated. Producers MUST emit LF; consumers MUST accept LF and CRLF.

4. StreamHeader

The mandatory first capsule (stream_seq: 0). A stream without it is non-conformant; consumers that slice streams MUST retain line 1. It states the invocation constants once so no other line repeats them.

field req meaning
event, kind both "StreamHeader"
stream_seq 0
ts RFC 3339 UTC, ms precision, Z
schema "vitals/v<major>.<minor>" — the compatibility key consumers dispatch on
spec_revision exact immutable revision, e.g. "0.1-draft.5" — validators pin to this
schema_uri stable URI of the schema (a hint; consumers MUST NOT auto-fetch, §9)
schema_sha256 SHA-256 of the bundled schema, for trusted binding
build_id opaque id unique per invocation (UUID recommended)
producer {tool, tool_version, impl}
capabilities what this producer can emit, so consumers tell “unsupported” from “missing” (§11)
root absolute path of the top-level build dir; REQUIRED when any relative path appears
privacy declared redaction posture {mode, redacted} (§9)
style "contextual" (default) or "verbose"
{"event":"StreamHeader","kind":"StreamHeader","stream_seq":0,"schema":"vitals/v0.1",
 "spec_revision":"0.1-draft.5","build_id":"0d9e4c2a-7b31-4f8e-9a12-c3d5e6f7a8b9",
 "ts":"2026-07-12T08:30:02.621Z",
 "producer":{"tool":"maven","tool_version":"3.9.9","impl":"vitals-maven/0.1.0"},
 "capabilities":["diagnostics","tests","work-avoidance","portable-paths"],
 "root":"/home/u/proj","privacy":{"mode":"portable","redacted":true},"style":"contextual"}

5. Capsule envelope

Fields that may appear on any capsule. Schema-owned names — producers MUST NOT repurpose them.

field req meaning
event producer-native name (open set). When there is no richer native name, equals kind. Consumers MUST NOT branch on it.
kind tool-neutral classification (closed set) — the consumer’s dispatch key
stream_seq monotonic record counter, 0 on the header, +1 each record, no gaps. Detects interior loss/reorder; cannot detect a truncated tail.
ts absolute RFC 3339 UTC instant. Wall clock — never subtract timestamps for durations; use duration_ms, order by stream_seq.
th opaque thread/lane id under parallel builds
unit full unit coordinate, stream-unique (Maven groupId:artifactId:version; npm name@version)
step producer-scoped step id (Maven plugin:goal@executionId)
eid execution id — the primary correlation key. Set on *Started, echoed on the matching *Finished; REQUIRED on every non-skipped Unit/Step span and every execution-scoped child (Diagnostic, Output, TestSummary, TestCaseFailed)
parent_eid eid of the enclosing span, per the fixed parent-kind matrix (§8)
retry_of_eid on a whole-span re-execution: the eid it supersedes (latest in a chain is the final outcome)
status on *Finished: success | failure | cancelled | skipped
work on success: executed | up-to-date | cache | no-source (an up-to-date test step did not re-run)
duration_ms elapsed span time from a monotonic clock; absence = unknown, not instant
exit_code raw exit code on external-process StepFinished
exception {type, msg, stack?, causes?}; stack is a capped string array

Extension fields — ext. All non-schema fields live inside a single schema-owned ext object keyed by a reverse-DNS namespace the extender controls, e.g. "ext":{"net.sourceforge.pmd":{"priority":3}}. The top level stays reserved for the schema; cross-tool logic uses only schema-owned fields. Keys under ext beginning vitals. are reserved for future schema use.

Naming (normative). Keys are lowercase snake_case with a closed whitelist of abbreviations (ts, msg, loc, line/col, stack, fp, dir, abs_path, th, eid). kind/event values are PascalCase type names; every other enum value is lowercase-kebab (success, up-to-date).

Numbers/time (normative). Every number MUST be an exact IEEE-754 double (0 ≤ n ≤ 2⁵³−1). loc positions are 1-based positive; an end MUST NOT precede its start. ts has exactly three fractional digits and a literal Z.

6. The kind taxonomy

Closed set in v1 — consumers MUST ignore unknown kinds. Fourteen kinds:

kind semantics
StreamHeader stream context (§4)
BuildConfig what the build was asked to do
BuildStarted / BuildFinished outermost span
UnitStarted / UnitFinished a buildable unit (Maven module, Gradle project)
StepStarted / StepFinished a unit of executable work inside a unit
Diagnostic one normalized finding (§6.3)
TestSummary / TestCaseFailed one aggregate per test-step execution / one finally-failed case (§6.5)
Output captured stdout/stderr, attributed and capped
Pulse terminal, deduped outcome roll-up — read first (§6.4)
Meta producer self-diagnostics; never affects the build

Build status ≠ stream state. status/outcome describe the build. Whether the stream is complete, truncated, or corrupt is a separate, consumer-derived judgment (§8.5) — a dead producer cannot report its own death.

6.1 Span capsules & terminal statuses

Six kinds share the start/end span pattern at three scopes (Build ⊃ Unit ⊃ Step). The end node’s status carries the outcome: success (+ work), failure (+ exception/exit_code), cancelled (+ reason, caused_by), or skipped (a single-node Finished with no Started, no eid). Process death emits no line at all — detected via the unmatched Started (§8). UnitStarted carries dir (unit basedir relative to root), the anchor for every loc.path in the unit.

6.2 BuildConfig

Emitted once before BuildStarted. Closes the “what was asked” gap: targets (required), plus optional cwd, command_display (redacted, display-only), args (machine-usable, redacted per §9), parallelism, offline.

6.3 Diagnostic — the flagship body

One normalized finding, semantic core inline.

field req meaning
tool producing analyzer (javac, checkstyle, pmd, spotbugs, resolver, surefire…)
rule_id stable {tool}:{rule} id, e.g. checkstyle:UnusedImports
severity error | warning | info | ignore — the only cross-tool comparable level
msg short human header (≤ 2,000 chars)
detail multi-line elaboration (≤ 8,000 chars)
loc optional location; producers MUST NOT fabricate one
fp producer opaque occurrence/dedup token; normative dedup key is (unit, fp). Stable across rebuilds of the same source, not across edits
fp_scheme e.g. vitals/occurrence/v1
stable_fp optional cross-build-stable fingerprint (no line number) for multi-round repair; 64-char SHA-256 hex
stable_fp_scheme core value vitals/stable/v1; REQUIRED when stable_fp is present

Location & path resolution (normative, load-bearing). The repair loop ends with “open the file and edit it,” so a Diagnostic must resolve to a real path. Never assume module dir = module name (a module demo-web-service can live in server/). loc.path is relative with forward slashes; loc.path_base (unit | root) names the anchor; dir on UnitStarted/Pulse.units[] gives the unit’s basedir. Resolution: path_base:"unit"normalize(checkout_root ⊕ dir(unit) ⊕ loc.path), else against root. Safety: normalize before join, resolve .. lexically, and a consumer MUST verify the resolved path lies inside its workspace before opening it. Resolver diagnostics have no location (never point them at pom.xml).

vitals/stable/v1 fingerprint (normative). A reproducible digest so two producers (and one producer across line drift) agree. Ordered string components: c0=rule_id; c1=version-independent unit identity; c2=located path (path_base + / + normalized path, line/column excluded); c3=normalized msg (NFC, trimmed, whitespace collapsed). SHA-256 of the UTF-8 JSON array [c0,c1,c2,c3], emitted as 64-char lowercase hex. A different recipe MUST take a new scheme name.

6.4 Pulse

Emitted once, immediately before BuildFinished. Small, deduped, authoritative — sourced from the build tool’s own result records, not replayed from the stream. The entry point for every consumer.

{"event":"Pulse","kind":"Pulse","ts":"…","outcome":"failure","duration_ms":52704,
 "totals":{"units":4,"succeeded":1,"failed":1,"cancelled":1,"skipped":1,
  "diagnostics":{"error":3,"warning":7,"info":0,"ignore":0},
  "tests":{"total":342,"passed":338,"failed":2,"skipped":1,"flaky":1}},
 "units":[
  {"unit":"com.example:api:1.0.0","dir":"api","status":"success","duration_ms":21033},
  {"unit":"com.example:core:1.0.0","dir":"server","status":"failure","duration_ms":30122,
   "failed_steps":["compiler:compile@default-compile"],
   "exception":{"type":"…CompilationFailureException","msg":"Compilation failure"}}]}

outcome ∈ success | failure | cancelled and MUST equal BuildFinished.status. totals.units = succeeded + failed + cancelled + skipped. totals.diagnostics is a dense four-key map, post-dedup by (unit, fp). totals.tests aggregates only the final execution of each test step (latest eid in a retry_of_eid chain), omitted when no test ran. Each units[] carries dir, so pulse + diagnostics alone resolve every path. The pulse does not carry per-step work — the up-to-date-test trap requires consulting StepFinished.

6.5 Output · Meta · Tests

7. Producer rules

8. Consumer rules

9. Security & privacy (normative)

A Vitals stream routinely carries sensitive data and its consumers may act on it, so security is normative. Producers MUST strip userinfo and auth params from URLs, mask secret-bearing command-line flags (args_redacted:true), and SHOULD scrub secret patterns from free text. Consumers MUST treat all free-text fields as untrusted (never execute, template, or shell-interpolate them), MUST apply §6.3 path normalization and the workspace-boundary check before opening a resolved path, and MUST NOT auto-fetch schema_uri (SSRF) — resolve spec_revision through a trusted registry and verify schema_sha256. The privacy header (mode: local | portable) documents posture but does not assert the stream is clean.

10. Versioning & evolution

Two identifiers, two jobs. schema (vitals/v<major>.<minor>) is the compatibility key consumers dispatch on. Additive changes — new event names, new optional fields, new kind values — bump the minor (v0.1 → v0.2). Removal, retyping, or semantic change of a schema-owned field is breaking ⇒ bump the major, published as parallel schema files. spec_revision is the exact immutable draft within a schema value that validators pin to. The JSON Schema for a given spec_revision is the tie-breaker over this prose.

11. Conformance

A producer is conformant if its records pass the body schema for its spec_revision, it honors stream cardinality/ordering, keeps eids unique and stream_seq gap-free, obeys its declared capabilities, enforces size/redaction/failure-isolation rules, and passes positive and negative fixtures. A consumer is conformant if it dispatches on the schema major, ignores unknown fields/kinds, never subtracts timestamps for durations, tolerates a truncated tail, distinguishes an incomplete/corrupt stream from a failed build, and resolves paths with the workspace-safety check.

Every producer ships a profile pinning its native event → kind map, unit/step identity, severity mapping (the only cross-tool level — e.g. PMD priority 1–2→error, 3→warning, 4–5→info; SpotBugs 1→error, 2→warning, 3→info), path mapping, ext namespaces, fingerprint algorithm, and known lossiness.

Semantic validation (normative). Per-record JSON Schema cannot express the cross-record contract, so a semantic validator checks the whole stream and classifies failures as CORRUPT, INCOMPLETE, or WARNING. Key rules: header first exactly once at stream_seq:0; stream_seq +1 with no gaps (interior gap = CORRUPT, missing tail = INCOMPLETE); lifecycle cardinality; eid uniqueness and Started/Finished pairing; parent_eid/retry_of_eid integrity; execution-scoped children reference a real eid; BuildFinished.status == Pulse.outcome; Pulse.units[] agree with their UnitFinished; aggregate arithmetic closes; path-safety (an escaping path or path_base:"unit" without a unit is CORRUPT).


Changelog

Freeze of the format is gated on the machine JSON Schema, the semantic validator (§11), and a cross-tool golden corpus once a second producer (Gradle) lands. Until then, spec_revision advances as 0.1-draft.N under a stable vitals/v0.1; additive changes bump the schema minor, breaking changes the major (§10).