Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
cac6a2a
Add an independent .NET adapter
jbwinters Oct 29, 2026
d9039db
Test the .NET adapter offline and against Core
jbwinters Oct 29, 2026
3d0c60d
Run the .NET adapter's tests in CI
jbwinters Oct 29, 2026
de2cc29
Send no handler answer once the handler's token is cancelled
jbwinters Oct 29, 2026
1551f25
Document the .NET adapter, its limits, and its evidence
jbwinters Oct 29, 2026
37dd858
Bound the abandoned-handler test so a regression fails instead of han…
jbwinters Oct 29, 2026
8e49a6b
Run handler token callbacks on a pool thread, never on the caller's
jbwinters Oct 29, 2026
d8fa912
Cap poll timeouts in ticks so a long deadline cannot poll forever
jbwinters Oct 29, 2026
c2c7b3d
Refuse an undefined failure category instead of letting it escape Invoke
jbwinters Oct 29, 2026
d4a53d6
Dispatch from the same registry snapshot the invocation advertised
jbwinters Oct 29, 2026
4e88505
Explain the process-global test seams in Host
jbwinters Oct 29, 2026
81be343
Build the .NET 8 CI job with the .NET 8 SDK
jbwinters Oct 29, 2026
147eb6c
Document the .NET handler token, deadline range, and pid-based tree kill
jbwinters Oct 29, 2026
f906ebb
Cancel the .NET handler token from a dedicated thread when the invoca…
jbwinters Oct 29, 2026
6e3849a
Cancel the .NET handler token before the worker is killed
jbwinters Oct 29, 2026
ada0c27
Document when the .NET handler token is cancelled and where callbacks…
jbwinters Oct 29, 2026
3d2f20c
Record the .NET handler-token evidence: 82 offline and 87 live tests
jbwinters Oct 29, 2026
b681efe
Start the .NET handler token's cancellation before every worker kill
jbwinters Oct 29, 2026
52eb5de
Never let starting the .NET token cancellation escape Invoke or skip …
jbwinters Oct 29, 2026
1d73c53
Keep the .NET thread count in the resource test to the current invoca…
jbwinters Oct 29, 2026
6f96b93
Describe the .NET 5 ms poll as a requested timeout and document the c…
jbwinters Oct 29, 2026
4e32b9f
Record the .NET kill-order and cancellation-fallback evidence: 90 off…
jbwinters Oct 29, 2026
936421f
Kill a .NET worker whose pipe setup fails through the common kill path
jbwinters Oct 29, 2026
8fbd207
Assert one .NET stop notice per kill, on the killing thread
jbwinters Oct 29, 2026
55ff96d
Make the .NET post-handler kill-order case deterministic
jbwinters Oct 29, 2026
f3e9e90
Show HandlerTokenNotCancelled in the .NET result's ToString
jbwinters Oct 29, 2026
e0a01ac
Leave runtime assembly files out of the .NET descriptor counts
jbwinters Oct 29, 2026
66eb43d
Record the .NET post-spawn kill and stop-notice evidence: 94 offline …
jbwinters Oct 29, 2026
093022d
Close the .NET worker's obtained pipe streams when checking them fails
jbwinters Oct 29, 2026
abf75cc
Leave only the runtime's and test output's assemblies out of the .NET…
jbwinters Oct 29, 2026
1eeb122
Read the .NET descriptor counts at once after each release instead of…
jbwinters Oct 29, 2026
03eca5d
Keep the .NET kill-path methods out of inlining so the kill-order sta…
jbwinters Oct 29, 2026
d53f719
Record the .NET pipe-check cleanup evidence: 96 offline and 101 live …
jbwinters Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,31 @@ jobs:
- name: Offline adapter and SDK tests
run: mvn -B -ntp verify

dotnet:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
dotnet: ["8.0", "10.0"]
defaults:
run:
working-directory: dotnet
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ matrix.dotnet }}.x
# The runner may also have a newer SDK, which dotnet would pick by
# default. Pin this job to the .NET 8 SDK so it builds as an 8.0-only
# toolchain would (the test project targets net8.0 alone there).
- name: Pin the .NET 8 SDK
if: matrix.dotnet == '8.0'
run: |
printf '{"sdk":{"version":"8.0.100","rollForward":"latestFeature"}}\n' > global.json
dotnet --version | grep '^8\.0\.'
- name: Offline tests
run: dotnet test test/JacquardHost.Tests -f net${{ matrix.dotnet }}

bootstrap:
runs-on: ubuntu-latest
strategy:
Expand Down Expand Up @@ -183,3 +208,15 @@ jobs:
test "$status" = 1
run --workdir "$RUNNER_TEMP/java-recovery" --deliver-pending fulfillment | grep -q '{"delivered":1}'
run --workdir "$RUNNER_TEMP/java-recovery" --deliver-pending fulfillment | grep -q '{"delivered":0}'
- uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x
- name: Run independent .NET adapter against Core
run: |
dotnet test test/JacquardHost.Tests -f net8.0
dotnet test test/JacquardHost.Tests -f net10.0
working-directory: dotnet
env:
JACQUARD_REQUIRE_LIVE: "1"
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ invent messages that Core has not specified.
- Keep the first carrier serial, bounded, versioned, and fail-closed.
- Preserve transport independence: process framing is one carrier, not the
semantic definition of the boundary.
- The Python, TypeScript, Go, Rust, and Java adapters each implement the
pinned fixtures independently. Keep pin constants and kit copies generated
- The Python, TypeScript, Go, Rust, Java, and .NET adapters each implement
the pinned fixtures independently. Keep pin constants and kit copies generated
by `scripts/core_pin.py`.
- Add tests with behavior. Use the standard library until a reviewed need
justifies a dependency.
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@
worker's output FIFOs, and `HandlerContext.of` for calling a handler
outside an invocation. CI runs the SDK's offline and live tests and the
four projects.
- .NET adapter: an independent project in `dotnet/` (package
`JacquardLang.Host`, targeting .NET 8 and tested on .NET 8 and 10) with no
runtime dependencies beyond the .NET base class library. It provides strict
codecs that parse integers exactly from their text, the session state
machine, a bounded process carrier for `jacquard host worker`, and a `Host`
with a closed operation registry. It carries the pinned kit as embedded
resources and replays all 21 recordings. It is protocol-level only, with no
SDK layer. .NET cannot start the worker in its own process group or
interrupt a pipe read or write, so the carrier never calls one that can
block: it waits on the C library's `poll(2)` and runs workers on Linux and
macOS only. It kills the worker's process tree with `Process.Kill(true)`,
reports a descendant that keeps the pipes open after the worker exits as a
carrier loss, and documents what it misses in `docs/LIMITS.md`.
`scripts/core_pin.py` now generates `dotnet/src/JacquardHost/Compat.cs` and
keeps the .NET kit copy in sync, and CI runs the offline tests on .NET 8 and
10 and the live tests against the pinned Core.

## 0.2.0 — 2026-10-25

Expand Down
41 changes: 36 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ Contributions are licensed under [Apache 2.0](LICENSE), matching this repository
`core-pin.json` names the one Core release all adapters target. Do not edit
`src/jacquard_host/compat.py`, `typescript/src/compat.ts`, `go/protocol/compat.go`,
`rust/src/compat.rs`, `java/src/main/java/org/jacquardlang/host/Compat.java`,
the Go, Rust, and Java kit copies under `go/kit/data/`, `rust/kit/`, and
`java/src/main/resources/org/jacquardlang/host/kit/`, or the generated block
`dotnet/src/JacquardHost/Compat.cs`, the Go, Rust, Java, and .NET kit copies
under `go/kit/data/`, `rust/kit/`,
`java/src/main/resources/org/jacquardlang/host/kit/`, and
`dotnet/src/JacquardHost/kit/`, or the generated block
in `docs/COMPATIBILITY.md` by hand: change the pin with
`python3 scripts/core_pin.py update --tag ...`, which also vendors the kit, and
`python3 scripts/core_pin.py check` (run in CI) reports any drift.
Expand Down Expand Up @@ -142,14 +144,42 @@ sync. The README's Java examples are compiled test sources under
differ). Copy the repository `NOTICE` to `java/NOTICE` when it changes (a test
fails if they differ).

## .NET

The independent .NET adapter lives in `dotnet/` as package `JacquardLang.Host`
(`dotnet/src/JacquardHost`). It targets .NET 8, has no runtime dependencies
beyond the .NET base class library, and does not call Python, Node.js, Go,
Rust, or Java; it calls the C library's `poll(2)` and runs workers on Linux
and macOS only. NUnit, its adapter, and the test SDK are its only (test)
dependencies. The .NET 8 or 10 SDK is required.

```sh
cd dotnet
dotnet test test/JacquardHost.Tests -f net8.0
dotnet test test/JacquardHost.Tests -f net10.0 # with a .NET 10 SDK
# With the pinned Core configured (eval "$(python3 ../scripts/core_pin.py install)"):
JACQUARD_REQUIRE_LIVE=1 dotnet test test/JacquardHost.Tests -f net8.0
```

CI runs the offline tests on .NET 8 and 10, and the live tests on both in the
shared pinned-Core job. The build treats warnings as errors, with nullable
reference types and the `8.0-recommended` analyzers.
`dotnet/src/JacquardHost/Compat.cs` is generated and
`dotnet/src/JacquardHost/kit/` is a byte copy of the pinned kit.
`scripts/core_pin.py` maintains both; do not edit them by hand. The
`JacquardHost.StandIn` program under `dotnet/test/` is a fixture for the
offline tests. Copy the repository `NOTICE` to `dotnet/NOTICE` when it changes
(a test fails if they differ).

## Releasing

A release is one commit on `main` with green CI. To cut one:

1. Open a pull request that dates the CHANGELOG entry and updates any version
references in the docs. Keep the Python (`src/jacquard_host/__init__.py`),
npm (`typescript/package.json`, `typescript/src/version.ts`), Rust
(`rust/Cargo.toml`), Java (`java/pom.xml`), and release versions equal.
(`rust/Cargo.toml`), Java (`java/pom.xml`), .NET
(`dotnet/Directory.Build.props`), and release versions equal.
2. After it merges, tag the merge commit twice, with annotated tags
(`git tag -a`):
`jacquard-host-X.Y.Z` for the repository and its assets, and `go/vX.Y.Z`
Expand All @@ -162,8 +192,9 @@ A release is one commit on `main` with green CI. To cut one:
4. Publish a GitHub release for `jacquard-host-X.Y.Z` with the CHANGELOG
entry as its notes and the assets attached.

Nothing is published to PyPI, npm, crates.io, or Maven Central. The Rust
crate is used from the repository tag, and the Java artifact is built from it.
Nothing is published to PyPI, npm, crates.io, Maven Central, or NuGet. The
Rust crate is used from the repository tag, and the Java artifact and the .NET
package are built from it.

## TypeScript

Expand Down
8 changes: 8 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@ prune rust/target
include java/pom.xml java/LICENSE java/NOTICE
recursive-include java *.java *.md *.json *.jac
prune java/target
include dotnet/Directory.Build.props dotnet/.editorconfig dotnet/LICENSE dotnet/NOTICE dotnet/JacquardHost.sln
recursive-include dotnet *.cs *.csproj *.md *.json *.jac
prune dotnet/src/JacquardHost/bin
prune dotnet/src/JacquardHost/obj
prune dotnet/test/JacquardHost.Tests/bin
prune dotnet/test/JacquardHost.Tests/obj
prune dotnet/test/JacquardHost.StandIn/bin
prune dotnet/test/JacquardHost.StandIn/obj
7 changes: 4 additions & 3 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ This product includes conformance artifacts from Jacquard Core,
https://github.com/jbwinters/jacquard-lang, licensed under Apache License 2.0.
The artifacts under src/jacquard_host/data/conformance/host-protocol-v0/,
and their byte-identical copies under go/kit/data/host-protocol-v0/,
rust/kit/host-protocol-v0/, and
java/src/main/resources/org/jacquardlang/host/kit/host-protocol-v0/, are
copied verbatim from the commits recorded in docs/COMPATIBILITY.md.
rust/kit/host-protocol-v0/,
java/src/main/resources/org/jacquardlang/host/kit/host-protocol-v0/, and
dotnet/src/JacquardHost/kit/host-protocol-v0/, are copied verbatim from the
commits recorded in docs/COMPATIBILITY.md.

Upstream notice:
Jacquard
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ the [Go SDK](go/README.md), the [Rust SDK](rust/README.md), and the
TypeScript SDK supports async service methods, the Go SDK maps native Go types
and context cancellation, the Rust SDK maps native Rust types through `record!`
and `variants!` declarations, and the Java SDK maps records, enums, and sealed
interfaces through explicit codecs.
interfaces through explicit codecs. A protocol-level [.NET](dotnet/README.md)
(.NET 8+) adapter also implements the pinned protocol independently. It has a
closed operation registry and kit replay, but no SDK layer yet.

**Experimental:** the SDK targets Core's pinned `jacquard-host-v0` protocol.
Core is installed separately. Each invocation runs one fresh, serial worker;
Expand Down Expand Up @@ -116,8 +118,9 @@ participant collection require separate application work.

See [CONTRIBUTING.md](CONTRIBUTING.md) for offline tests, installed-distribution
checks, and live runs. CI tests Python 3.11–3.13, Node.js 22/24, Go 1.24 and
stable, Rust 1.88 and stable, and Java 21 and 25. It separately installs the
pinned Core release for every adapter's live suite and the public examples.
stable, Rust 1.88 and stable, Java 21 and 25, and .NET 8 and 10. It
separately installs the pinned Core release for every adapter's live suite and
the public examples.

[Evidence](docs/EVIDENCE.md) records the checks and platform claims;
[development experiments](docs/development/README.md) preserve the usability
Expand Down
17 changes: 10 additions & 7 deletions docs/COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ release, recorded once in [`core-pin.json`](../core-pin.json). The Python
constants (`src/jacquard_host/compat.py`), the TypeScript constants
(`typescript/src/compat.ts`), the Go constants (`go/protocol/compat.go`), the
Rust constants (`rust/src/compat.rs`), the Java constants
(`java/src/main/java/org/jacquardlang/host/Compat.java`), and the block below
are generated from that file by `scripts/core_pin.py sync`.
(`java/src/main/java/org/jacquardlang/host/Compat.java`), the .NET constants
(`dotnet/src/JacquardHost/Compat.cs`), and the block below are generated from
that file by `scripts/core_pin.py sync`.
`scripts/core_pin.py check` fails CI when any of them drifts from the pin. It
also fails when any vendored kit copy drifts: the Python package data, Go's
embedded `go/kit/data/`, the Rust crate's embedded `rust/kit/`, or the Java
class-path copy under `java/src/main/resources/`. Adapters verify the kit's four
embedded `go/kit/data/`, the Rust crate's embedded `rust/kit/`, the Java
class-path copy under `java/src/main/resources/`, or the .NET embedded copy
under `dotnet/src/JacquardHost/kit/`. Adapters verify the kit's four
digests independently at load time, and the SDKs refuse a Core whose
`--version` differs from the pinned version.

Expand Down Expand Up @@ -43,7 +45,7 @@ Release assets verified by `scripts/core_pin.py install`:
| macos-x86_64 | `jacquard-macos-x86_64.tar.gz` | `8efc54bcf7573cb7cfd35fa3e77a584680ce68de872bdb212ed8369a974ff967` |
<!-- core-pin:end -->

Python, TypeScript, Go, Rust, and Java replay the same 21 recordings against the pinned
Python, TypeScript, Go, Rust, Java, and .NET replay the same 21 recordings against the pinned
worker, including Core's recorded `noncanonical-target-hash` divergence: the
HB.1 vector expects E1603 while the shipped preflight classifies an uppercase
target hash as a malformed scalar (E1601). The transcript is recorded as
Expand All @@ -54,8 +56,8 @@ independent v0 evidence; it does not change the protocol version or declare v1.
The adapters decode boundary descriptors in Core's order: the kind, then the
exact fields, then one node of `max_value_nodes`, then any identity, then the
list count. Malformed input therefore gets the same error code from every
adapter and from Core. For recording comparisons, Python, Go, Rust, and Java sort object
keys by code point, while TypeScript sorts them by UTF-16 code unit. The two orders
adapter and from Core. For recording comparisons, Python, Go, Rust, Java, and .NET sort
object keys by code point, while TypeScript sorts them by UTF-16 code unit. The two orders
differ only for keys outside the Basic Multilingual Plane. Every protocol key is
ASCII, so the comparisons agree.

Expand Down Expand Up @@ -117,6 +119,7 @@ python3 -m pip install . && python3 scripts/check_examples.py
(cd go && go test -race ./... && go test -count=1 ./...)
(cd rust && cargo test)
(cd java && JACQUARD_REQUIRE_LIVE=1 mvn -B verify)
(cd dotnet && JACQUARD_REQUIRE_LIVE=1 dotnet test test/JacquardHost.Tests -f net8.0)
```

`update` resolves the tag to its commit, copies the kit files from
Expand Down
Loading
Loading