docs(dotnet): plan the .NET (C#) port - #42
Merged
Conversation
Full-pipeline port on the CLR (no runtime sharing), gated on all four conformance artifacts. C# owns the engine; F# is a later facade over it (Kotlin-over-Java route, registry-only conformance). Coordinates use the post-rename Varar scheme (NuGet Varar.*; varar.config.json; varar-examples). - ADR 0008 (.NET port) + ADR 0009 (VSTest ITestDiscoverer/ITestExecutor adapter) - core+facade and config+runner+adapter design specs - two TDD task plans Key forks (static-language precedent): injected Registrar, full-replacement immutable Value state, [CallerFilePath]/[CallerLineNumber] call-site capture. UTF-16 offsets expected free (C# strings are UTF-16); official Cucumber.CucumberExpressions 20.0.0 gives exact version parity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
…fsets Sub-project 1, task T0 of the .NET (C#) port plan. - dotnet/ workspace: Varar.sln + Varar.Core / Varar (facade) / Varar.Core.Tests skeleton projects; global.json, shared Directory.Build.props (nullable, warnings-as-errors), .gitignore. - References Cucumber.CucumberExpressions 20.0.0 (exact cross-port parity). - Confirms empirically that the library reports UTF-16 code-unit match offsets, so no code-point conversion layer is needed (CucumberOffsetTests). This resolves the plan's biggest open question early. Environment note: pinned to net8.0 (the SDK available here) rather than the plan's eventual net10.0; both are LTS and the dependency is netstandard2.0. Build + tests + `dotnet format --verify-no-changes` all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
Sub-project 1, task T1 — the immutable data + serialization foundation the pipeline is gated on. - Value: closed record hierarchy (null/bool/int/float/string/list/map) with structural equality mirroring deep-equal.ts (Int(2) != Float(2.0), lists order-sensitive, maps order-insensitive). Port of the Rust Value model. - CanonicalJson: hand-rolled serializer (UTF-16 key sort, 2-space indent, LF + trailing newline, raw non-ASCII, control chars \uXXXX, integral doubles as integers) — ported from canonical_json.rs rather than configured on System.Text.Json, for byte-exact control (as Java/Rust do). Gated by re-serializing 8 committed goldens (bundles 04/11/12/15) byte-for-byte. - Span: source range in UTF-16 code units + 1-based line/col; port of span.ts. 25 tests green; dotnet format clean. Removed the T0 placeholder smoke test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
Sub-project 1, task T2 (core half) — the step/parameter-type registry the
registry.json gate (T4) projects from.
- Registry / StepRegistration / addStep / defineParameterType: port of
registry.ts (immutable step list; duplicate-expression detection with both
source positions; custom types tracked for the {name, regexp} projection).
- ParameterTypeRegistry + VararParameterType: the .NET cucumber-expressions
package ships only IParameterType/IParameterTypeRegistry (no built-in
registry), so var supplies its own, pre-loading int/float/double/word/string/
anonymous from the package's own ParameterTypeConstants patterns for
cross-port byte-parity. Each type also carries a Value transform (wired now,
exercised at matching/T5).
- StepRole: StepKind + inferStepRole, port of step-role.ts.
The injected-Registrar author facade (DefineState/Stimulus/Sensor) is the next
increment. 35 tests green; dotnet format clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
Sub-project 1, task T2 (facade half) — the authoring surface the .steps.cs conformance fixtures (T4) are written against. - Steps builder in the Varar facade: static Register(Registry) -> Registry entry point (injected-Registrar model, ADR 0008; no module-scope accumulator), folding stimulus/sensor/param into the core registry. - Full-replacement state: a stimulus returns the whole next Value. Typed lambda overloads for 0/1/2 captures over Value (the C# analogue of Rust's IntoHandler); handlers fail by throwing. - Source file/line captured via [CallerFilePath]/[CallerLineNumber]; the file's stem is the cross-port stepFile. Per-file state factories thread through the core Registry (ContextFactories) — what the TS facade keeps in a module global. New Varar.Tests project. 41 tests green (6 facade + 35 core); dotnet format clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
…e green Sub-project 1, task T3 — the var-doc.json milestone. Ports the Markdown parse pipeline and gates it byte-for-byte against the committed goldens across all 15 conformance bundles. - Ast: the immutable block AST (heading/paragraph/list_item/blockquote/table/ fence/thematic_break), Row/SegmentOffset/Example/VarDoc, RawLine + the scanner-plugin port. Port of ast.ts. - Scanner: line-based block scanner (fences, tables, thematic breaks, blockquotes, headings, list items, paragraphs) with a JS-slice-compatible clamping helper. Port of scanner.ts. - Structurer, TableCells, Parse: examples-under-scopes, row cell spans, and the parse entry point. Ports of structurer.ts / table-cells.ts / parse.ts. - Conformance.ToVarDocArtifact: the var-doc.json projection to Value. UTF-16 offsets verified free via bundles 11-emoji-offsets / 12-combining-marks (part of the 15). 51 core tests green (15 bundles byte-for-byte); dotnet format clean. Kept as chore(dotnet) per the plan (feat scopes wait for the NuGet release target in sub-project 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
Sub-project 1, task T4 — the registry.json milestone.
- Authored conformance/bundles/*/**.steps.cs for all 15 bundles: injected-
Registrar Register(Registry) fixtures with the same expressions, kinds, custom
parameter types, and (best-effort) handlers as the .steps.rs siblings. Namespaced
Varar.Corpus.B## to avoid the Varar.Core.Conformance name.
- Conformance.ToRegistryArtifact: projects each step to {expression,
parameterTypeNames} (names read from the compiled expression's ParameterTypes,
which cover order/duplicates/anonymous) plus custom {name, regexp}.
- Varar.Tests harness: compiles the corpus fixtures, maps bundle -> Register/State,
and gates registry.json byte-for-byte across 15 bundles.
- HandlerException in the facade for the throw-to-fail model.
Two of four artifacts now green (var-doc + registry) x 15 bundles. 73 tests green;
dotnet format clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
…e green Sub-project 1, task T5 — the plan.json milestone. Ports matching and planning and gates plan.json byte-for-byte across all 15 bundles. - Matcher: findHits/resolveHits (port of matcher.ts). Scans each sentence with the cucumber regex (anchors stripped), then extracts per-parameter spans and values from cucumber's TreeRegexp Group tree (Group.Start/End are UTF-16, confirmed) — the .NET stand-in for the JS Argument.getValue()/group API the package omits. Ambiguity + overlap resolution ported 1:1. - Sentences: sentence splitter with backtick/quote/number/abbreviation guards. - Diagnostics: ambiguous-match / error-fence-without-step / drift. - Plan: per-example planning — segment-map span lifting, header-bound table rows, error-fence expected-failure, table/doc-string attachment, example naming. - Conformance.ToPlanArtifact: args as the raw source slice of each param span. Three of four artifacts green (var-doc + registry + plan) x 15 bundles. 88 tests green; dotnet format clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
Sub-project 1, task T6 — the trace.json milestone, completing the core: all four conformance artifacts (var-doc, registry, plan, trace) x 15 bundles now reproduce the committed goldens byte-for-byte. - Execute: the executor on the full-replacement state model (port of execute.rs / execute.ts). Per-example, per-stepfile state; stimulus replaces state wholesale; sensor return compared against its slots (inline params, then trailing table/doc string); header-bound row checks after the loop; error-fence expected-failure inversion. Handlers fail by throwing. - Diff engine: cell-diff (compareRow/compareTable + renderCellValue), param-diff (compareParams with format rendering), doc-string-diff (compareDocString), and the structured errors (CellMismatch/DocStringMismatch/ReturnShape/UnexpectedPass). - FailureAnchor + Conformance.ToTraceArtifact/toFailureArtifact: the inline trace projection (stepFile stem, failure kind + anchor + cells/diff). Handlers authored in the T4 fixtures now execute; the return-based comparison contract (bare returns, positional arrays, header-bound rows, doc strings) is exercised against the goldens. 103 tests green; dotnet format clean. Drift (T7, unit-gated) is the remaining tail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
Sub-project 1, task T7 — the drift feature (unit-gated, no golden), completing the pure core. - Hash: FNV-1a (32-bit) over UTF-16 code units → fnv1a:<8 hex>. Port of hash.ts. - IBaselineStore: the varar.lock.json port interface (filesystem impl is the runner's, sub-project 2). - Drift: Jaccard word-similarity re-identification (threshold 0.5), live-example derivation, detectDrift, reconcileDrift, and the varar.lock.json parser + its own insertion-order serializer (JSON.stringify(_,null,2)+"\n", paths sorted) — not the recursive-sort canonical JSON. Port of drift.ts. Proven by translating hash.test.ts / drift.test.ts (27 tests), incl. the FNV-1a known vectors. 130 tests green total; dotnet format clean. Sub-project 1 done: all four conformance artifacts x 15 bundles byte-for-byte + drift unit-gated. Remaining (sub-project 2): var-config reader, runner, VSTest adapter, tree-sitter dialect, and repo/release integration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
Sub-project 2, task P1 — the varar.config.json reader.
- Varar.Config: strict, fail-loud parser of the canonical
{ docs:{include,exclude}, steps, snippets, scannerPlugins } shape (port of
config.ts): no docs/steps defaults, unknown keys / wrong types / non-string
arrays are errors (message prefixed with the config path), missing file →
empty. ParsedVarConfig + VarGlobs types; ToArtifact projection.
- Reproduces conformance/config/cases/* (8) byte-for-byte: golden.json via the
core's canonical JSON, or an expect-error.txt case must throw. no-config-file
uses the empty default.
87 core tests green (8 config cases + error cases); dotnet format clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
Sub-project 2, task P2 — the imperative shell. - Discovery: the shared glob→regex semantics (** / * / ? / /**/ ), a recursive file walk, and include/exclude filtering — ported to match every other runner. - FileBaselineStore: the filesystem IBaselineStore (varar.lock.json read/write). - Runner: PlanSpec (parse+plan), ExampleNames (innermost-heading dedup with [n]), RunExample (delegates to the core executor), LoadSteps (reflects and chains every static Register(Registry) in an assembly — the injected-Registrar discovery), and RenderFailure (reuses the core diff payloads, .md-anchored). Runner-level drift reconciliation is exercised through the real FileBaselineStore. 103 core tests green; dotnet format clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
Sub-project 2, task P3 — the dotnet test binding (ADR 0009).
- Varar.TestAdapter: a custom VSTest adapter. VararTestDiscoverer
([FileExtension(".dll")], [DefaultExecutorUri("executor://varar")]) loads the
built test assembly, walks up to varar.config.json, globs the .md specs, plans
each via the runner, and emits one TestCase per example — CodeFilePath/LineNumber
point at the .md source line. VararTestExecutor runs each example through the
core (state from the registry's DefineState factories) and reports pass/fail with
the runner's .md-anchored failure render. Ships as Varar.TestAdapter.dll so
dotnet test auto-loads it on package reference.
- samples/Varar.Sample: a minimal consumer (varar.config.json + counter.md +
counter.steps.cs) proving auto-load — `dotnet test` discovers and runs
"I increment. The count is 1" green with no user wiring.
Full solution green (155 tests); dotnet format clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
Sub-project 2, task P4 — the per-language authoring surface. - tree-sitter-dialects/csharp.ts: step-def query (s.Stimulus/s.Sensor invocations on a member access), custom-parameter-type query (s.Param with a plain or verbatim regexp), C# string decoding (escape sequences + @"" verbatim), and lambda handler-param extraction. Queries verified empirically against tree-sitter-c-sharp 0.23.5 and the 15 .steps.cs fixtures. - Wired into tree-sitter-scanner (SPECS/EXTENSIONS + LanguageId 'csharp') and the grammar loaders (test loader + LSP node loader) and the VS Code bundler copy list; grammar dep added to language/lsp + knip ignores. The vitest/website playground loaders stay TypeScript-only by design. - The shared scanner now lower-cases @function-name for the StepKind, so C#'s idiomatic PascalCase Stimulus/Sensor map to stimulus/sensor (a no-op for every existing port, whose method names are already lower-case). Proven by tree-sitter-scanner-csharp.test.ts (extracts the right kinds + verbatim/plain custom types). Not yet added to languages.json — that lands in P5 with the docs/editor tabs the cross-language gates require. 95 language-package tests green; biome + tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
Sub-project 2, task P5 (build axis). - Makefile: a `dotnet` target (dotnet format --verify-no-changes + build + test of the solution, then the VSTest adapter smoke sample), threaded into `check` and documented in the header — mirroring each other port's gate. - .github/workflows/dotnet.yml: sets up the .NET 8 SDK and runs the same gate on dotnet/** and conformance/** changes. `make dotnet` green locally (155 solution tests + the adapter sample). The website/editor axis (languages.json entry, docs code tabs, front-page editor tabs, CM_LANGUAGE), the examples/csharp-* consumer, and the NuGet release target remain — they land together since adding csharp to languages.json gates the website build until its tabs/examples exist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
Sub-project 2, task P5 (examples axis).
- examples/csharp-vstest: a standalone consumer that runs the six shared Markdown
specs (hello-var, deep-thought, roman-numerals, tables-and-docstrings, yahtzee,
library) as `dotnet test` tests via the VSTest adapter — 30 examples, all green.
The .md specs are symlinks to the typescript-vitest originals; the SUT
(RomanNumerals, Yahtzee, Library with dates/money) lives in src/, the injected-
Registrar step files in steps/*.steps.cs (full-replacement Value state, custom
{date}/{money}/{title} parameter types). Project references the local build; a
real project uses the published NuGet packages.
- Fix {string} transform: the .NET cucumber build captures {string} WITH its
surrounding quotes (inner groups compiled non-capturing), so strip them before
unescaping. The conformance corpus never compared a transformed {string} value
(every {string} sensor returns nothing), so this gap only surfaced in the
hello-var example. Conformance stays green.
- make dotnet + dotnet.yml now run the consumer (replacing the throwaway smoke
sample); examples/README.md row added.
Solution 155 tests + example 30 examples green; dotnet format clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
Add C# to languages.json (the shared source of truth), the SiteLang union, and the CodeMirror highlighter map, then wire the C# code tabs everywhere the language-coverage drift gate requires: every `<Tabs syncKey="lang">` group in the stimuli/sensors/custom-parameters/tables-and-doc-strings/get-started docs, and a C# `<File>` in the three front-page editors (Deep Thought, Library, Roman Numerals), sourced from examples/csharp-vstest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
… (P5) Wire the .NET port into the release pipeline the same way Rust is wired: a parked NuGet publish target (68-nuget.sh, gated by DOTNET_NUGET_ENABLED) with a go-live checklist, kept in lock-step with the varar-examples sync — while parked the csharp-* samples are excluded from the sync (their project references to dotnet/ can't resolve until the packages are on NuGet) and pinned to the release version once live. Add the C# port to the README build/coverage table (build badge live, coverage n/a until coverlet is wired) and coverage.json. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
Update the adding-a-language-port skill overview: seven ports now reach full behavioural parity (Rust and C# added), and C#/Rust are the precedents for a full-pipeline port in a static language with an explicit Value model, an injected registrar, and full-replacement state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
…src/ move The library sample's domain import moved from './library' to '../src/library' when the system-under-test was split into a production source set, but the ts-diagnostics test still filtered unresolved-module noise for './library' only, so the real import's noise leaked through and failed the type-check assertion. Match the domain module by its '/library'' suffix instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
…nd Roman editors The interactive editors loaded the TypeScript steps and domain files at flat paths, so the browser runner could not resolve the sample's `../src/library` import and showed a red "Cannot import" banner. Give the files their real `steps/…` + `src/…` layout so the relative import resolves exactly as it does on disk; the in-browser language service now resolves it too. Tab labels are unchanged (they render the basename). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XgoYfpakx2RwYwu6ztLPQc
…m, and keep step closures on the call line
Step files now expose `register(&mut Steps)` instead of the
`register(Registry) -> Registry` threading, dropping the
`Steps::from_registry(r)` / `s.into_registry()` bookends from every fixture and
harness — matching the Java/Kotlin injected-registrar model. `param_with_format`
is merged into a single `param(name, regexp, parse, Option<FormatFn>)`, the
one-`param` shape every other port already uses. A repo-root rustfmt.toml sets
`fn_call_width = 100` so `s.stimulus("expr", |state, arg| { … })` keeps the
expression and closure together on the call line instead of exploding one
argument per line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XgoYfpakx2RwYwu6ztLPQc
…PI surface minimal Update the reference and tutorial C#/Rust snippets to the injected-builder `Register`/`register` shape and the unified `param`, refresh the .NET port's ADR / plans / specs, and add a "keep the public API surface minimal" principle to the adding-a-language-port skill: expose only the author builder methods, keep registry construction internal / crate-private, and prefer one method with an optional argument over near-duplicate variants (the `param` lesson). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XgoYfpakx2RwYwu6ztLPQc
…t v3 Retarget from net8.0 to net10.0 (the current LTS) via Directory.Build.props and global.json, pin the SDK to 10.0.302 in dotnet/.tool-versions, bump Microsoft.NET.Test.Sdk and Microsoft.TestPlatform.ObjectModel to 18.8.1, and migrate the test projects to xUnit v3 (xunit.v3.mtp-off 3.2.2 + xunit.runner.visualstudio 3.1.5, OutputType=Exe) still on the VSTest runner. Update the standalone example consumer, the CI workflow, the Makefile, and the port README to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XgoYfpakx2RwYwu6ztLPQc
…nto Register, and shrink the public API Author step files now expose `void Register(Steps s)` — the framework injects the builder — instead of threading a Registry through `Steps.From(r)` / `return s.ToRegistry()`, matching the Java/Kotlin/Rust injected-registrar model. With that plumbing no longer author-facing, `Steps.From`/`ToRegistry`/ctor and the Registry construction API (`Create`/`AddStep`/`DefineParameterType`/ `WithContextFactory`, `StepInput`/`ParameterTypeInput`) become `internal`, exposed to the runner and tests via InternalsVisibleTo, so adapters receive a Registry as an opaque token rather than building one by hand. Also a behaviour-preserving idiomatic sweep across the port: collection expressions, primary constructors, range / index-from-end operators, `[GeneratedRegex]`, `char.IsAscii*`/`IsNullOrWhiteSpace`, and redundant-using removal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XgoYfpakx2RwYwu6ztLPQc
…hecks The per-language tree-sitter dialects are parallel by design — each new port (C# most recently) adds a dialect whose param-extraction/query shape mirrors a sibling's, which jscpd flags as a clone. Ruby and Python wrapped that shared shape in inline `jscpd:ignore` blocks; generalise the exemption to the whole `packages/language/src/tree-sitter-dialects/` folder so csharp.ts (vs java.ts) and every future dialect are covered by one rule, and drop the now-redundant inline markers (keeping their rationale as a plain comment). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XgoYfpakx2RwYwu6ztLPQc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Planning docs for a .NET (C#) language port, produced with the
adding-a-language-portskill. Planning only — no port code. Stacked onrename-to-varar(#39), so the diff is just these six docs and all coordinates use the post-rename Varar naming.The mechanical decisions
var-doc/registry/plan/trace) × 15 bundles + the config corpus byte-for-byte; drift is unit-gated..steps.fsfixtures.Valuestate (no runtimedeep_freeze), and[CallerFilePath]/[CallerLineNumber]call-site capture for step source location.ITestDiscoverer/ITestExecutor, auto-loaded via*.TestAdapter.dll) — the structural analog of Java's custom JUnitTestEngine, framework-neutral (works withdotnet testregardless of xUnit/NUnit/MSTest) and discovery-time, giving one selectable test per.mdexample. (ADR 0009, with the data-driven-[Theory]alternative rejected.)Two de-risking findings
string/charandRegex.Indexare UTF-16 code-unit indexed like JS/JVM, so the Python port's conversion layer shouldn't recur — still gated on bundles11-emoji-offsets/12-combining-marksrather than assumed.Cucumber.CucumberExpressions20.0.0 is on NuGet (Cucumber org, June 2026) — exact version parity, no hand-ported grammar or{float}gap.Package shape (post-rename NuGet idiom)
Varar.Core·Varar(facade) ·Varar.Config·Varar.Runner·Varar.TestAdapterFiles
doc/adr/0008-dotnet-port.md— .NET port (full pipeline; C# owns engine; F# later facade)doc/adr/0009-dotnet-test-adapter-integration.md— VSTest adapter integrationdoc/superpowers/specs/2026-07-19-dotnet-core-port-design.md— core + facadedoc/superpowers/specs/2026-07-19-dotnet-runner-adapter-design.md— config + runner + adapterdoc/superpowers/plans/2026-07-19-dotnet-core-port.md— core TDD plan (staged by the 4 artifacts)doc/superpowers/plans/2026-07-19-dotnet-runner-adapters.md— runner/adapter + repo/release integration🤖 Generated with Claude Code
https://claude.ai/code/session_01TZsEqbSdXCtqvKcs5cuMMj
Generated by Claude Code