Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,44 @@ jobs:
shell: pwsh
run: ./scripts/package-smoke.ps1

differential:
name: Cross-language differential
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22.19.0
cache: npm
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: stable
cache-dependency-path: ports/go/go.sum
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
cache: pip
cache-dependency-path: ports/python/pyproject.toml
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: |
8.0.x
10.0.x
- name: Use supported npm
run: npm install --global npm@10.9.7
- run: npm ci
- name: Install the Python port
run: python -m pip install ./ports/python
- name: Download Go modules
working-directory: ports/go
run: go mod download
- name: Restore .NET with locked dependencies
working-directory: ports/dotnet
run: dotnet restore --locked-mode
- name: Compare every implementation over one corpus
run: npm run differential

required:
name: Required checks
if: always()
Expand All @@ -207,6 +245,7 @@ jobs:
- python-package
- dotnet
- dotnet-package
- differential
runs-on: ubuntu-latest
steps:
- name: Require successful CI
Expand All @@ -218,6 +257,7 @@ jobs:
PYTHON_PACKAGE_RESULT: ${{ needs.python-package.result }}
DOTNET_RESULT: ${{ needs.dotnet.result }}
DOTNET_PACKAGE_RESULT: ${{ needs.dotnet-package.result }}
DIFFERENTIAL_RESULT: ${{ needs.differential.result }}
run: |
test "$NODE_RESULT" = "success"
test "$BENCHMARK_RESULT" = "success"
Expand All @@ -226,3 +266,4 @@ jobs:
test "$PYTHON_PACKAGE_RESULT" = "success"
test "$DOTNET_RESULT" = "success"
test "$DOTNET_PACKAGE_RESULT" = "success"
test "$DIFFERENTIAL_RESULT" = "success"
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## Unreleased

- Added a required cross-language differential CI gate that runs the TypeScript,
Go, Python, and .NET command-line tools over one shared corpus of golden,
edge, invalid, malformed, and seeded random inputs and compares their complete
verification results, error codes, and verification-record digests
(`npm run differential`, documented in `docs/DIFFERENTIAL.md`).
- Fixed the TypeScript CLI accepting transport bytes that are not valid UTF-8.
Node's lossy decoding replaced malformed bytes with U+FFFD and then verified
the corrupted evidence; the Go, Python, and .NET ports already rejected it.
The CLI now reports `WORLDCUT_INVALID_JSON`, and a byte-order mark stays
rejected.
- Hardened the Go port so a JSON number that underflows the IEEE-754 double
range, such as `1e-400`, is accepted as a finite zero like the other ports,
while syntax errors and overflow to infinity are still rejected.

## 0.2.0 - 2026-09-03

- Added language-neutral protocol, canonicalization, and conformance
Expand Down
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ Before proposing a release-affecting change:
npm run release:check
```

Changes to protocol semantics, canonicalization, conformance data, or any
language port must also pass the four-toolchain differential gate:

```sh
npm run differential
```

Its Go, Python, and .NET prerequisites and reproducible seed controls are
documented in [`docs/DIFFERENTIAL.md`](docs/DIFFERENTIAL.md).

## Pull requests

Keep changes focused and include tests for observable behavior. Protocol
Expand Down
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ Demonstrated package, GitHub integration, and benchmark evidence is summarized
in [`docs/VALIDATION.md`](docs/VALIDATION.md).

Language-neutral protocol semantics and golden vectors are under
[`spec/0.1`](spec/0.1) and [`conformance/0.1`](conformance/0.1).
[`spec/0.1`](spec/0.1) and [`conformance/0.1`](conformance/0.1). A required CI
gate additionally runs all four implementations over one shared corpus of golden,
edge, invalid, malformed, and seeded random inputs and compares their complete
results, as described in
[`docs/DIFFERENTIAL.md`](docs/DIFFERENTIAL.md).

## Implementations

Expand All @@ -57,6 +61,14 @@ Language-neutral protocol semantics and golden vectors are under
| [Python](ports/python) | 0.1 / 0.1.2 | Independent conformant verifier and CLI; integrations not yet included |
| [.NET](ports/dotnet) | 0.1 / 0.1.2 | Independent conformant verifier and CLI for .NET 8 and .NET 10; integrations not yet included |

Every port passes the committed vectors, and the
[cross-language differential suite](docs/DIFFERENTIAL.md) checks that they still
agree with the TypeScript reference on inputs no vector covers:

```sh
npm run differential
```

## Run the examples

```sh
Expand Down Expand Up @@ -470,6 +482,23 @@ npm run benchmark

The project uses the Node.js test runner and has no runtime dependencies.

The conformance corpus is checked with Node.js:

```sh
npm run conformance:check
```

Cross-language work additionally needs Go, Python, and .NET toolchains:

```sh
npm run differential
```

`npm run differential` builds each port's CLI once and compares all four
implementations over one shared corpus. Its seed, case count, and executable
overrides are documented in
[`docs/DIFFERENTIAL.md`](docs/DIFFERENTIAL.md).

Protocol details and runtime assumptions are documented in
[`docs/PROTOCOL.md`](docs/PROTOCOL.md). Production deployment requirements are
documented in [`docs/PRODUCTION.md`](docs/PRODUCTION.md).
Expand Down
203 changes: 203 additions & 0 deletions docs/DIFFERENTIAL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# Cross-language differential verification

The four WorldCut implementations share one protocol but no code. Passing the
committed vectors in `conformance/0.1` proves that each one agrees with the
specification on a small fixed corpus. It does not prove that they still agree
on inputs nobody wrote a vector for.

The differential suite closes that gap. It runs the TypeScript, Go, Python, and
.NET command-line tools over one identical corpus of transport bytes and
compares their complete parsed verification results.

```sh
npm run differential
```

The suite is a required CI check. The `differential` job in
[`.github/workflows/ci.yml`](../.github/workflows/ci.yml) runs it on every pull
request and `Required checks` fails unless it succeeds.

## What is compared

For every case the harness spawns each CLI with the same input file, then
classifies the outcome as a verification result, a stable error envelope, or an
unusable output. An unusable output always fails the case.

| Case category | Comparison |
| --- | --- |
| Verification input that TypeScript accepts | Every port must print a result that is structurally identical to the TypeScript result, including summaries, `details`, acquisition action identifiers, costs and order, the acquisition plan, coverage counters, protocol and engine versions, and `verificationRecordDigest`. |
| Committed verification vector | The TypeScript result must additionally equal the committed `expected` result. |
| Verification input that TypeScript rejects | Every port must fail with the same stable `WORLDCUT_*` code. |
| Committed invalid vector | Every port must fail with the code recorded in `invalid-vectors.json`. |
| Malformed raw transport bytes | Every port must fail with an outcome that `spec/0.1/CONFORMANCE.md` permits: `PARSE_ERROR` or `WORLDCUT_INVALID_INPUT`. A port that accepts the bytes fails the case. |

`verificationRecordDigest` is checked twice: it must match
`^[0-9a-f]{64}$` in every port, and every port's digest must equal the
TypeScript digest. Case pairs that are canonically identical but textually
different — reordered members, reordered observations and requirements, `\u`
escapes versus literal UTF-8 — must also produce the same digest as each other.

## What is deliberately ignored

Results are compared after `JSON.parse`, so the following never fail a case:

- indentation, spacing, and line endings;
- object member order;
- `\uXXXX` escaping versus literal UTF-8;
- number spelling, for example `1e+21` versus `1E+21` versus `1000000...`;
- `-0` versus `0`, because `worldcut-json-v1` serializes negative zero as `0`
and the two spellings can never produce different digests.

Everything else is treated as a semantic difference, including a missing member,
a different array order, a different summary string, and a different cost.

## Case categories

| Category | Source | Count |
| --- | --- | ---: |
| `golden` | every case in `conformance/0.1/verification-vectors.json` | 15 |
| `invalid` | every case in `conformance/0.1/invalid-vectors.json` | 12 |
| `raw` | every case in `conformance/0.1/raw-vectors.json` | 1 |
| `example` | every published fixture in `examples/` | 4 |
| `edge` | handcrafted deterministic cases | 31 |
| `transport` | handcrafted malformed byte sequences | 20 |
| `random` | seeded generated inputs | `--count`, default 500 |

The `edge` category covers finite IEEE-754 underflow (`1e-400`), negative zero,
alternative number spellings, boundary doubles, integer precision loss, UTF-16
member ordering across ASCII, Latin-1, full-width, and astral names, whitespace
and array-index value paths, structural `value_equals` comparison, deep nesting,
every dependency and temporal outcome, acquisition action de-duplication,
required and advisory aggregation, cost boundaries, and input array reordering.

The `transport` category covers empty and whitespace-only files, truncated
documents, trailing values, trailing commas, byte-order marks, invalid UTF-8,
raw control characters, NUL bytes, unpaired surrogate escapes, `NaN` and
`Infinity` literals, numbers that overflow to infinity, leading zeros,
hexadecimal, single quotes, and non-object top-level values.

Cases whose meaning depends on lexical form are authored as text or bytes, never
as JavaScript values. `JSON.stringify` would turn `1e-400` into `0` and `-0`
into `0` before any CLI could observe them.

### Randomized inputs

Randomized cases are generated from `${seed}:${index}` with a seeded sfc32
generator, so a seed and count always reproduce the same bytes on every
platform. They exercise nested arrays and objects, Unicode strings and member
names, UTF-16 ordering, safe finite doubles and integers, negative zero,
`value_equals` hits and misses, satisfied, violated, and unknown dependency
cases, temporal overlap and gap cases, reordered observation and requirement
arrays, acquisition planning and de-duplication, and required and advisory
aggregation. Nesting stays far below the 48-level transport cap documented by
the .NET port.

About a third of the generated corpus is built in a coherent mode where every
requirement is satisfiable, so the suite keeps reaching `CONTRACT_SATISFIED` and
`NOT_NEEDED` acquisition plans rather than only failure paths. The run fails if
fewer than 75% of randomized cases produce a verification result, or if the
randomized corpus stops reaching all three verdicts.

## Options

| Flag | Environment variable | Default |
| --- | --- | --- |
| `--seed <text>` | `WORLDCUT_DIFFERENTIAL_SEED` | `worldcut-0.1` |
| `--count <n>` | `WORLDCUT_DIFFERENTIAL_COUNT` | `500` |
| `--jobs <n>` | `WORLDCUT_DIFFERENTIAL_JOBS` | `max(2, min(6, available parallelism))` |
| `--only <substring>` | — | all cases |
| `--category <name>` | — | all categories |
| `--max-failures <n>` | — | `10` reported in full |
| `--timeout-ms <n>` | `WORLDCUT_DIFFERENTIAL_TIMEOUT_MS` | `60000` per CLI invocation |
| `--list` | — | print the corpus and exit |
| `--self-check-only` | — | run the harness self-checks and exit |

```sh
npm run differential -- --seed release-audit --count 2000
npm run differential -- --only edge/number-underflow-positive --count 0
npm run differential -- --category transport --count 0 --list
```

The harness runs deterministic self-checks before it starts any port. Those
checks cover the seeded generator, the raw-lexeme writer, the structural
comparison, the outcome mapping, and corpus invariants, so a broken harness
fails loudly instead of comparing a weaker corpus. `npm test` runs them too, so
a harness regression is caught by the Node job even though that job has no Go,
Python, or .NET toolchain.

## Prerequisites

| Port | Requirement |
| --- | --- |
| TypeScript | Node.js 22.19 or newer. `npm run differential` builds `dist/` first. |
| Go | A Go toolchain that satisfies `ports/go/go.mod`. The harness builds `cmd/worldcut-go` once. |
| Python | Python 3.11 or newer with the `ports/python` package installed, for example `python -m pip install ./ports/python`. |
| .NET | The .NET SDKs named in `ports/dotnet/global.json`. The harness builds `WorldCut.Tool` once. |

Executables are resolved from `PATH` unless overridden:

| Variable | Default | Purpose |
| --- | --- | --- |
| `WORLDCUT_NODE` | the running `node` | TypeScript CLI host |
| `WORLDCUT_GO` | `go` | Go toolchain |
| `WORLDCUT_PYTHON` | `python` | Python interpreter that can import `worldcut` |
| `WORLDCUT_DOTNET` | `dotnet` | .NET host |
| `WORLDCUT_DOTNET_FRAMEWORK` | `net8.0` | .NET target framework to build and run |

Example on a machine with private toolchains:

```sh
WORLDCUT_GO=/opt/go/bin/go \
WORLDCUT_PYTHON=ports/python/.venv/bin/python \
WORLDCUT_DOTNET=/opt/dotnet/dotnet \
npm run differential
```

Inputs are written to a temporary directory outside the repository and that
directory is always removed, including after a failure.

Each CLI invocation is terminated if it exceeds the configured timeout. A
timeout, runtime error, argument error, or file error never counts as an
allowed parser rejection for malformed transport bytes.

## Reading a failure

A failing case prints its identifier and category, the seed and count that
produced it, a ready-to-paste reproduction command, the exact input bytes with
their length and SHA-256, and the specific differences per port as JSON
pointers. Inputs larger than 8 KiB, or inputs that are not valid UTF-8, are
printed as base64 so the exact bytes survive. Nothing else from the workspace is
printed.

## Why TypeScript is the oracle

The TypeScript package is the reference implementation. `spec/0.1` and the
committed vectors are generated from it by `npm run conformance:update`, and
`spec/0.1/CANONICALIZATION.md` defines canonicalization in terms of the
ECMAScript rules it follows. When the ports disagree, TypeScript defines the
answer unless TypeScript itself is shown to violate the specification, in which
case the specification, the vectors, and every implementation are corrected
together.

Using one oracle is a pragmatic choice, not a claim that TypeScript is correct.
The suite also re-checks TypeScript against the committed golden results on
every run, so a regression in the oracle fails the gate instead of being copied
into the other ports.

## What this does and does not establish

Agreement across a large shared corpus is evidence of protocol equivalence for
the inputs that were compared. It is not a proof.

The suite does not:

- exhaustively cover the input space, or replace the committed vectors;
- prove the four implementations are equivalent for untested inputs;
- constitute a formal verification, a model check, or a refinement proof;
- compare library APIs, only the command-line tools;
- exercise every supported language runtime version — CI runs the differential
job on one Node.js, Go, Python, and .NET version each, while the independent
per-language jobs cover the full support matrix.

Its value is regression pressure: an accidental divergence introduced by a
change to any one port is very likely to fail this gate before it is released.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"docs/INTEGRATIONS.md",
"docs/AGENTIC_DATA_KERNEL.md",
"docs/VALIDATION.md",
"docs/DIFFERENTIAL.md",
"spec",
"conformance",
"README.md",
Expand All @@ -85,12 +86,13 @@
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
"build": "npm run clean && tsc -p tsconfig.json",
"prepack": "npm run build",
"check": "tsc -p tsconfig.json --noEmit",
"check": "tsc -p tsconfig.json --noEmit && tsc -p scripts/differential/jsconfig.json",
"test": "npm run build && node --test \"dist/test/*.test.js\"",
"test:package": "node scripts/test-package.mjs",
"release:check": "npm run check && npm test && npm run examples && npm run benchmark && npm run test:package",
"conformance:update": "npm run build && node scripts/generate-conformance.mjs --write",
"conformance:check": "npm run build && node scripts/generate-conformance.mjs",
"differential": "npm run conformance:check && node scripts/differential.mjs",
"example": "npm run examples",
"examples": "npm run build && node dist/examples/fixtures.js",
"verify": "npm run build && node dist/cli.js",
Expand Down
Loading
Loading