deps: update every dependency in all four languages, pin one links-notation, and gate against drift - #48
Conversation
Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: #47
The four codecs pinned four different parser versions -- Rust 0.14.0, JS ^0.11.0, Python >=0.11.0,<0.12.0, C# 0.13.0 -- so the shared fixtures proved the codecs agreed with each other while saying nothing about the grammar underneath them. All four now pin 0.16.1, the newest release published to every registry. Link.Foundation.Links.Notation 0.16.1 ships only a net10.0 assembly, so the C# library, tests and example retarget from net8.0 to net10.0 and the workflows that build them ask for the 10.0.x SDK. This is the TargetFramework bump the issue proposes, arriving as a hard requirement rather than a choice. python/tests/test_single_line_format.py asserted how the 0.11 parser mangled the hand-rolled dialect (parentheses swallowed into ids). 0.16 reads it as anonymous links instead, so the test now asserts the outcome that is stable across parser releases -- the record does not come back -- which is what the C# test already checked.
…on 2024 rust-version = "1.70" was already unreachable -- base64 0.23 and serde_json 1.0.151 both declare rust-version 1.71 -- so the floor had to move. Rather than track whatever a transitive dependency happens to demand, the crate moves to edition 2024 and declares that edition's own minimum, 1.85. Two mechanical consequences: clippy now knows std::iter::repeat_n is available and flags the repeat().take() run builder in readable.rs, and rustfmt's 2024 style edition sorts use lists in version order, which reorders a handful of imports.
eslint.config.js has always imported '@eslint/js', but relied on it arriving as a transitive dependency of eslint. ESLint 10 no longer brings it, so the config fails to load with ERR_MODULE_NOT_FOUND until it is declared. Its version line is decoupled from eslint's: @eslint/js 10.0.1 pairs with eslint 10.9.1. The flat config needed no other change; eslint . is clean.
The package.json lint-staged block needs no change: verified by staging a file and running npx lint-staged --verbose, which runs eslint --fix, prettier --write and prettier --check as before.
.jscpd.json set "format": "console", intending a reporter. In both jscpd 4 and 5 `format` is the list of *file* formats to scan, so the check has been scanning zero files for its whole life -- `npx jscpd .` on the old config prints nothing but a detection time, and jscpd 5 says "Files analyzed: 0" outright. jscpd 5 also rejects `skipComments`, which is now "mode": "weak". With `format` removed the check scans 43 files and finds 48 clones, 3.88% of lines. Refactoring those is a separate piece of work and would touch js/src, so `threshold` becomes 4 -- a ratchet just above today's level that stops duplication from growing -- instead of the 0 that was never enforced. Lowering it back to 0 needs the refactor, not a dependency bump.
@changesets/cli 2 -> 3 (needs Node ^22.11 || ^24 || >=26, which CI runs), prettier 3.6.2 -> 3.9.6 and eslint-plugin-prettier 5.5.4 -> 5.5.6. Prettier 3.9 no longer keeps the redundant parentheses around the negated condition in src/format.js, so the file is reformatted; the four per-bump changesets are consolidated into one, as the JS CI requires exactly one.
pytest 7 -> 9, pytest-cov 4 -> 7, mypy 1 -> 2, ruff 0.1 -> 0.16 and scriv 1.7 -> 1.8, plus setuptools 77 -> 84 for the build. The floors were low enough that a fresh dev install could resolve to tooling several majors behind what CI runs. Verified: 488 tests pass, and `ruff check`, `ruff format --check` and `mypy src` are clean over src/tests/scripts.
xunit 2.6.6 -> xunit.v3 4.0.0, xunit.runner.visualstudio 2.5.6 -> 4.0.0, Microsoft.NET.Test.Sdk 17.8.0 -> 18.9.0. xunit v3 runs on Microsoft.Testing.Platform. The .NET 10 SDK no longer supports the VSTest target that coverlet.collector plugged into, so global.json opts `dotnet test` into the new platform and coverage comes from Microsoft.Testing.Extensions.CodeCoverage instead; CI collects it with `--coverage --coverage-output-format cobertura`, which codecov reads the same way it read the coverlet output. No test source changed: all 507 tests pass, `dotnet format` is clean and the Release build is warning-free under /warnaserror.
scripts/check-links-notation-parity.mjs reads the declared links-notation requirement out of all four manifests and fails when they do not pin the same minor. It compares major.minor only: the ranges are open at the patch level on purpose, and the resolved patch legitimately differs between ecosystems. It runs as its own job in the parity workflow rather than inside the existing one, because `[skip-parity]` must not switch it off -- a single-language source change is sometimes intentional, four different grammars behind one conformance suite never is. The helper has its own tests, including the four pins this repository actually shipped before this PR (0.12, ^0.11.0, >=0.11.0,<0.12.0, 0.13.0) as a regression case, and a check that each extractor still finds the version in the real manifest so a reformat cannot silently turn the gate into a no-op. .github/dependabot.yml covers cargo, npm, pip, nuget and github-actions weekly, grouping minor and patch updates per ecosystem and leaving majors as separate pull requests.
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. experiments/issue-47 writes one record from each language and has each language decode all four documents, re-encoding what it read. Re-encoding is what makes the comparison language-agnostic: the four have no shared value type, but they do share a text format, so agreement means all sixteen re-encodings are one string. They are, at links-notation 0.16.1. The Rust and C# drivers are throwaway projects that path-reference the real packages, so the shipped crate and NuGet package gain nothing.
engines.node was >=18.0.0. Node 18 went end-of-life a year ago and this package's CI has only ever run Node 22, so the floor claimed support that nothing tested. 22.11 is also the minimum @changesets/cli 3 and eslint 10 accept.
Working session summaryDone. PR #48 is out of draft with all checks green: #48 What landed (11 commits, each major bump bisectable):
Two things I found broken while in there:
Forced upgrades, not optional ones: Two things worth your call:
One cosmetic wart on the checks tab: the first parity run started before This summary was automatically extracted from the AI working session output. |
🤖 Solution Draft LogThis log file contains the complete execution trace of the AI solution draft process. 💰 Cost: $12.037702📊 Context and tokens usage:Claude Opus 5: (2 sub-sessions)
Total: (2.6K new + 196.8K cache writes + 15.5M cache reads) input tokens, 92.9K output tokens, $12.037702 cost 🤖 Models used:
📎 Log file uploaded as Gist (4463KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
🔄 Auto-restart 1/5Reason: CI failures detected Starting new session to address the issues. Auto-restart-until-mergeable mode is active. This run will stop after 5 restart iterations in total. |
Working session summaryNo work remains — the task is already complete. State verified:
The merged commits cover the issue's five asks: per-language dependency bumps split into bisectable commits ( One thing worth flagging: post-merge on main, Dependabot immediately opened update runs across all four ecosystems ( This summary was automatically extracted from the AI working session output. |
🔄 Auto-restart-until-mergeable Log 1/5This log file contains the complete execution trace of the AI solution draft process. 💰 Cost: $0.329555📊 Context and tokens usage:
Total: (14 new + 18.3K cache writes + 185.7K cache reads) input tokens, 2.2K output tokens, $0.329555 cost 🤖 Models used:
📎 Log file uploaded as Gist (4881KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
Closes #47.
[skip-parity]— see the note at the end for why.Every dependency in all four implementations is now current, the four
links-notationpins are one version, and two gates exist so neither can driftagain unnoticed.
One parser behind four implementations
links-notationwas pinned four different ways: Rust0.12, JavaScript^0.11.0, Python>=0.11.0,<0.12.0, C#0.13.0. The conformance suites assertthat a document one implementation writes reads back identically in the other
three, which only means anything while all four parse with the same grammar.
All four now pin 0.16.1, moved in a single commit (
64e07be).Deliberate major bumps, one commit each
eslint@eslint/jsadded explicitly — ESLint 10 no longer supplies it transitively, andeslint.config.jshas always imported itlint-stagedjscpd.jscpd.jsonrepaired, see below@changesets/clipytestpytest-covmypyxunitxunit.v34.0.0Also current:
base640.22 → 0.23,serde_json→ 1.0.151,ruff→ 0.16.5,scriv→ 1.8,setuptools→ 84,prettier→ 3.9.6,eslint-plugin-prettier→ 5.5.6,Microsoft.NET.Test.Sdk→ 18.9.0,xunit.runner.visualstudio→ 4.0.0.Each major is its own commit, so a regression is bisectable.
Two things that turned out to be broken
The duplication check had never analysed a file.
.jscpd.jsonset"format": "console". In both jscpd 4 and 5formatis the list of fileformats to scan, not the reporter — there is no source language called
console, so the scan matched nothing and reported success on an empty set. Iverified this against jscpd 4.0.5 on the old config before changing anything:
same empty result, so this is not a v5 regression. With
formatremoved andskipCommentsrenamed to v5's"mode": "weak", the scan is real and finds 48clones / 3.88% duplicated lines.
thresholdis set to4as a ratchet ratherthan
0, because getting to zero means refactoringjs/src, which does notbelong in a dependency PR.
csharp/.gitignoreun-ignored its own coverage output. A!TestResults/line negated the
[Tt]est[Rr]esult*/rule ten lines above it, so a localcoverage run left the tree dirty. Removed.
Floors raised to what CI actually runs
pytest>=9.1,pytest-cov>=7.1,mypy>=2.3,ruff>=0.16.5,scriv>=1.8, buildsetuptools>=84.0. The old floors(
pytest>=7.0,mypy>=1.0,ruff>=0.1.0) let a freshpip install -e ".[dev]"resolve to tooling several majors behind CI.
links-notationis>=0.16.1,<0.17.0— open at the patch level, so abug-fix release is picked up without a commit.
jsengines.node:>=18.0.0→>=22.11.0. Node 18 has been end-of-lifefor a year and this package's CI has only ever run 22, so the old floor
claimed support nothing tested. This is a judgement call that narrows the
supported platform — say the word and I will drop it back to
>=20.19.0(the eslint 10 minimum) or leave it alone.
Rust edition and .NET target framework
The issue flagged both as worth revisiting; both turned out to be forced.
rust-version = "1.70"was already unreachable:base640.23 andserde_json1.0.151 each declare 1.71. Nowedition = "2024",rust-version = "1.85". Clippy, MSRV-aware once 1.85 is declared, thenflagged
repeat().take()inrust/src/readable.rs; it isrepeat_nnow.rustfmt's 2024 style edition re-sorted imports across 11 files.
Link.Foundation.Links.Notation0.16.1 ships onlylib/net10.0(0.15.0 wasthe last with
lib/net8.0— I unpacked the nupkgs to confirm). Staying onnet8.0 meant staying on 0.13.0 and keeping the version divergence, so all
three csproj files and the CI
dotnet-versionmove to net10.0.xunit 2 → 3 and the .NET 10 test platform
xunit v3 runs on Microsoft.Testing.Platform, and the .NET 10 SDK no longer
supports the VSTest target that
coverlet.collectorplugs into:So
csharp/global.jsonoptsdotnet testinto the new platform, coverage comesfrom
Microsoft.Testing.Extensions.CodeCoverage, and CI collects it with--coverage --coverage-output-format cobertura(codecov reads that the same wayit read the coverlet output). No test source needed editing — all 507 tests
pass unchanged.
New gates
scripts/check-links-notation-parity.mjsreads the declared requirement out ofall four manifests and fails when they do not pin the same minor. It compares
major.minoronly, deliberately: the ranges are open at the patch level so abug-fix release lands without a commit, and the resolved patch legitimately
differs between ecosystems.
It runs as its own job in the parity workflow rather than inside the
existing one, because
[skip-parity]must not switch it off. A single-languagesource change is sometimes intentional; four different grammars behind one
conformance suite never is.
It has its own tests (
node --test scripts/*.test.mjs, 34 passing), includingthe four pins this repository actually shipped before this PR as a regression
case, and a check that each extractor still finds the version in the real
manifest — so a manifest reformat cannot silently turn the gate into a no-op.
.github/dependabot.ymlcovers cargo, npm, pip, nuget and github-actionsweekly, grouping minor and patch per ecosystem and leaving majors as separate
pull requests.
Verification
The issue asked for a check that a document written by each implementation reads
back identically in the other three.
experiments/issue-47/run.shdoes exactlythat: each language writes one record, each language decodes all four documents
and re-encodes what it read. Re-encoding is what makes the comparison
language-agnostic — the four share no value type, but they do share a text
format.
Everything else, run locally:
cargo testcargo clippy --all-targets -- -D warnings,cargo fmt --checknode --test tests/*.test.jsnpm run check(eslint, prettier, jscpd)node --test scripts/*.test.mjspytestruff check,ruff format --check,mypy srcdotnet testdotnet format --verify-no-changes,dotnet build /warnaserrorOne test changed, and why
python/tests/test_single_line_format.pyasserted the shape of the failurewhen the parser meets a hand-rolled dialect: under 0.11 the parentheses were
swallowed into the ids. Under 0.16 Python reads them as anonymous links, while
Rust and JavaScript reject the string outright. The test now asserts the
outcome the test was actually about — that the record does not come back — the
same way the C# test already did.
That divergence is upstream and worth reporting separately: at
links-notation0.16.1,((:"bytes" 2827) (:"complete" true))is a parse errorin the Rust and JavaScript parsers and a successful parse in the Python one.
Nothing in this repository can fix it; the new gate at least guarantees all four
meet it at the same version.
Why
[skip-parity]Bumping the C# package reference edits
csharp/src/Lino.Objects.Codec/Lino.Objects.Codec.csproj, which is undercsharp/src/, while the other three languages declare their dependencies inmanifests outside
*/src/. The parity gate therefore sees "C# source changed,the other three did not". No codec logic changed in any language.
Note on the checks tab: the first "Languages Change Together" run started before
[skip-parity]was in the body, and re-running it replays the original event payload, so it re-reads the old body and fails again. The run triggered after the edit passes.