varar-core 0.7.0 → 0.8.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d857a484c127ac8dc313cff7dc855f7924e9b90dd9935b3575e880a4abc5e9c
4
- data.tar.gz: becbdbfe8eda5afc06b7506b719fafd3868e1506cec1e484e461766b7b39225a
3
+ metadata.gz: 4e14558a60af014ad7a1e7edfea61644a415408c5da1e384d6c34d4f191a2743
4
+ data.tar.gz: f91c617e21f8affcd745676fffba8be6b1ab484ec9c2c4e17b7173d6d07e1733
5
5
  SHA512:
6
- metadata.gz: f1f17d5bf2fef77cc8c8dbb4d0991c72232b08d0fe139f0c60768016c86b2eeeca33e54b66a4098ba1ab5bf15cbb00aa91f0a3e48e9ceadc7c457c630085d62c
7
- data.tar.gz: 05404cecafaeca822ede078824c9bfad5d959236ea7c23f00a8b44a9d0ffe4a002ff9363d7c52bc86649d2d3bfd6b353f68c67c1131ac0c867172beac0e0913f
6
+ metadata.gz: 4a89ff743d9d00027565cad5933ad28f2bc05af437a773feeac38f9c7c640ee25d548af761852343259afed64654205360e5e2983df9fbd82bc14a1167746013
7
+ data.tar.gz: 66c9b893012bd279f337db321fbae0262802bf579f627a7d7e0db37ff4183b3ec3112c413af5614481521d61543ca2e634b966d54260032615cf0c97057fd89f
@@ -46,6 +46,6 @@ module Varar
46
46
  # one example. See ADR 0012.
47
47
  Example = Data.define(:scope_stack, :span, :body, :preceded_by_delimiter)
48
48
 
49
- VarDoc = Data.define(:path, :source, :examples, :orphan_attachments)
49
+ Doc = Data.define(:path, :source, :examples, :orphan_attachments)
50
50
  end
51
51
  end
@@ -4,39 +4,39 @@ require 'json'
4
4
 
5
5
  module Varar
6
6
  module Core
7
- # JSON serializers byte-for-byte compatible with JS `JSON.stringify(v, null, 2)`:
7
+ # Writes varar.lock.json the way JS `JSON.stringify(v, null, 2)` does:
8
8
  # 2-space indent, LF, trailing newline, non-ASCII raw, empty containers as
9
- # {}/[]. `canonical_stringify` recursively sorts object keys (the goldens);
10
- # `ordered_stringify` preserves insertion order (varar.lock.json).
9
+ # {}/[], keys in insertion order.
11
10
  #
12
- # The container layout is hand-rolled because Ruby's JSON.pretty_generate
13
- # renders empty arrays/objects as "[\n\n]". Scalar encoding is delegated to
14
- # the stdlib, which matches JS (escapes " \ control chars, keeps non-ASCII raw).
11
+ # A committed, language-shared file, so the layout is hand-rolled rather
12
+ # than left to JSON.pretty_generate, which renders empty arrays/objects as
13
+ # "[\n\n]" a Ruby run would otherwise churn the file against every other
14
+ # port's. Scalar encoding is delegated to the stdlib, which matches JS
15
+ # (escapes " \ control chars, keeps non-ASCII raw).
16
+ #
17
+ # Conformance goldens are NOT compared through here: a port has to agree
18
+ # with the goldens' CONTENT, and every spec parses them and compares deep
19
+ # equality.
15
20
  module CanonicalJson
16
21
  module_function
17
22
 
18
- def canonical_stringify(value)
19
- "#{encode(value, '', sort_keys: true)}\n"
20
- end
21
-
22
23
  def ordered_stringify(value)
23
- "#{encode(value, '', sort_keys: false)}\n"
24
+ "#{encode(value, '')}\n"
24
25
  end
25
26
 
26
- def encode(value, indent, sort_keys:)
27
+ def encode(value, indent)
27
28
  case value
28
29
  when Hash
29
30
  return '{}' if value.empty?
30
31
 
31
- keys = sort_keys ? value.keys.sort : value.keys
32
32
  inner = "#{indent} "
33
- items = keys.map { |key| "#{inner}#{key.to_s.to_json}: #{encode(value[key], inner, sort_keys: sort_keys)}" }
33
+ items = value.keys.map { |key| "#{inner}#{key.to_s.to_json}: #{encode(value[key], inner)}" }
34
34
  "{\n#{items.join(",\n")}\n#{indent}}"
35
35
  when Array
36
36
  return '[]' if value.empty?
37
37
 
38
38
  inner = "#{indent} "
39
- items = value.map { |element| "#{inner}#{encode(element, inner, sort_keys: sort_keys)}" }
39
+ items = value.map { |element| "#{inner}#{encode(element, inner)}" }
40
40
  "[\n#{items.join(",\n")}\n#{indent}]"
41
41
  else
42
42
  value.to_json
@@ -98,8 +98,8 @@ module Varar
98
98
  }
99
99
  end
100
100
 
101
- # Project a VarDoc to the wire dict for the var-doc artifact.
102
- def to_var_doc_artifact(doc)
101
+ # Project a Doc to the wire dict for the var-doc artifact.
102
+ def to_doc_artifact(doc)
103
103
  {
104
104
  'path' => doc.path,
105
105
  'examples' => doc.examples.map { |ex| example_hash(ex) },
@@ -137,7 +137,7 @@ module Varar
137
137
 
138
138
  # Project an ExecutionPlan to the wire dict for the plan artifact.
139
139
  def to_plan_artifact(plan)
140
- source = plan.var_doc.source
140
+ source = plan.doc.source
141
141
  {
142
142
  'examples' => plan.examples.map { |ex| planned_example_hash(ex, source) },
143
143
  'diagnostics' => plan.diagnostics.map do |d|
@@ -206,8 +206,8 @@ module Varar
206
206
  end
207
207
 
208
208
  # Run all examples and return the four-artifact bundle. Port of runConformance.
209
- def run_conformance(var_doc, registry, create_context, parameter_types = [])
210
- execution = Plan.plan(var_doc, registry)
209
+ def run_conformance(doc, registry, create_context, parameter_types = [])
210
+ execution = Plan.plan(doc, registry)
211
211
  observed = Hash.new { |h, k| h[k] = [] }
212
212
  observer = ->(o) { observed[o.example_index] << o }
213
213
  queue = Execute.collect_examples(execution, create_context: create_context, observer: observer)
@@ -244,7 +244,7 @@ module Varar
244
244
  end
245
245
 
246
246
  {
247
- var_doc: to_var_doc_artifact(var_doc),
247
+ doc: to_doc_artifact(doc),
248
248
  registry: to_registry_artifact(registry, parameter_types),
249
249
  plan: to_plan_artifact(execution),
250
250
  trace: { 'examples' => trace_examples }
@@ -10,14 +10,14 @@ module Varar
10
10
  module Core
11
11
  # One example-producing paragraph, as recorded in the baseline.
12
12
  BaselineExample = Data.define(:name, :line)
13
- # The committed baseline for one spec file.
14
- SpecBaseline = Data.define(:source_hash, :examples)
15
- # The whole varar.lock.json: every spec keyed by its POSIX path.
16
- VarLock = Data.define(:version, :specs)
13
+ # The committed baseline for one oath file.
14
+ OathBaseline = Data.define(:source_hash, :examples)
15
+ # The whole varar.lock.json: every oath keyed by its POSIX path.
16
+ LockFile = Data.define(:version, :oaths)
17
17
  # A paragraph the baseline says was an example and now matches no step.
18
18
  Drift = Data.define(:name, :line, :span)
19
19
 
20
- # Spec drift detection: a paragraph the committed varar.lock.json baseline
20
+ # Oath drift detection: a paragraph the committed varar.lock.json baseline
21
21
  # recorded as an example that now matches no step. Pure, byte-identical to
22
22
  # the TS port so varar.lock.json is shared across languages. Port of drift.ts.
23
23
  #
@@ -62,25 +62,25 @@ module Varar
62
62
  end
63
63
 
64
64
  # The current example-producing paragraphs, in document order.
65
- def live_examples(var_doc, plan)
66
- var_doc.examples.filter_map do |candidate|
65
+ def live_examples(doc, plan)
66
+ doc.examples.filter_map do |candidate|
67
67
  next unless live?(candidate.span, plan)
68
68
 
69
69
  BaselineExample.new(name: Plan.derive_example_name(candidate.body), line: candidate.span.start_line)
70
70
  end
71
71
  end
72
72
 
73
- def derive_spec_baseline(source, var_doc, plan)
74
- SpecBaseline.new(source_hash: Hash32.hash_source(source), examples: live_examples(var_doc, plan))
73
+ def derive_oath_baseline(source, doc, plan)
74
+ OathBaseline.new(source_hash: Hash32.hash_source(source), examples: live_examples(doc, plan))
75
75
  end
76
76
 
77
77
  # Paragraphs the baseline recorded as examples that now match zero steps.
78
78
  # Each re-identified by the most word-similar current paragraph at/above
79
79
  # the threshold (exact name scores 1; ties break toward the nearest line).
80
- def detect_drift(baseline, var_doc, plan)
80
+ def detect_drift(baseline, doc, plan)
81
81
  return [] if baseline.nil?
82
82
 
83
- candidates = var_doc.examples
83
+ candidates = doc.examples
84
84
  tokens = candidates.map { |c| tokenize(Plan.derive_example_name(c.body)) }
85
85
  live = candidates.map { |c| live?(c.span, plan) }
86
86
 
@@ -111,43 +111,76 @@ module Varar
111
111
  drifts.map { |d| Diagnostics.drift_detected(d.name, d.span) }
112
112
  end
113
113
 
114
- # One spec's baseline reconciliation against a BaselineStore. In update
114
+ # One oath's baseline reconciliation against a BaselineStore. In update
115
115
  # mode, accept all drift (re-record, report nothing); otherwise detect
116
116
  # drift and rewrite the baseline only on a clean run, so an unacknowledged
117
117
  # drift keeps its old entry (and stays red).
118
- def reconcile_drift(store, spec_path, source, var_doc, plan, update: false)
118
+ def reconcile_drift(store, oath_path, source, doc, plan, update: false)
119
119
  text = store.read
120
- lock = text ? parse_var_lock(text) : nil
121
- baseline = lock ? lock.specs[spec_path] : nil
122
- drifts = update ? [] : detect_drift(baseline, var_doc, plan)
120
+ lock = text ? parse_lock_file(text) : nil
121
+ baseline = lock ? lock.oaths[oath_path] : nil
122
+ drifts = update ? [] : detect_drift(baseline, doc, plan)
123
123
  if update || drifts.empty?
124
- specs = lock ? lock.specs.dup : {}
125
- specs[spec_path] = derive_spec_baseline(source, var_doc, plan)
126
- store.write(stringify_var_lock(VarLock.new(version: 1, specs: specs)))
124
+ oaths = lock ? lock.oaths.dup : {}
125
+ oaths[oath_path] = derive_oath_baseline(source, doc, plan)
126
+ store.write(stringify_lock_file(LockFile.new(version: 2, oaths: oaths)))
127
127
  end
128
128
  drifts
129
129
  end
130
130
 
131
- def parse_var_lock(text)
131
+ # Drop every baseline whose oath path is not in +keep_paths+ — the entries
132
+ # left behind when an oath is deleted or moved. Pure counterpart of
133
+ # parse_lock_file / stringify_lock_file; the caller decides what "still
134
+ # exists" means.
135
+ def prune_lock_file(lock, keep_paths)
136
+ keep = keep_paths.to_a
137
+ LockFile.new(version: 2, oaths: lock.oaths.slice(*keep))
138
+ end
139
+
140
+ # The whole-lock counterpart of reconcile_drift, run ONCE per run rather
141
+ # than per oath: reconciliation cannot see paths that no longer exist, so
142
+ # without this the lock silently accumulates dead entries and stops being
143
+ # a faithful inventory of the oath set (#70).
144
+ #
145
+ # +keep_paths+ MUST be everything the +docs+ globs currently match — never
146
+ # the set the run happened to execute. Runs are routinely filtered, and
147
+ # pruning against a filtered set would delete live baselines.
148
+ #
149
+ # Removal is still not *gated*: a deleted oath is a different signal from
150
+ # drift and stays ungated (ADR 0002). This only stops preserving dead
151
+ # state, and only under +update+. Returns the paths removed (or, without
152
+ # +update+, the ones that would be).
153
+ def prune_baselines(store, keep_paths, update: false)
154
+ text = store.read
155
+ lock = text ? parse_lock_file(text) : nil
156
+ return [] unless lock
157
+
158
+ keep = keep_paths.to_a
159
+ stale = lock.oaths.keys.reject { |path| keep.include?(path) }
160
+ store.write(stringify_lock_file(prune_lock_file(lock, keep))) if update && !stale.empty?
161
+ stale
162
+ end
163
+
164
+ def parse_lock_file(text)
132
165
  parsed = JSON.parse(text)
133
- return nil unless parsed.is_a?(::Hash) && parsed['version'] == 1
166
+ return nil unless parsed.is_a?(::Hash) && parsed['version'] == 2
134
167
 
135
- specs_raw = parsed['specs']
136
- return nil unless specs_raw.is_a?(::Hash)
168
+ oaths_raw = parsed['oaths']
169
+ return nil unless oaths_raw.is_a?(::Hash)
137
170
 
138
- specs = {}
139
- specs_raw.each do |path, value|
140
- baseline = parse_spec_baseline(value)
171
+ oaths = {}
172
+ oaths_raw.each do |path, value|
173
+ baseline = parse_oath_baseline(value)
141
174
  return nil if baseline.nil?
142
175
 
143
- specs[path] = baseline
176
+ oaths[path] = baseline
144
177
  end
145
- VarLock.new(version: 1, specs: specs)
178
+ LockFile.new(version: 2, oaths: oaths)
146
179
  rescue JSON::ParserError, TypeError
147
180
  nil
148
181
  end
149
182
 
150
- def parse_spec_baseline(value)
183
+ def parse_oath_baseline(value)
151
184
  return nil unless value.is_a?(::Hash)
152
185
 
153
186
  source_hash = value['sourceHash']
@@ -161,7 +194,7 @@ module Varar
161
194
 
162
195
  examples << parsed
163
196
  end
164
- SpecBaseline.new(source_hash: source_hash, examples: examples)
197
+ OathBaseline.new(source_hash: source_hash, examples: examples)
165
198
  end
166
199
 
167
200
  def parse_baseline_example(value)
@@ -174,19 +207,19 @@ module Varar
174
207
  BaselineExample.new(name: name, line: line)
175
208
  end
176
209
 
177
- # Serialize varar.lock.json deterministically: spec paths sorted, examples
178
- # in document order, insertion-order keys otherwise (version, specs;
210
+ # Serialize varar.lock.json deterministically: oath paths sorted, examples
211
+ # in document order, insertion-order keys otherwise (version, oaths;
179
212
  # sourceHash, examples; name, line) — NOT canonical JSON's key sort.
180
- def stringify_var_lock(lock)
181
- specs = {}
182
- lock.specs.keys.sort.each do |path|
183
- baseline = lock.specs[path]
184
- specs[path] = {
213
+ def stringify_lock_file(lock)
214
+ oaths = {}
215
+ lock.oaths.keys.sort.each do |path|
216
+ baseline = lock.oaths[path]
217
+ oaths[path] = {
185
218
  'sourceHash' => baseline.source_hash,
186
219
  'examples' => baseline.examples.map { |e| { 'name' => e.name, 'line' => e.line } }
187
220
  }
188
221
  end
189
- CanonicalJson.ordered_stringify({ 'version' => 1, 'specs' => specs })
222
+ CanonicalJson.ordered_stringify({ 'version' => 2, 'oaths' => oaths })
190
223
  end
191
224
  end
192
225
  end
@@ -42,17 +42,17 @@ module Varar
42
42
  def execute_plan(plan, sink:, create_context:, observer: nil, reporter: nil)
43
43
  plan.diagnostics.each { |d| reporter.call(d) } if reporter
44
44
  create_ctx = create_context || ->(_file) { {} }
45
- var_path = plan.var_doc.path
45
+ oath_path = plan.doc.path
46
46
 
47
47
  plan.examples.each_with_index do |ex, example_index|
48
48
  seen_lines = {}
49
49
  ex.steps.each { |s| seen_lines[s.match_span.start_line] = true }
50
50
  info = { lines: seen_lines.keys }
51
- sink.call(ex.name, build_run(plan, ex, example_index, create_ctx, observer, var_path), info)
51
+ sink.call(ex.name, build_run(plan, ex, example_index, create_ctx, observer, oath_path), info)
52
52
  end
53
53
  end
54
54
 
55
- def build_run(plan, ex, example_index, create_ctx, observer, var_path)
55
+ def build_run(plan, ex, example_index, create_ctx, observer, oath_path)
56
56
  lambda do
57
57
  state_by_file = {}
58
58
  last_return = nil
@@ -97,7 +97,7 @@ module Varar
97
97
  raise ReturnShapeError, "unknown step kind: #{step.step_def.kind}"
98
98
  end
99
99
  rescue StandardError => e
100
- augmented = augment_stack(e, step, var_path)
100
+ augmented = augment_stack(e, step, oath_path)
101
101
  observer&.call(observation(ex, example_index, i + 1, file, 'fail', augmented))
102
102
  thrown = augmented
103
103
  break
@@ -117,7 +117,7 @@ module Varar
117
117
  bad = CellDiffs.compare_row(last_return, ex.row_checks).reject(&:ok)
118
118
  if row_error || !bad.empty?
119
119
  last_step = ex.steps.last
120
- augmented = augment_stack(row_error || CellMismatchError.new(bad), last_step, var_path)
120
+ augmented = augment_stack(row_error || CellMismatchError.new(bad), last_step, oath_path)
121
121
  observer&.call(observation(ex, example_index, ex.steps.length,
122
122
  last_step.step_def.expression_source_file, 'fail', augmented))
123
123
  thrown = augmented
@@ -129,7 +129,7 @@ module Varar
129
129
  if thrown.nil?
130
130
  error = UnexpectedPassError.new
131
131
  last = ex.steps.last
132
- raise(last ? augment_stack(error, last, var_path) : error)
132
+ raise(last ? augment_stack(error, last, oath_path) : error)
133
133
  end
134
134
  raise thrown if ex.expected_error_message && !thrown.message.include?(ex.expected_error_message)
135
135
 
@@ -177,7 +177,7 @@ module Varar
177
177
 
178
178
  inline_returned = slots[0...step.args.length]
179
179
  source_texts = step.param_spans.map do |s|
180
- Offsets.utf16_slice(plan.var_doc.source, s.start_offset, s.end_offset)
180
+ Offsets.utf16_slice(plan.doc.source, s.start_offset, s.end_offset)
181
181
  end
182
182
  param_diffs = ParamDiff.compare_params(inline_returned, step.args, step.param_spans, source_texts,
183
183
  step.formats).reject(&:ok)
@@ -199,9 +199,12 @@ module Varar
199
199
  end
200
200
 
201
201
  # In TS this injects a synthetic `at <text> (path:line:col)` frame for
202
- # editor navigation; the conformance trace derives the anchor separately
203
- # via failure_anchor, so here it is a no-op that returns the error.
204
- def augment_stack(error, _step, _var_path)
202
+ # editor navigation. Ruby has no writable stack text to splice into, so
203
+ # it records the anchor structurally instead the failing step's span
204
+ # (or the first mismatched cell's), which Failures.to_failure reads back
205
+ # so a renderer underlines the step and not its whole line.
206
+ def augment_stack(error, step, _var_path)
207
+ FailureAnchor.attach_anchor(error, FailureAnchor.failure_anchor(error, step.match_span))
205
208
  error
206
209
  end
207
210
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'varar/core/cell_diff'
4
+ require 'varar/core/failure_anchor'
5
+ require 'varar/core/result'
6
+
7
+ module Varar
8
+ module Core
9
+ # Converts a caught step error into the structured ExampleFailure payload —
10
+ # port of failure.ts / failure.py / Failure.java / failure.rs. Shared by
11
+ # every producer so failures are byte-identical across ports.
12
+ #
13
+ # Where TS scrapes an injected `<path>:line:col` stack frame for the failing
14
+ # line, Ruby reads it off the anchor the executor attached (the Rust port
15
+ # does the same): a Ruby backtrace has no synthetic frame to scrape, and the
16
+ # anchor already carries the line the frame would have named.
17
+ module Failures
18
+ module_function
19
+
20
+ # A caught step error → the ExampleResult.failure payload.
21
+ #
22
+ # `fallback_line` is used when the error carries no anchor, i.e. it never
23
+ # passed through a step.
24
+ def to_failure(error, _oath_path, fallback_line)
25
+ anchor = FailureAnchor.attached_anchor(error)
26
+
27
+ ExampleFailure.new(
28
+ line: anchor ? anchor.start_line : fallback_line,
29
+ message: error.message,
30
+ stack: render_stack(error),
31
+ cells: failing_cells(error),
32
+ anchor: anchor && AnchorRange.new(from: anchor.start_offset, to: anchor.end_offset)
33
+ )
34
+ end
35
+
36
+ # Every mismatched cell — table, header-bound row, inline capture or doc
37
+ # string. nil (not an empty array) when the error is not a mismatch, so
38
+ # the key stays absent in the serialized payload.
39
+ def failing_cells(error)
40
+ return nil unless error.is_a?(CellMismatchError)
41
+
42
+ failing = error.cells.reject(&:ok).map do |c|
43
+ CellFailure.new(from: c.span.start_offset, to: c.span.end_offset, actual: c.actual)
44
+ end
45
+ failing.empty? ? nil : failing
46
+ end
47
+
48
+ # Display-only: the message plus Ruby's own backtrace. Runtime-shaped by
49
+ # design (ADR 0014) — no consumer parses it.
50
+ def render_stack(error)
51
+ ([error.message] + Array(error.backtrace)).join("\n")
52
+ end
53
+ end
54
+ end
55
+ end
@@ -12,6 +12,13 @@ module Varar
12
12
  module FailureAnchor
13
13
  module_function
14
14
 
15
+ # The anchor travels with the raised error, from the executor (which knows
16
+ # the step) to Failures.to_failure (which only sees the error) — the same
17
+ # job TS does with a global symbol on the Error. An instance variable on
18
+ # the exception, so it never shows up in `inspect` output the way an
19
+ # extra attribute would.
20
+ ANCHOR_IVAR = :@varar_failure_anchor
21
+
15
22
  def failure_anchor(error, fallback)
16
23
  case error
17
24
  when CellMismatchError
@@ -21,6 +28,19 @@ module Varar
21
28
  fallback
22
29
  end
23
30
  end
31
+
32
+ # Record on the error itself where the failure points.
33
+ def attach_anchor(error, anchor)
34
+ error.instance_variable_set(ANCHOR_IVAR, anchor) if error.respond_to?(:instance_variable_set)
35
+ end
36
+
37
+ # The anchor the executor attached, or nil if there is none — then a
38
+ # renderer only has the failing line to go on.
39
+ def attached_anchor(error)
40
+ return nil unless error.respond_to?(:instance_variable_get)
41
+
42
+ error.instance_variable_get(ANCHOR_IVAR)
43
+ end
24
44
  end
25
45
  end
26
46
  end
@@ -5,7 +5,7 @@ require 'varar/core/structurer'
5
5
 
6
6
  module Varar
7
7
  module Core
8
- # Parse +source+ into a VarDoc: scan blocks, then group into Examples.
8
+ # Parse +source+ into a Doc: scan blocks, then group into Examples.
9
9
  # Port of parse.ts.
10
10
  module Parse
11
11
  module_function
@@ -29,9 +29,9 @@ module Varar
29
29
  end
30
30
  end
31
31
 
32
- ExecutionPlan = Data.define(:var_doc, :examples, :diagnostics)
32
+ ExecutionPlan = Data.define(:doc, :examples, :diagnostics)
33
33
 
34
- # Produce an ExecutionPlan from a VarDoc + Registry: match step expressions
34
+ # Produce an ExecutionPlan from a Doc + Registry: match step expressions
35
35
  # against every text block, attach trailing tables/fences, detect
36
36
  # header-bound tables, and collect diagnostics. Port of plan.ts.
37
37
  module Plan
@@ -52,11 +52,11 @@ module Varar
52
52
 
53
53
  module_function
54
54
 
55
- def plan(var_doc, registry)
55
+ def plan(doc, registry)
56
56
  diagnostics = []
57
57
 
58
58
  # Phase 1: plan each candidate paragraph independently into a "unit".
59
- units = var_doc.examples.map { |ex| plan_candidate(ex, var_doc, registry, diagnostics) }
59
+ units = doc.examples.map { |ex| plan_candidate(ex, doc, registry, diagnostics) }
60
60
 
61
61
  # Phase 2: group adjacent candidates into examples. A matching candidate
62
62
  # continues the open example when no delimiter (heading / `---`) precedes
@@ -66,7 +66,7 @@ module Varar
66
66
  examples = []
67
67
  open = nil
68
68
  flush = lambda do
69
- examples << finish_merged(open, var_doc.source) if open
69
+ examples << finish_merged(open, doc.source) if open
70
70
  open = nil
71
71
  end
72
72
  units.each do |unit|
@@ -89,7 +89,7 @@ module Varar
89
89
  end
90
90
  flush.call
91
91
 
92
- ExecutionPlan.new(var_doc: var_doc, examples: examples, diagnostics: diagnostics)
92
+ ExecutionPlan.new(doc: doc, examples: examples, diagnostics: diagnostics)
93
93
  end
94
94
 
95
95
  def start_merged(unit)
@@ -124,7 +124,7 @@ module Varar
124
124
 
125
125
  # Plan a single candidate paragraph (plus attached tables/fences) in
126
126
  # isolation. Emits ambiguity / error-fence diagnostics into +diagnostics+.
127
- def plan_candidate(ex, var_doc, registry, diagnostics)
127
+ def plan_candidate(ex, doc, registry, diagnostics)
128
128
  had_ambiguous = false
129
129
  steps_by_block = {}
130
130
 
@@ -135,7 +135,7 @@ module Varar
135
135
  result = plan_block(block.text, registry)
136
136
 
137
137
  result.ambiguities.each do |collision|
138
- span = lift_span(var_doc.source, block, collision.match_start, collision.match_end)
138
+ span = lift_span(doc.source, block, collision.match_start, collision.match_end)
139
139
  cp_start = Offsets.cp_index_for_utf16(block.text, collision.match_start)
140
140
  cp_end = Offsets.cp_index_for_utf16(block.text, collision.match_end)
141
141
  diagnostics << Diagnostics.ambiguous_match(
@@ -159,8 +159,8 @@ module Varar
159
159
  steps_by_block[idx] = result.steps.map do |hit|
160
160
  PlannedStep.new(
161
161
  text: Offsets.utf16_slice(block.text, hit.match_start, hit.match_end),
162
- match_span: lift_span(var_doc.source, block, hit.match_start, hit.match_end),
163
- param_spans: hit.param_spans.map { |p| lift_span(var_doc.source, block, p.start, p.end) },
162
+ match_span: lift_span(doc.source, block, hit.match_start, hit.match_end),
163
+ param_spans: hit.param_spans.map { |p| lift_span(doc.source, block, p.start, p.end) },
164
164
  step_def: hit.step_def,
165
165
  args: hit.args,
166
166
  formats: hit.formats
@@ -169,7 +169,7 @@ module Varar
169
169
  end
170
170
 
171
171
  # Header-bound table detection.
172
- bound = had_ambiguous ? nil : detect_header_bound(ex, steps_by_block, var_doc.source)
172
+ bound = had_ambiguous ? nil : detect_header_bound(ex, steps_by_block, doc.source)
173
173
  if bound
174
174
  table, binding_step, header_spans = bound
175
175
  header_binding = HeaderBinding.new(
@@ -39,7 +39,7 @@ module Varar
39
39
  end
40
40
 
41
41
  # Seed Varar's own built-in parameter types (beyond cucumber-expressions'
42
- # int/float/string/word). Shared by every port so specs match
42
+ # int/float/string/word). Shared by every port so oaths match
43
43
  # identically. Built-ins are NOT tracked as custom parameter types, so
44
44
  # they never appear in the conformance registry.json projection.
45
45
  def seed_builtins(registry)
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Varar
4
+ module Core
5
+ # Run-result records — port of result.ts / result.rb's siblings in every
6
+ # other port. The persisted .varar/<oath_path>.json file is a serialized
7
+ # OathResults, read by the language server to place run diagnostics in the
8
+ # editor (ADR 0014).
9
+
10
+ # One mismatched CELL as a source-offset range plus the runtime value.
11
+ # `from`/`to` are absolute UTF-16 source offsets; `to` is exclusive.
12
+ CellFailure = Data.define(:from, :to, :actual)
13
+
14
+ # Where a failure points in the source: an offset range, `to` exclusive.
15
+ # The failing step's match span, or the first mismatched cell's span (the
16
+ # failure_anchor rule) — what lets a renderer underline the step that
17
+ # failed rather than the whole line it sits on.
18
+ AnchorRange = Data.define(:from, :to)
19
+
20
+ # The failure payload of a failed ExampleResult. `cells` and `anchor` are
21
+ # nil when they do not apply, and serialize as absent (not null), so a
22
+ # reader that predates them still parses the file. `stack` is deliberately
23
+ # runtime-shaped — no consumer parses it.
24
+ ExampleFailure = Data.define(:line, :message, :stack, :cells, :anchor) do
25
+ def initialize(line:, message:, stack:, cells: nil, anchor: nil)
26
+ super
27
+ end
28
+ end
29
+
30
+ # The run result for one BDD example. `lines` are the 1-based source lines
31
+ # of its steps (the editor's line-wash anchors).
32
+ ExampleResult = Data.define(:name, :status, :lines, :failure) do
33
+ def initialize(name:, status:, lines:, failure: nil)
34
+ super
35
+ end
36
+ end
37
+
38
+ # The persisted run result for one oath file. `oath_path` uses POSIX
39
+ # separators and is relative to the workspace root; `source_hash` is
40
+ # Hashing.hash_source over the oath as it was run, so a reader can tell
41
+ # whether the offsets still apply to the buffer in front of it.
42
+ OathResults = Data.define(:version, :oath_path, :source_hash, :examples)
43
+
44
+ # Projection of OathResults onto the JSON shape of .varar/<oath_path>.json.
45
+ #
46
+ # The wire format is the TypeScript one (ADR 0014): camelCase names,
47
+ # declaration order, and the optional members absent rather than null so a
48
+ # reader that predates them still parses the file. Pure — writing the file
49
+ # is the shell's job.
50
+ module Results
51
+ module_function
52
+
53
+ def to_wire(results)
54
+ {
55
+ 'version' => results.version,
56
+ 'oathPath' => results.oath_path,
57
+ 'sourceHash' => results.source_hash,
58
+ 'examples' => results.examples.map { |e| example_to_wire(e) }
59
+ }
60
+ end
61
+
62
+ def example_to_wire(example)
63
+ out = { 'name' => example.name, 'status' => example.status, 'lines' => example.lines.to_a }
64
+ out['failure'] = failure_to_wire(example.failure) if example.failure
65
+ out
66
+ end
67
+
68
+ def failure_to_wire(failure)
69
+ out = { 'line' => failure.line, 'message' => failure.message, 'stack' => failure.stack }
70
+ if failure.cells && !failure.cells.empty?
71
+ out['cells'] = failure.cells.map { |c| { 'from' => c.from, 'to' => c.to, 'actual' => c.actual } }
72
+ end
73
+ out['anchor'] = { 'from' => failure.anchor.from, 'to' => failure.anchor.to } if failure.anchor
74
+ out
75
+ end
76
+ end
77
+ end
78
+ end
@@ -66,7 +66,7 @@ module Varar
66
66
  end
67
67
  end
68
68
 
69
- VarDoc.new(
69
+ Doc.new(
70
70
  path: path,
71
71
  source: source,
72
72
  examples: examples,
data/lib/varar/core.rb CHANGED
@@ -4,7 +4,7 @@ module Varar
4
4
  # The pure functional core: parse, match, plan, execute, diffs, drift, and
5
5
  # the conformance projections. No filesystem, network, globals, or time.
6
6
  module Core
7
- VERSION = '0.7.0'
7
+ VERSION = '0.8.0'
8
8
  end
9
9
  end
10
10
 
@@ -24,6 +24,8 @@ require 'varar/core/plan'
24
24
  require 'varar/core/doc_string_diff'
25
25
  require 'varar/core/param_diff'
26
26
  require 'varar/core/failure_anchor'
27
+ require 'varar/core/result'
28
+ require 'varar/core/failure'
27
29
  require 'varar/core/execute'
28
30
  require 'varar/core/hash'
29
31
  require 'varar/core/drift'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: varar-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aslak Hellesøy
@@ -40,6 +40,7 @@ files:
40
40
  - lib/varar/core/doc_string_diff.rb
41
41
  - lib/varar/core/drift.rb
42
42
  - lib/varar/core/execute.rb
43
+ - lib/varar/core/failure.rb
43
44
  - lib/varar/core/failure_anchor.rb
44
45
  - lib/varar/core/hash.rb
45
46
  - lib/varar/core/matcher.rb
@@ -47,6 +48,7 @@ files:
47
48
  - lib/varar/core/parse.rb
48
49
  - lib/varar/core/plan.rb
49
50
  - lib/varar/core/registry.rb
51
+ - lib/varar/core/result.rb
50
52
  - lib/varar/core/scanner.rb
51
53
  - lib/varar/core/sentences.rb
52
54
  - lib/varar/core/span.rb