diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..d07854a
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,79 @@
+# Automated dependency updates for every ecosystem in this repository.
+#
+# Issue #47 found each language several majors behind: eslint 9 while 10 was
+# out, pytest 7 while 9 was, xunit 2 while 3 was, and four different
+# `links-notation` pins. Nothing was watching, so the drift was only noticed
+# when someone went looking. Dependabot opens the pull request instead.
+#
+# `github-actions` is included because a pinned action drifting is the same
+# problem one layer down, and it is the one no language pipeline covers.
+version: 2
+
+updates:
+ - package-ecosystem: cargo
+ directory: /rust
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 5
+ commit-message:
+ prefix: 'chore(rust)'
+ groups:
+ # One pull request for the whole routine batch: reviewing a dozen patch
+ # bumps separately is how they end up ignored. Majors stay separate,
+ # because each needs its own reading.
+ rust-minor-and-patch:
+ update-types:
+ - minor
+ - patch
+
+ - package-ecosystem: npm
+ directory: /js
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 5
+ commit-message:
+ prefix: 'chore(js)'
+ groups:
+ js-minor-and-patch:
+ update-types:
+ - minor
+ - patch
+
+ - package-ecosystem: pip
+ directory: /python
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 5
+ commit-message:
+ prefix: 'chore(python)'
+ groups:
+ python-minor-and-patch:
+ update-types:
+ - minor
+ - patch
+
+ - package-ecosystem: nuget
+ directory: /csharp
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 5
+ commit-message:
+ prefix: 'chore(csharp)'
+ groups:
+ csharp-minor-and-patch:
+ update-types:
+ - minor
+ - patch
+
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 5
+ commit-message:
+ prefix: 'chore(ci)'
+ groups:
+ actions-minor-and-patch:
+ update-types:
+ - minor
+ - patch
diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml
index 5fe5381..f56b782 100644
--- a/.github/workflows/csharp.yml
+++ b/.github/workflows/csharp.yml
@@ -124,7 +124,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '10.0.x'
# Node powers the shared CI helper scripts under csharp/scripts.
- name: Setup Node.js
@@ -206,7 +206,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '10.0.x'
- name: Restore dependencies
working-directory: ./csharp
@@ -218,7 +218,7 @@ jobs:
- name: Run tests
working-directory: ./csharp
- run: dotnet test --configuration Release --no-build --verbosity normal --collect:"XPlat Code Coverage"
+ run: dotnet test --configuration Release --no-build --coverage --coverage-output-format cobertura
- name: Run example
working-directory: ./csharp/examples
@@ -269,7 +269,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '10.0.x'
- name: Restore dependencies
working-directory: ./csharp
@@ -355,7 +355,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '10.0.x'
- name: Setup Node.js
uses: actions/setup-node@v7
@@ -498,7 +498,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '10.0.x'
- name: Setup Node.js
uses: actions/setup-node@v7
diff --git a/.github/workflows/parity.yml b/.github/workflows/parity.yml
index 43ece0d..fb5e4ac 100644
--- a/.github/workflows/parity.yml
+++ b/.github/workflows/parity.yml
@@ -47,3 +47,25 @@ jobs:
contains(github.event.pull_request.body, '[skip-parity]'))
&& 'opted out with [skip-parity] in the pull request' || '' }}
run: node scripts/check-language-parity.mjs
+
+ # Separate job on purpose: `[skip-parity]` opts out of the source-parity gate
+ # above, which a legitimate single-language change sometimes needs. It must
+ # not also switch off the version check -- four implementations parsing with
+ # four different grammars is never intentional (issue #47).
+ links-notation-version:
+ name: Implementations Pin One Parser Version
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v7
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v7
+ with:
+ node-version: '22'
+
+ - name: Check version-gate helper's own tests
+ run: node --test scripts/check-links-notation-parity.test.mjs
+
+ - name: Enforce one links-notation version across implementations
+ run: node scripts/check-links-notation-parity.mjs
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
index f173bd4..a2f3e25 100644
--- a/.github/workflows/security.yml
+++ b/.github/workflows/security.yml
@@ -55,7 +55,7 @@ jobs:
if: matrix.language == 'csharp'
uses: actions/setup-dotnet@v6
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '10.0.x'
- name: Autobuild
uses: github/codeql-action/autobuild@v4
@@ -82,7 +82,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '10.0.x'
# `dotnet list package --vulnerable` exits 0 even when it finds
# advisories, so the report is inspected rather than the exit code.
diff --git a/.gitignore b/.gitignore
index 45f5a72..6115b65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -175,3 +175,6 @@ dmypy.json
# Cargo build output of the scratch crates under experiments/
experiments/**/target/
+
+# jscpd duplication reports
+reports/
diff --git a/.gitkeep b/.gitkeep
index c1a5f09..e4cda94 100644
--- a/.gitkeep
+++ b/.gitkeep
@@ -3,4 +3,5 @@
# Updated: 2026-08-20T06:10:07.182Z
# Updated: 2026-08-20T07:45:09.136Z
# Updated: 2026-08-27T11:41:42.189Z
-# Updated: 2026-08-27T14:37:49.393Z
\ No newline at end of file
+# Updated: 2026-08-27T14:37:49.393Z
+# Updated: 2026-08-28T13:39:51.268Z
\ No newline at end of file
diff --git a/README.md b/README.md
index a149960..70e2cae 100644
--- a/README.md
+++ b/README.md
@@ -25,8 +25,8 @@ This library provides universal serialization and deserialization with built-in
- **[Python](python/)** - Full implementation for Python 3.8+
- **[JavaScript](js/)** - Full implementation for Node.js 18+
-- **[Rust](rust/)** - Full implementation for Rust 1.70+
-- **[C#](csharp/)** - Full implementation for .NET 8.0+
+- **[Rust](rust/)** - Full implementation for Rust 1.85+
+- **[C#](csharp/)** - Full implementation for .NET 10.0+
All implementations share the same design philosophy and provide feature parity.
diff --git a/csharp/.changeset/20260828_120000_issue_47_links_notation_0_16_1.md b/csharp/.changeset/20260828_120000_issue_47_links_notation_0_16_1.md
new file mode 100644
index 0000000..9e6bdc6
--- /dev/null
+++ b/csharp/.changeset/20260828_120000_issue_47_links_notation_0_16_1.md
@@ -0,0 +1,5 @@
+---
+'Lino.Objects.Codec': minor
+---
+
+Update `Link.Foundation.Links.Notation` from 0.13.0 to 0.16.1 and retarget the library, tests and example from `net8.0` to `net10.0`, which that release requires. All four implementations now pin `links-notation` 0.16.1, so the conformance suites run against one grammar instead of four (issue #47).
diff --git a/csharp/.changeset/20260828_140000_issue_47_xunit_v3.md b/csharp/.changeset/20260828_140000_issue_47_xunit_v3.md
new file mode 100644
index 0000000..7c71a9b
--- /dev/null
+++ b/csharp/.changeset/20260828_140000_issue_47_xunit_v3.md
@@ -0,0 +1,11 @@
+---
+'Lino.Objects.Codec': patch
+---
+
+Update the test tooling to current majors (issue #47).
+
+- `xunit` 2.6.6 -> `xunit.v3` 4.0.0, `xunit.runner.visualstudio` 2.5.6 -> 4.0.0 and `Microsoft.NET.Test.Sdk` 17.8.0 -> 18.9.0.
+- xunit v3 runs on Microsoft.Testing.Platform, and the .NET 10 SDK no longer supports the VSTest target that `coverlet.collector` plugs into. `global.json` opts `dotnet test` into the new platform, and coverage now comes from `Microsoft.Testing.Extensions.CodeCoverage` via `dotnet test --coverage --coverage-output-format cobertura`.
+- Dropped the `!TestResults/` line from `.gitignore`: it un-ignored the directory that `[Tt]est[Rr]esult*/` two lines above deliberately ignores, so a local coverage run left the tree dirty.
+
+All 507 tests pass unchanged; no test source needed editing.
diff --git a/csharp/.gitignore b/csharp/.gitignore
index 8b786e9..e0bd004 100644
--- a/csharp/.gitignore
+++ b/csharp/.gitignore
@@ -39,7 +39,6 @@ packages/
*.dll
*.exe
*.pdb
-!TestResults/
# User-specific files
*.rsuser
diff --git a/csharp/examples/BasicUsage.csproj b/csharp/examples/BasicUsage.csproj
index 767cd65..7613711 100644
--- a/csharp/examples/BasicUsage.csproj
+++ b/csharp/examples/BasicUsage.csproj
@@ -2,7 +2,7 @@
Exe
- net8.0
+ net10.0
enable
enable
diff --git a/csharp/global.json b/csharp/global.json
new file mode 100644
index 0000000..3140116
--- /dev/null
+++ b/csharp/global.json
@@ -0,0 +1,5 @@
+{
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ }
+}
diff --git a/csharp/src/Lino.Objects.Codec/Lino.Objects.Codec.csproj b/csharp/src/Lino.Objects.Codec/Lino.Objects.Codec.csproj
index 89ad37c..ebdec31 100644
--- a/csharp/src/Lino.Objects.Codec/Lino.Objects.Codec.csproj
+++ b/csharp/src/Lino.Objects.Codec/Lino.Objects.Codec.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net10.0
enable
enable
Lino.Objects.Codec
@@ -22,7 +22,7 @@
-
+
diff --git a/csharp/tests/Lino.Objects.Codec.Tests/Lino.Objects.Codec.Tests.csproj b/csharp/tests/Lino.Objects.Codec.Tests/Lino.Objects.Codec.Tests.csproj
index 4b853ee..ed1f9d8 100644
--- a/csharp/tests/Lino.Objects.Codec.Tests/Lino.Objects.Codec.Tests.csproj
+++ b/csharp/tests/Lino.Objects.Codec.Tests/Lino.Objects.Codec.Tests.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net10.0
enable
enable
false
@@ -9,16 +9,17 @@
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
+
+
diff --git a/experiments/issue-47/.gitignore b/experiments/issue-47/.gitignore
new file mode 100644
index 0000000..a53b921
--- /dev/null
+++ b/experiments/issue-47/.gitignore
@@ -0,0 +1,6 @@
+# Throwaway build and run artefacts of the interop drivers.
+out/
+rust-interop/target/
+rust-interop/Cargo.lock
+csharp-interop/bin/
+csharp-interop/obj/
diff --git a/experiments/issue-47/README.md b/experiments/issue-47/README.md
new file mode 100644
index 0000000..4079a6a
--- /dev/null
+++ b/experiments/issue-47/README.md
@@ -0,0 +1,25 @@
+# Cross-implementation round trip at `links-notation` 0.16.1
+
+Issue #47 asks for a check that "a document written by each implementation reads
+back identically in the other three" once all four pin the same parser version.
+
+Each program here does two things:
+
+- `write ` -- encode one fixed record with `encodeLine` and write it out.
+- `read ` -- decode every `*.lino` in the directory and print
+ `encodeLine(decoded)` for each.
+
+Re-encoding is what makes the comparison language-agnostic: there is no shared
+value type across Rust, JavaScript, Python and C#, but there is a shared text
+format, so if every implementation reads every document to the same value, all
+sixteen re-encodings are the same string.
+
+Run it with:
+
+```bash
+./experiments/issue-47/run.sh
+```
+
+The Rust and C# drivers are separate throwaway projects that path-reference the
+real packages, so nothing here changes what the crate, the npm package or the
+NuGet package ship.
diff --git a/experiments/issue-47/csharp-interop/Interop.csproj b/experiments/issue-47/csharp-interop/Interop.csproj
new file mode 100644
index 0000000..8670b63
--- /dev/null
+++ b/experiments/issue-47/csharp-interop/Interop.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+ Issue47.Interop
+
+
+
+
+
+
+
diff --git a/experiments/issue-47/csharp-interop/Program.cs b/experiments/issue-47/csharp-interop/Program.cs
new file mode 100644
index 0000000..5270b64
--- /dev/null
+++ b/experiments/issue-47/csharp-interop/Program.cs
@@ -0,0 +1,39 @@
+// C# side of the cross-implementation round trip (issue #47).
+
+using Lino.Objects.Codec;
+
+// The record every implementation writes.
+static Dictionary Record() => new()
+{
+ ["phase"] = "stream_end",
+ ["bytes"] = 2827L,
+ ["complete"] = true,
+ ["server"] = new Dictionary
+ {
+ ["host"] = "127.0.0.1",
+ ["port"] = 18878L,
+ },
+ ["models"] = new List