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
60 changes: 60 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
node-version: [22, 24, 26]

defaults:
run:
working-directory: js

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

# pnpm version is pinned via the "packageManager" field in js/package.json.
- name: Enable Corepack
run: corepack enable

- name: Resolve pnpm store path
run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_ENV"

- uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('js/pnpm-lock.yaml') }}
restore-keys: pnpm-store-${{ runner.os }}-node${{ matrix.node-version }}-

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Typecheck
run: pnpm run typecheck

- name: Lint
run: pnpm run lint

- name: Format check
run: pnpm run format:check

- name: Test
run: pnpm test

- name: Build
run: pnpm run build
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ graft/

## 現在の実装状態

### 完了済み(触らないこと)
### 完了済み

- `buffer.ts`: ByteWriter / ByteReader(uvarint, svarint, f64, str)
- `format.ts`: Tag 0〜31(Null〜WeakSet)、KeyKind、MAGIC(`GRF1`)、VERSION
Expand Down
101 changes: 101 additions & 0 deletions conformance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Graft Conformance Harness

This directory defines how language implementations prove they conform to the
Graft binary format. The format itself is specified in
[`../spec/FORMAT.md`](../spec/FORMAT.md) — that document is the single source of
truth. This README only describes the *testing* contract.

There is no implementation code here yet; this is the skeleton that future
language ports build against.

---

## 1. Golden vectors

The canonical test inputs live in [`../spec/golden/`](../spec/golden/). Each
`.bin` file is a complete Graft stream (`MAGIC VERSION ROOT COUNT NODE{COUNT}`,
see FORMAT.md §3) produced by the reference JS encoder.

Every file's root is a single `Object` node whose keys name the individual
cases. Decode the whole file once, then assert on each key.

| File | Covers (FORMAT.md §) | Cases |
|------------------|----------------------|-------|
| `primitive.bin` | §5.1 | null, undefined, bool, int, float, NaN, -0, ±Infinity |
| `bigint.bin` | §5.1 | zero, large positive, large negative |
| `string.bin` | §2.4 | empty, ASCII, multibyte UTF-8, emoji (surrogate pairs) |
| `date.bin` | §5.3 | epoch, pre-epoch (negative ms), normal, far future |
| `bytes.bin` | §5.1 `Bytes` | ArrayBuffer (non-empty + empty) |
| `typedarray.bin` | §5.4 | every `ElementType` (0–10) + an empty array |
| `map_set.bin` | §5.5 | Map & Set, including an object-identity key shared between them |
| `symbol.bin` | §5.2 | Registered, Unique (with file-internal identity), WellKnown |
| `cycles.bin` | §4 | self-cycle, cross-references, shared identity |

### Regenerating

The vectors are generated from the reference implementation:

```bash
cd js && npx tsx scripts/gen-golden.ts
```

Regenerate only when FORMAT.md changes. The committed `.bin` files are the
authority that other languages test against — treat a diff in these files
during review as a deliberate format change, not an incidental one.

---

## 2. How a new implementation passes conformance

A conforming implementation must satisfy FORMAT.md §8. Concretely, against the
golden vectors:

1. **Read** each `.bin` file as raw bytes.
2. **Verify the header**: `MAGIC == "GRF1"`, `VERSION == 1`. Reject otherwise.
3. **Decode** the full heap using the two-pass algorithm (FORMAT.md §4):
allocate `COUNT` placeholder slots, parse every node, then resolve all
references. This is mandatory — cycles and shared identity cannot be
restored with a single recursive pass.
4. **Assert** the decoded values match the expected structure for each key.
Use the documented fallback (FORMAT.md §5) for any type the target language
cannot represent natively, and assert against the fallback shape instead.
5. **Error** on unknown tags (the reserved ranges in FORMAT.md §6) rather than
skipping them.

A round-trip test (decode → re-encode → byte-compare) is **not** required and
generally won't hold across languages: encoders may legitimately order heap
nodes differently. Conformance is defined by *decoded value equality*, not
byte equality. The one stable guarantee is the JS reference encoder
reproducing its own golden bytes.

### Expected-value notation

FORMAT.md §8 references a `.meta.json` sidecar describing each vector's expected
decoded structure in a language-neutral notation. Those files are not yet
generated. Until they exist, the expectations are defined by the case names in
the table above plus the inputs in `js/scripts/gen-golden.ts`, which is the
human-readable description of what each vector contains.

---

## 3. Directory convention for language implementations

Each language port gets its own subdirectory under `conformance/`:

```
conformance/
README.md ← this file
<lang>/ ← e.g. go/, rust/, python/, ruby/, cpp/
README.md ← how to build & run this port's conformance suite
... ← decoder + a test runner over ../../spec/golden/
```

Requirements for each `<lang>/`:

- A test runner that loads every `.bin` from `spec/golden/`, decodes it, and
asserts the documented expectations.
- A short `README.md` with the exact build/run command (e.g. `go test ./...`,
`cargo test`, `pytest`).
- No modification of `spec/golden/` or `spec/FORMAT.md` from within a language
directory. Format changes flow the other way: edit FORMAT.md first, then the
reference encoder, regenerate golden, then update each port.
1 change: 1 addition & 0 deletions js/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
save-exact=true
4 changes: 4 additions & 0 deletions js/.oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"ignorePatterns": []
}
13 changes: 13 additions & 0 deletions js/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "unicorn", "oxc"],
"categories": {
"correctness": "error"
},
"rules": {
"unicorn/no-new-array": "off"
},
"env": {
"builtin": true
}
}
42 changes: 33 additions & 9 deletions js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,41 @@
"name": "greft",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"author": "",
"files": [
"dist"
],
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsdown",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"lint": "oxlint",
"lint:fix": "oxlint --fix",
"format": "oxfmt",
"format:check": "oxfmt --check"
},
"devDependencies": {
"@types/node": "^25.9.1",
"tsx": "^4.22.3",
"typescript": "^6.0.3"
}
"@types/node": "25.9.1",
"fast-check": "4.8.0",
"oxfmt": "0.52.0",
"oxlint": "1.67.0",
"tsdown": "0.22.1",
"tsx": "4.22.3",
"typescript": "6.0.3",
"vitest": "4.1.7"
},
"packageManager": "pnpm@11.5.0+sha512.dbfcc4f81cf48597afd4bc391ffdf12c11f1a9fb83a395bfa6b0a2d9cc2fd8ffebafdb1ccbd529632153f793904c2615b7f09fe1a345473fd1c35845172a8eb1"
}
Loading
Loading