Skip to content

deps: update every dependency in all four languages, pin one links-notation, and gate against drift - #48

Merged
konard merged 12 commits into
mainfrom
issue-47-7a09fbbdea01
Aug 28, 2026
Merged

deps: update every dependency in all four languages, pin one links-notation, and gate against drift#48
konard merged 12 commits into
mainfrom
issue-47-7a09fbbdea01

Conversation

@konard

@konard konard commented Aug 28, 2026

Copy link
Copy Markdown
Member

Closes #47.

[skip-parity] — see the note at the end for why.

Every dependency in all four implementations is now current, the four
links-notation pins are one version, and two gates exist so neither can drift
again unnoticed.

One parser behind four implementations

links-notation was pinned four different ways: Rust 0.12, JavaScript
^0.11.0, Python >=0.11.0,<0.12.0, C# 0.13.0. The conformance suites assert
that 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

from to what it needed
eslint 9 10 @eslint/js added explicitly — ESLint 10 no longer supplies it transitively, and eslint.config.js has always imported it
lint-staged 16 17 nothing
jscpd 4 5 .jscpd.json repaired, see below
@changesets/cli 2 3 Node ≥ 22.11
pytest 7 9 nothing
pytest-cov 4 7 nothing
mypy 1 2 nothing
xunit 2.6.6 xunit.v3 4.0.0 new test platform, see below

Also current: base64 0.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.json set
"format": "console". In both jscpd 4 and 5 format is the list of file
formats to scan
, not the reporter — there is no source language called
console, so the scan matched nothing and reported success on an empty set. I
verified this against jscpd 4.0.5 on the old config before changing anything:
same empty result, so this is not a v5 regression. With format removed and
skipComments renamed to v5's "mode": "weak", the scan is real and finds 48
clones / 3.88% duplicated lines. threshold is set to 4 as a ratchet rather
than 0, because getting to zero means refactoring js/src, which does not
belong in a dependency PR.

csharp/.gitignore un-ignored its own coverage output. A !TestResults/
line negated the [Tt]est[Rr]esult*/ rule ten lines above it, so a local
coverage run left the tree dirty. Removed.

Floors raised to what CI actually runs

  • Python dev extras: pytest>=9.1, pytest-cov>=7.1, mypy>=2.3,
    ruff>=0.16.5, scriv>=1.8, build setuptools>=84.0. The old floors
    (pytest>=7.0, mypy>=1.0, ruff>=0.1.0) let a fresh pip install -e ".[dev]"
    resolve to tooling several majors behind CI.
  • Python links-notation is >=0.16.1,<0.17.0 — open at the patch level, so a
    bug-fix release is picked up without a commit.
  • js engines.node: >=18.0.0>=22.11.0. Node 18 has been end-of-life
    for 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: base64 0.23 and
    serde_json 1.0.151 each declare 1.71. Now edition = "2024",
    rust-version = "1.85". Clippy, MSRV-aware once 1.85 is declared, then
    flagged repeat().take() in rust/src/readable.rs; it is repeat_n now.
    rustfmt's 2024 style edition re-sorted imports across 11 files.
  • Link.Foundation.Links.Notation 0.16.1 ships only lib/net10.0 (0.15.0 was
    the last with lib/net8.0 — I unpacked the nupkgs to confirm). Staying on
    net8.0 meant staying on 0.13.0 and keeping the version divergence, so all
    three csproj files and the CI dotnet-version move 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.collector plugs into:

error : Testing with VSTest target is no longer supported by
Microsoft.Testing.Platform on .NET 10 SDK and later.

So csharp/global.json opts dotnet test into the new platform, coverage comes
from Microsoft.Testing.Extensions.CodeCoverage, and CI collects it with
--coverage --coverage-output-format cobertura (codecov reads that the same way
it read the coverlet output). No test source needed editing — all 507 tests
pass unchanged.

New gates

scripts/check-links-notation-parity.mjs reads the declared requirement out of
all four manifests and fails when they do not pin the same minor. It compares
major.minor only, deliberately: the ranges are open at the patch level so a
bug-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-language
source 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), including
the 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.yml covers cargo, npm, pip, nuget and github-actions
weekly, 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.sh does exactly
that: 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.

--- comparing ---
PASS: all 16 readings agree on:
  (o: (phase "stream_end") (bytes 2827) (complete true) (server (o: (host "127.0.0.1") (port 18878))) (models ("claude-haiku" "claude-opus")))

Everything else, run locally:

result
cargo test 119 passed
cargo clippy --all-targets -- -D warnings, cargo fmt --check clean
node --test tests/*.test.js 497 passed
npm run check (eslint, prettier, jscpd) clean
node --test scripts/*.test.mjs 34 passed
pytest 488 passed
ruff check, ruff format --check, mypy src clean
dotnet test 507 passed
dotnet format --verify-no-changes, dotnet build /warnaserror clean
each language's example runs

One test changed, and why

python/tests/test_single_line_format.py asserted the shape of the failure
when 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-notation 0.16.1, ((:"bytes" 2827) (:"complete" true)) is a parse error
in 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 under
csharp/src/, while the other three languages declare their dependencies in
manifests 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.

Adding .gitkeep for PR creation (default mode).
This file will be removed when the task is complete.

Issue: #47
@konard konard self-assigned this Aug 28, 2026
konard added 11 commits August 28, 2026 13:50
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.
@konard konard changed the title [WIP] Update all dependencies in all four languages: major versions behind in each, and four different links-notation pins deps: update every dependency in all four languages, pin one links-notation, and gate against drift Aug 28, 2026
@konard
konard marked this pull request as ready for review August 28, 2026 14:16
@konard

konard commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

Working session summary

Done. PR #48 is out of draft with all checks green: #48

What landed (11 commits, each major bump bisectable):

  • One parser everywhere. links-notation was pinned four different ways (Rust 0.12, JS ^0.11.0, Python >=0.11.0,<0.12.0, C# 0.13.0). All four now pin 0.16.1 in a single commit.
  • Every dependency current, including the deliberate majors: eslint 9→10 (needed an explicit @eslint/js), lint-staged 16→17, @changesets/cli 2→3, jscpd 4→5, pytest 7→9, pytest-cov 4→7, mypy 1→2, xunit 2→xunit.v3 4.
  • Two gates against recurrence. scripts/check-links-notation-parity.mjs (34 tests, incl. the pre-fix pins as a regression case) runs as its own parity job so [skip-parity] can't switch it off; .github/dependabot.yml covers cargo, npm, pip, nuget and github-actions.
  • Cross-language round trip in experiments/issue-47/run.sh: each language writes a record, each reads all four — 16 re-encodings, all identical.

Two things I found broken while in there:

  1. js/.jscpd.json set "format": "console", which is the file-format filter, not the reporter — so the duplication check had never analyzed a single file. I confirmed this against jscpd 4.0.5 on the old config first, so it isn't a v5 regression. Now real: 48 clones / 3.88%, with threshold: 4 as a ratchet rather than refactoring js/src inside a dependency PR.
  2. csharp/.gitignore had a !TestResults/ line negating the ignore rule ten lines above it.

Forced upgrades, not optional ones: rust-version = "1.70" was already unreachable (base64 0.23 and serde_json both declare 1.71) → edition 2024 / 1.85; and Link.Foundation.Links.Notation 0.16.1 ships only lib/net10.0 (verified by unpacking the nupkgs), so net8.0 meant staying divergent → net10.0 everywhere. xunit v3 then forced the .NET 10 test platform: coverlet.collector plugs into VSTest, which the .NET 10 SDK no longer supports, so coverage moves to Microsoft.Testing.Extensions.CodeCoverage. No test source needed editing; 507 tests pass unchanged.

Two things worth your call:

  • I raised js engines.node from >=18.0.0 to >=22.11.0. That narrows the supported platform. My reasoning is in the PR: Node 18 is a year EOL and CI has only ever run 22, so the old floor claimed untested support. Easy to drop back if you disagree.
  • At 0.16.1 there's a genuine upstream divergence: ((:"bytes" 2827) (:"complete" true)) is a parse error in the Rust and JS parsers and a successful parse in the Python one. Nothing here can fix it — it's a links-notation issue — but the new gate at least guarantees all four meet it at the same version. I documented it in the PR rather than filing upstream, since that's a separate repo.

One cosmetic wart on the checks tab: the first parity run started before [skip-parity] was in the body, and re-running it replays the stale event payload, so it re-reads the old body and fails again. The runs triggered after the edit pass; the PR is MERGEABLE.


This summary was automatically extracted from the AI working session output.

@konard

konard commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

🤖 Solution Draft Log

This 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)

  1. 116.4K / 1M (12%) input tokens, 39.0K / 128K (30%) output tokens
  2. 116.1K / 1M (12%) input tokens, 42.8K / 128K (33%) output tokens

Total: (2.6K new + 196.8K cache writes + 15.5M cache reads) input tokens, 92.9K output tokens, $12.037702 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus (claude-opus-5)
  • Thinking level: medium (~15999 tokens)
  • Model: Claude Opus 5 (claude-opus-5)

📎 Log file uploaded as Gist (4463KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard
konard merged commit 6b5c50f into main Aug 28, 2026
59 of 61 checks passed
@konard

konard commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

🔄 Auto-restart 1/5

Reason: 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.

@konard

konard commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

Working session summary

No 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 (chore(python), chore(csharp) xunit 2→3 migration, chore(js) Node floor), a single pinned links-notation version with the Implementations Pin One Parser Version gate, Dependabot across all four ecosystems, and test(experiments) verifying each implementation's output reads back in the other three.

One thing worth flagging: post-merge on main, Dependabot immediately opened update runs across all four ecosystems (cargo, pip, npm_and_yarn, nuget). That's the automation from ask #5 working as intended, but it means new dependency PRs will appear shortly and will need review.


This summary was automatically extracted from the AI working session output.

@konard

konard commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

🔄 Auto-restart-until-mergeable Log 1/5

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost: $0.329555

📊 Context and tokens usage:

  • 32.4K / 1M (3%) input tokens, 2.2K / 128K (2%) output tokens

Total: (14 new + 18.3K cache writes + 185.7K cache reads) input tokens, 2.2K output tokens, $0.329555 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus (claude-opus-5)
  • Model: Claude Opus 5 (claude-opus-5)

📎 Log file uploaded as Gist (4881KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update all dependencies in all four languages: major versions behind in each, and four different links-notation pins

1 participant