Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conformance/adapter/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ assert_run_results() {
while IFS= read -r oath; do
record="$abs/.varar/$oath.json"
[ -f "$record" ] || fail "$dir: the run wrote no .varar/$oath.json" "Every adapter persists one run record per oath — the language server reads them." "See doc/adr/0014-run-results-are-a-cross-port-contract.md."
jq -e --arg o "$oath" '.version == 1 and .oathPath == $o and (.sourceHash | startswith("fnv1a:"))' "$record" >/dev/null || fail "$dir: .varar/$oath.json is not the documented payload" "Expected version 1, oathPath \"$oath\", and an fnv1a: sourceHash." "Got: $(jq -c '{version, oathPath, sourceHash}' "$record")"
jq -e --arg o "$oath" '.version == 2 and .oathPath == $o and (.sourceHash | startswith("fnv1a:"))' "$record" >/dev/null || fail "$dir: .varar/$oath.json is not the documented payload" "Expected version 2, oathPath \"$oath\", and an fnv1a: sourceHash." "Got: $(jq -c '{version, oathPath, sourceHash}' "$record")"
jq -e '[.examples[].lines[0]] == ([.examples[].lines[0]] | sort)' "$record" >/dev/null ||
fail "$dir: .varar/$oath.json lists its examples out of document order" "Sort them by first line before writing — the framework's own order is not the oath's." "Got lines: $(jq -c '[.examples[].lines[0]]' "$record")"
done <<<"$(oaths_on_disk "$dir")"
Expand Down
12 changes: 11 additions & 1 deletion conformance/run-results/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,22 @@ reads the same payload.

## The value every port builds

Three examples, one per branch of the writer:
Four examples, one per branch of the writer:

1. **passed** — no `failure` key at all.
2. **failed with a mismatch** — `cells` and `anchor` both present, a `£` in the
name and a `\n` in the message.
3. **failed by throwing** — `failure` present, `cells` and `anchor` both absent.
4. **failed inside a referenced section** (ADR 0016) — `failure.docPath` names
the oath the failing step was *written* in, and every offset in that failure
(`line`, `cells`, `anchor`) is into **that** document, not this one. Its hash
is the `documents` entry at the top level. `lines` holds only this oath's own
lines: a spliced step contributes none, because its line is not in this file.

That last one is the reason for `version: 2`. A consumer that places a failure
in the oath named by `oathPath` without checking `docPath` will underline
whatever text happens to sit at those offsets — which is why the field is
pinned here rather than left to each port.

`stack` is fixed to `<stack>` here. On disk it is runtime-shaped (a V8 stack, a
JVM trace, a rendered Rust location) and no consumer parses it — but it is part
Expand Down
32 changes: 31 additions & 1 deletion conformance/run-results/expected.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
{
"version": 1,
"version": 2,
"oathPath": "varar/library.md",
"sourceHash": "fnv1a:1622dfca",
"documents": [
{
"path": "varar/shared/loans.md",
"sourceHash": "fnv1a:2f0e1d3c"
}
],
"examples": [
{
"name": "Maya borrowed *Emma*, due back on June 1, 2026",
Expand Down Expand Up @@ -47,6 +53,30 @@
"message": "expected the library to refuse",
"stack": "<stack>"
}
},
{
"name": "An overdue loan blocks a new one",
"status": "failed",
"lines": [
20
],
"failure": {
"line": 6,
"message": "expected 3 but was 2",
"stack": "<stack>",
"cells": [
{
"from": 41,
"to": 42,
"actual": "2"
}
],
"anchor": {
"from": 41,
"to": 42
},
"docPath": "varar/shared/loans.md"
}
}
]
}
8 changes: 7 additions & 1 deletion doc/adr/0014-run-results-are-a-cross-port-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,13 @@ and the corpus pins the format.**
- `sourceHash` is `hashSource(source)` over the oath's bytes as run. The LSP
drops every diagnostic when the hash no longer matches the buffer, which
is what stops a stale result from pointing at moved text.
- `version` is `1`.
- `version` is `2`. Version 2 adds the per-step document identity reference
blocks need (ADR 0016): `failure.docPath` names the document a failure's
`line`, `cells` and `anchor` are offsets into — absent means the oath
itself — and a top-level `documents` array carries a hash per other oath
whose steps this run spliced in, so a consumer can tell a stale failure
from a live one exactly as `sourceHash` does for the oath. `lines` holds
only the running oath's own lines.

3. **`stack` stays runtime-shaped.** A V8 stack, a JVM stack trace and a Rust
rendered location have nothing in common, and no consumer parses it — it is
Expand Down
15 changes: 7 additions & 8 deletions doc/adr/0016-reuse-is-a-link.md
Original file line number Diff line number Diff line change
Expand Up @@ -640,14 +640,13 @@ Two adapter-level notes:

### Still open

- **Run-result v2 (ADR 0014).** `docPath` reaches the plan and the plan artifact,
but *not* the persisted `.varar/<oath>.json` payload. Until it does, a failure
inside a referenced section is reported to the LSP with spans in that section's
document and a `sourceHash` for the referencing one, so the editor will not
place it. **A mismatch inside a shared section is therefore not yet rendered
correctly in editors** — the run still fails, with the correct message, in
every runner. This is the next piece of work, and it is a cross-port payload
change with its own golden.
- ~~**Run-result v2 (ADR 0014).**~~ **Done** — `.varar/<oath>.json` is version 2:
`failure.docPath` names the document its offsets address, `documents` carries a
hash per referenced oath, and `lines` holds only the running oath's own lines.
The LSP publishes a spliced failure against the referenced document's URI (a
shared oath has no result file of its own, so its failures previously had no
way to reach the editor at all). `conformance/run-results/expected.json` grew a
fourth example, which is what gated the other six ports.
- **Ambiguous anchors** (deviation 2) are undetected; the lint rule requiring
unique headings in a referenced file is not written.
- **LSP reference support** — go-to-definition and hover on a reference block —
Expand Down
20 changes: 18 additions & 2 deletions dotnet/Varar.Core.Tests/RunResultsWireTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ private static string ExpectedPath()
}

private static OathResults Results() => new(
1,
2,
"varar/library.md",
"fnv1a:1622dfca",
[
Expand All @@ -49,7 +49,23 @@ private static string ExpectedPath()
ExampleStatus.Failed,
[8, 9],
new ExampleFailure(9, "expected the library to refuse", "<stack>")),
]);

// A failure inside a section this oath referenced (ADR 0016): every offset is into
// varar/shared/loans.md, named by DocPath and hashed in Documents. Lines holds only
// this oath's own lines.
new ExampleResult(
"An overdue loan blocks a new one",
ExampleStatus.Failed,
[20],
new ExampleFailure(
6,
"expected 3 but was 2",
"<stack>",
ImmutableArray.Create(new CellFailure(41, 42, "2")),
new AnchorRange(41, 42),
"varar/shared/loans.md")),
],
[new ReferencedDocument("varar/shared/loans.md", "fnv1a:2f0e1d3c")]);

[Fact]
public void TheWireFormatMatchesTheCrossPortFixture()
Expand Down
12 changes: 12 additions & 0 deletions dotnet/Varar.Core/Execute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ public static class Execute
// failing step's span (or the first mismatched cell's), which Failures.ToFailure
// reads back so a renderer underlines the step and not its whole line.
FailureAnchor.Attach(err, FailureAnchor.Anchor(err, step.MatchSpan));
// A step a reference block spliced in has spans in the document it was WRITTEN in
// (ADR 0016), so the payload must name that file.
if (step.DocPath is not null)
{
FailureAnchor.AttachDocPath(err, step.DocPath);
}

observations.Add(new StepObservation(i + 1, "fail", err));
thrown = err;
break;
Expand All @@ -110,6 +117,11 @@ public static class Execute
{
var err = rowError ?? new CellMismatchError(bad);
FailureAnchor.Attach(err, FailureAnchor.Anchor(err, steps[^1].MatchSpan));
if (steps[^1].DocPath is not null)
{
FailureAnchor.AttachDocPath(err, steps[^1].DocPath!);
}

observations.Add(new StepObservation(steps.Length, "fail", err));
thrown = err;
}
Expand Down
5 changes: 4 additions & 1 deletion dotnet/Varar.Core/Failure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public static ExampleFailure ToFailure(Exception error, string oathPath, int fal
Message: error.Message,
Stack: error.StackTrace ?? error.Message,
Cells: cells,
Anchor: anchor is null ? null : new AnchorRange(anchor.StartOffset, anchor.EndOffset));
Anchor: anchor is null ? null : new AnchorRange(anchor.StartOffset, anchor.EndOffset),
// The failing step may have been spliced in from another oath (ADR 0016); every offset
// above is then relative to THAT document.
DocPath: FailureAnchor.AttachedDocPath(error));
}
}
17 changes: 17 additions & 0 deletions dotnet/Varar.Core/FailureAnchor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,21 @@ public static void Attach(Exception? error, Span anchor)

/// <summary>The anchor the executor attached, or <c>null</c> if there is none.</summary>
public static Span? Attached(Exception? error) => error?.Data[AnchorKey] as Span;

private const string DocPathKey = "varar.failureDocPath";

/// <summary>
/// Records the document the anchor's offsets belong to, for a step a reference block spliced in
/// from another oath (ADR 0016). Travels the same way and for the same reason as the anchor:
/// the executor knows the step, and whoever builds the failure payload sees only the error.
/// </summary>
public static void AttachDocPath(Exception? error, string docPath)
{
if (error is not null)
{
error.Data[DocPathKey] = docPath;
}
}

public static string? AttachedDocPath(Exception? error) => error?.Data[DocPathKey] as string;
}
22 changes: 20 additions & 2 deletions dotnet/Varar.Core/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,25 @@ public enum ExampleStatus
/// <c>null</c> when they do not apply, and serialize as absent (not null) so a reader that predates
/// them still parses the file. <c>Stack</c> is deliberately runtime-shaped — no consumer parses it.
/// </summary>
/// <param name="DocPath">
/// The document <c>Line</c>, <c>Cells</c> and <c>Anchor</c> are offsets INTO. Null — the
/// overwhelming majority — means the oath itself. Set only when the failing step was spliced in
/// from another oath by a reference block (ADR 0016): its spans belong to that document, and a
/// renderer that placed them in this one would underline whatever text sat at those offsets.
/// </param>
public sealed record ExampleFailure(
int Line,
string Message,
string Stack,
ImmutableArray<CellFailure>? Cells = null,
AnchorRange? Anchor = null);
AnchorRange? Anchor = null,
string? DocPath = null);

/// <summary>
/// An oath other than this one that contributed steps to the run, with its source hash as run
/// (ADR 0016).
/// </summary>
public sealed record ReferencedDocument(string Path, string SourceHash);

/// <summary>
/// The run result for one BDD example. <c>Lines</c> are the 1-based source lines of its steps (the
Expand All @@ -50,8 +63,13 @@ public sealed record ExampleResult(
/// the workspace root; <c>SourceHash</c> is <see cref="Hash.HashSource"/> over the oath as it was
/// run, so a reader can tell whether the offsets still apply to the buffer in front of it.
/// </summary>
/// <param name="Documents">
/// Every OTHER document this run's steps came from — the oaths a reference block pulled steps in
/// from (ADR 0016), with their hashes as run. Empty when no step was spliced in.
/// </param>
public sealed record OathResults(
int Version,
string OathPath,
string SourceHash,
ImmutableArray<ExampleResult> Examples);
ImmutableArray<ExampleResult> Examples,
ImmutableArray<ReferencedDocument> Documents = default);
20 changes: 20 additions & 0 deletions dotnet/Varar.Core/ResultJson.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ public static string ToWireJson(OathResults results)
writer.WriteNumber("version", results.Version);
writer.WriteString("oathPath", results.OathPath);
writer.WriteString("sourceHash", results.SourceHash);
if (!results.Documents.IsDefaultOrEmpty)
{
writer.WriteStartArray("documents");
foreach (var document in results.Documents)
{
writer.WriteStartObject();
writer.WriteString("path", document.Path);
writer.WriteString("sourceHash", document.SourceHash);
writer.WriteEndObject();
}

writer.WriteEndArray();
}

writer.WriteStartArray("examples");
foreach (var example in results.Examples)
{
Expand Down Expand Up @@ -95,6 +109,12 @@ private static void WriteFailure(Utf8JsonWriter writer, ExampleFailure failure)
writer.WriteEndObject();
}

// Present only on a step a reference block spliced in from another oath (ADR 0016).
if (failure.DocPath is not null)
{
writer.WriteString("docPath", failure.DocPath);
}

writer.WriteEndObject();
}
}
44 changes: 41 additions & 3 deletions dotnet/Varar.Runner/Results.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ public sealed class Results
private readonly Dictionary<string, string> sources = new(StringComparer.Ordinal);
private readonly Dictionary<string, List<ExampleResult>> examples = new(StringComparer.Ordinal);

// Per oath: the other documents its steps were spliced in from (ADR 0016), as path → source.
private readonly Dictionary<string, Dictionary<string, string>> documents =
new(StringComparer.Ordinal);

/// <summary><c>&lt;root&gt;/.varar/&lt;oathPath&gt;.json</c> — the file the LSP watches.</summary>
public static string ResultFilePath(string root, string oathPath) =>
Path.Combine(root, ".varar", oathPath.Replace('/', Path.DirectorySeparatorChar) + ".json");
Expand All @@ -33,10 +37,33 @@ public static string Write(string root, OathResults results)
return out_;
}

/// <summary>Accumulates one example's outcome; the oath is written once its examples are in.</summary>
public void Record(string oathPath, string source, ExampleResult result)
/// <summary>
/// Accumulates one example's outcome; the oath is written once its examples are in.
/// <paramref name="referencedSources"/> carries the OTHER documents this oath's steps were
/// spliced in from (ADR 0016), as path → source; their hashes go in the payload so a consumer
/// can tell a stale failure from a live one.
/// </summary>
public void Record(
string oathPath,
string source,
ExampleResult result,
IReadOnlyDictionary<string, string>? referencedSources = null)
{
sources[oathPath] = source;
if (referencedSources is { Count: > 0 })
{
if (!documents.TryGetValue(oathPath, out var docs))
{
docs = [];
documents[oathPath] = docs;
}

foreach (var (path, text) in referencedSources)
{
docs[path] = text;
}
}

if (!examples.TryGetValue(oathPath, out var recorded))
{
recorded = [];
Expand All @@ -54,11 +81,22 @@ public void FlushAll(string root)
{
foreach (var (oathPath, recorded) in examples.ToList())
{
var docs = documents.TryGetValue(oathPath, out var held)
? held.OrderBy(d => d.Key, StringComparer.Ordinal)
.Select(d => new ReferencedDocument(d.Key, Hash.HashSource(d.Value)))
.ToImmutableArray()
: [];
Write(
root,
new OathResults(1, oathPath, Hash.HashSource(sources[oathPath]), [.. recorded]));
new OathResults(
2,
oathPath,
Hash.HashSource(sources[oathPath]),
[.. recorded],
docs));
}

examples.Clear();
documents.Clear();
}
}
Loading
Loading