# 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 { 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.