Result envelope — loom-result
Every engine invocation made through vyges mcp returns the same JSON object,
whatever the engine. One shape to parse, whether you called DRC, timing, or a layout viewer —
and a failure comes back in that shape too, never as a crash.
Where the tool descriptor says how to call an engine, this says what comes back.
Note: engines invoked directly on the command line print their own
--jsonpayload. The envelope is added by the layer that runs them, and the engine’s own output is carried inside it verbatim asresult.
A complete envelope
Real output from a vyges-em-ir run, abridged only where marked:
{
"schema": "loom-result/1.1",
"tool": "em-ir",
"tool_version": "0.1.15",
"status": "ok", // did the PROCESS run?
"engineering": { // what does the evidence say about the DESIGN?
"status": "pass",
"assertion": "power-integrity-met",
"summary": "assertion 'power-integrity-met' held on 'pi_met'"
},
"input_hash": "blake3:f81c6c0221446a1296d4541fa732e433517e759b6480a612d6dda769f7c3f3f7",
"result": { /* the engine's own --json output, verbatim */ },
"artifacts": [
{ "role": "emir_report", "path": "emir.rpt",
"hash": "blake3:9f2a4c1e7b3d8056a1c4f9e2b7d05384c6a1f9e2b7d05384c6a1f9e2b7d05384" }
],
"error": null,
"logs": {
"count": 1,
"events": [ /* vyges-events records */ ],
"summary": { "info_count": 1, "codes": ["EMIR-DONE"] }
},
"provenance": {
"cmd": ["vyges-em-ir", "run", "block.emir", "--json"],
"duration_ms": 34,
"env": {},
"maturity": "workflow-validated",
"limitations": [
"The job names the PDN or DEF/LEF and any power and decap maps; input_hash covers the job path and arguments, not their contents."
]
}
}
Fields
| Field | Always present | What it is |
|---|---|---|
schema | yes | Envelope version — loom-result/1.1. |
tool / tool_version | yes | Which engine ran, and its resolved version. |
status | yes | Execution state: ok or error. |
engineering | yes | Design verdict — see below. |
input_hash | yes | BLAKE3 over the resolved binary identity, version, declared environment, and argument vector. |
result | yes | The engine’s own --json, passed through unchanged. |
artifacts | yes (may be []) | Produced files: role, path, and a BLAKE3 hash (null if the file is declared but absent). Paths are workspace-relative where they can be (see below). Sign-off engines report the path as report_path in their own output, so asking for a report never costs you the parsed result. |
error | yes (null when fine) | Populated only when status is error. |
logs | yes (null when none) | Structured events plus a compact summary. |
provenance | yes | cmd, duration_ms, the declared env, plus the engine’s maturity and its limitations — see below. |
Parse defensively: new optional fields may appear, so ignore what you do not recognize.
The two status axes
The single most important thing about this envelope: status and engineering.status are
independent, and neither implies the other.
status | engineering.status | |
|---|---|---|
| Question | Could we invoke and observe the process? | What does the evidence support about the design? |
| Values | ok, error | pass, fail, unknown, not_applicable |
A DRC run that exits cleanly having found three violations is a successful execution of a failed check:
{ "status": "ok",
"engineering": { "status": "fail", "assertion": "drc-clean",
"summary": "3 rule violation(s) found" } }
Collapsing these into one field would force every caller to know each engine’s private JSON shape to learn whether the design actually passed — and would leave no way to say “the tool ran, but the evidence is not trustworthy.”
When the verdict is unknown
unknown is not a failure. It means no trustworthy conclusion was reached, and it is the
answer in every one of these cases:
- the engine declares no assertion — silence is never a pass;
- the execution did not succeed — a crashed, missing, or timed-out tool reached no conclusion
about the design, and reporting
failwould blame the design for a tooling defect; - the verdict field is missing, null, or the wrong type — absence of evidence is not evidence of a defect;
- the engine’s assertion is malformed and was therefore dropped;
- the engine’s maturity is
discovered— nothing has validated its result shape, so a claim from it is not yet something to rely on.
not_applicable is different again: the operation establishes no engineering claim at all — a
viewer, a format converter.
The rule to build on: treat anything other than pass as not proven, and never infer a
pass from a field you cannot interpret.
engineering.assertion names the claim (drc-clean, timing-met, …) and is null when none
was declared. Which field an engine derives its verdict from is published in its
descriptor.
Errors — structured, never a crash
Every failure mode — engine not installed, non-zero exit, unparsable output — comes back as the
same envelope with status: "error", so a session, flow, or job stays alive and can retry:
{ "status": "error",
"engineering": { "status": "unknown", "assertion": null,
"summary": "execution did not complete; no engineering conclusion reached" },
"error": { "code": "engine_nonzero", "message": "vyges-drc exited with code 2",
"exit_code": 2, "stderr_tail": "…" } }
error.code | Meaning |
|---|---|
not_installed | The engine binary is not on PATH — run vyges install <engine>. |
exec_failed | The process could not be started. |
engine_nonzero | It ran and exited non-zero; exit_code and stderr_tail are included. |
Note the pairing: an error envelope carries engineering.status: "unknown", never "fail".
input_hash — reproducibility and caching
input_hash is a BLAKE3 digest over the resolved binary identity (path or container image), the
engine version, the declared environment (PDK_ROOT and friends), and the full argument vector.
Pinning a different version, image, or PDK changes the hash.
Use it as a cache key and as a determinism check: the same input_hash must yield the same
artifacts hashes. If it does not, something outside the recorded inputs is leaking into the
result.
It covers the invocation, not the full transitive input closure — a SPICE deck’s .include
chain or a rule deck’s imports are not enumerated. In fact it does not hash file contents at
all: a job file that names a netlist contributes only its own path, so editing that netlist in
place leaves the hash unchanged. Treat it as an invocation fingerprint, not a content hash of every
byte the engine read.
Rather than leave that boundary to be rediscovered, every engine declares its own:
"provenance": {
"maturity": "workflow-validated",
"limitations": [
"The job names the floorplan and per-block power; input_hash covers the job path and arguments, not their contents."
]
}
limitations is the engine’s own statement, carried beside the hash it qualifies.
maturity says how far that engine’s evidence has been proven — and at discovered a verdict is
suppressed to unknown however well-formed the assertion. Both are declared in the
tool descriptor.
Artifact paths are workspace-relative
A path is reported relative to the working directory whenever it lies inside it:
"artifacts": [ { "role": "thermal_report", "path": "report.rpt", "hash": "blake3:…" } ]
An absolute path pins evidence to one machine and carries the host’s directory layout into a record meant to be shared, cached and compared. A path outside the workspace is left exactly as given — it has no portable form, and rewriting it would produce something that does not resolve.
The envelope is validated on the way out
Every envelope is checked against the published schema before it is returned. It is easy to validate only what arrives from elsewhere and never what leaves — the strong guarantees then apply to hypothetical third parties and the weak ones to your own evidence. This is the other way round.
A violation is our defect, not yours: it is reported as a MCP-ENVELOPE-INVALID event and the
envelope is still returned, because a caller holding a slightly wrong envelope is better off than
one holding none.
logs — the causal trail
Engines emit structured events on stderr. Those are collected here as events,
with a summary giving counts by severity and the distinct codes seen, so you get a queryable
trail plus a small handle without the full blob. logs is null when a run produced no events.
During a long run these same events stream live as MCP notifications/message, so an agent
sees progress instead of waiting for a capture-at-end dump.
Stability
loom-result/1.1 added the engineering block; 1.0 had a single status field. Additions are
backwards-compatible — parse defensively, ignore unknown fields, and treat a change to an existing
field’s meaning as a version bump.