# Cross-platform parity — adding & running vectors

Shade keeps its TypeScript and Kotlin implementations in lock-step via a
**single source of truth**: `test-vectors/*.json`. Both runners load the
same files and verify their native code produces byte-identical output.

This document covers:

1. How the parity gate works (CI)
2. How to run vectors locally
3. How to add a new vector

## How the gate works

```
                      ┌─────────────────────────────────┐
                      │   scripts/generate-vectors.ts   │
                      │   (TS reference implementation) │
                      └────────────────┬────────────────┘
                                       │ writes
                                       ▼
                      ┌─────────────────────────────────┐
                      │       test-vectors/*.json       │
                      │   { version: 2, vectors: [...] }│
                      └─────┬──────────────────┬────────┘
                            │                  │
                            │ loaded by        │ loaded by
                            ▼                  ▼
        ┌───────────────────────────┐  ┌───────────────────────────┐
        │ packages/shade-core/      │  │ android/shade-android/    │
        │   tests/cross-platform-   │  │   src/test/kotlin/.../    │
        │   vectors.test.ts         │  │   CrossPlatformVectorTest │
        │ (bun)                     │  │ (gradle JUnit4)           │
        └───────────────────────────┘  └───────────────────────────┘
                            │                  │
                            └─────────┬────────┘
                                      ▼
                       both must pass before merge
                       (.gitea/workflows/cross-vectors.yml)
```

The CI workflow has **two independent jobs** — `ts-vectors` and
`kotlin-vectors`. Either failing blocks the merge. The TS job also runs
`bun run vectors:gen` and fails if the result diverges from the committed
files: vector commits must come from the generator, never hand edits.

Vector files have a `version` integer at the top. Bump
`VECTOR_FILE_VERSION` in `scripts/generate-vectors.ts` whenever the
**schema** of any vector file changes (not just the values). Both test
suites assert the version matches their hard-coded expectation.

## Running vectors locally

### TypeScript

```bash
bun run test:vectors
# under the hood:
# bun test packages/shade-core/tests/cross-platform-vectors.test.ts
```

### Kotlin (JVM, no Android SDK required)

```bash
cd android
./gradlew :shade-android:test
```

Requires JDK 17. The wrapper downloads Gradle 8.10.2 on first run. Tink
1.15.0 (JVM JAR) is pulled from Maven Central.

### Regenerating vectors

When the protocol changes (new wire field, new label, new derivation step)
the TS reference is the source of truth. Edit `generate-vectors.ts`, then:

```bash
bun run vectors:gen
git diff test-vectors/   # eyeball the change
bun run test:vectors     # confirm TS still agrees
cd android && ./gradlew :shade-android:test  # confirm Kotlin still agrees
```

If Kotlin disagrees, **fix Kotlin** — TS is canonical. If both agree but
the diff is unintentional (e.g. you added a field by accident), revert
the generator change.

## Adding a new vector

A new sjekkpunkt has four pieces: generator code, schema, TS test,
Kotlin test. All four must land in the same PR; otherwise the gate
trips on the missing half.

### Step 1 — Add a generator function

In `scripts/generate-vectors.ts`, add a function that:

- Takes deterministic inputs (no randomness — fix every byte)
- Computes the value via the TS reference primitives
- Returns a `Vector[]` with a `description` per case + all inputs and outputs
  in hex

Example skeleton:

```ts
async function generateMyVectors(): Promise<Vector[]> {
  const input = new Uint8Array(32).fill(0xab);
  const output = await someRefImpl(input);
  return [{
    description: 'My new sjekkpunkt: known input → known output',
    input: hex(input),
    output: hex(output),
  }];
}
```

Wire it up in `main()`:

```ts
['my-vectors.json', { vectors: await generateMyVectors() }],
```

Run `bun run vectors:gen` → you should see `✓ my-vectors.json` and a new
file appears under `test-vectors/`.

### Step 2 — Add a TS test

In `packages/shade-core/tests/cross-platform-vectors.test.ts`:

```ts
test('My vectors match', async () => {
  const { vectors } = loadVectors('my-vectors.json');
  for (const v of vectors) {
    const actual = await someRefImpl(fromHex(v.input));
    expect(hex(actual)).toBe(v.output);
  }
});
```

`loadVectors` already asserts the version field matches. If you're
introducing a schema-breaking change, bump `EXPECTED_VECTOR_VERSION` and
`VECTOR_FILE_VERSION` together.

### Step 3 — Add the Kotlin equivalent

In
`android/shade-android/src/test/kotlin/no/zyon/shade/CrossPlatformVectorTest.kt`:

```kotlin
@Test
fun myVectorsMatch() {
    val vectors = loadVectors("my-vectors.json")
    for (i in 0 until vectors.length()) {
        val v = vectors.getJSONObject(i)
        val actual = someKotlinImpl(fromHex(v.getString("input")))
        assertEquals(v.getString("output"), hex(actual))
    }
}
```

If the Kotlin port doesn't yet have `someKotlinImpl`, that's the implementation
work the new vector is gating — write it and re-run the test until it passes.

### Step 4 — Verify the gate trips on divergence

Sanity check: temporarily flip a byte in your Kotlin port and run
`./gradlew :shade-android:test`. The test should fail within 60 seconds
(see `docs/V3.5.md` §Akseptansekriterier). Revert.

## Why a separate generator (vs. golden fixtures)?

Golden test fixtures rot — when the protocol changes, every test file
that pinned a literal hex string needs updating, and it's easy to
"update" Kotlin to match a stale TS-generated value. By centralising
vector generation in one TS script, **the protocol changes in one
place** (the reference impl + `generate-vectors.ts`), the file
regenerates with one command, and any platform that drifts gets caught
by the next CI run.

## Schema versioning

`{ "version": 2, "vectors": [...] }` is the file format. Bump the int
when the **shape** of any vector changes (e.g. you add a field consumers
must read). Both runners hard-code their expected version and refuse to
parse mismatched files — this catches the case where a new vector field
was added in TS but the Kotlin loader silently ignored it.

Schema changes go in the same PR as the bump + the matching loader
update on both sides.
