Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
bca3e10
Add TypeScript implementation design spec
Jul 4, 2026
37564a5
Add TypeScript implementation plan
Jul 4, 2026
2c20a48
TypeScript implementation: project scaffolding
Jul 4, 2026
028bf01
TypeScript implementation: configuration and migration model
Jul 4, 2026
354c00f
TypeScript implementation: template resolver
Jul 4, 2026
57ba78f
TypeScript implementation: use Object.hasOwn for template property lo…
Jul 4, 2026
c3a9da9
TypeScript implementation: canonical checksum
Jul 4, 2026
2ef4deb
TypeScript implementation: migration file loader
Jul 4, 2026
9192a46
TypeScript implementation: fail loudly on malformed migration YAML
Jul 4, 2026
3172867
TypeScript implementation: ES document types and index definition
Jul 4, 2026
b4a92cf
TypeScript implementation: fail loudly on malformed ES migration records
Jul 4, 2026
99249ca
TypeScript implementation: REST client operations
Jul 4, 2026
310972d
TypeScript implementation: distributed document lock
Jul 4, 2026
e8cf843
TypeScript implementation: Esque orchestrator with integrity verifica…
Jul 4, 2026
04a771c
TypeScript implementation: fix lock-release error shadowing in runMig…
Jul 4, 2026
0aeebda
TypeScript implementation: CLI entrypoint
Jul 4, 2026
76a1ddb
TypeScript implementation: fix CLI crash on bad URL and hang on inval…
Jul 4, 2026
621ccca
Register TypeScript implementation with compatibility harness
Jul 4, 2026
e9b8cd4
Add git-tag-based version script for TypeScript
Jul 4, 2026
7aa151c
Add TypeScript job to CI
Jul 4, 2026
369be12
Add TypeScript checks to pre-commit hook and Node to dev container
Jul 4, 2026
07eaf32
Fix pre-commit hook: use npm run lint to avoid npx biome false-pass
Jul 4, 2026
d2661a0
Document TypeScript implementation in CLAUDE.md
Jul 4, 2026
889802d
Fix stale 'both implementations' language in Architecture section now…
Jul 4, 2026
a60ba35
Restructure TypeScript docs to match JVM/Python section pattern
Jul 4, 2026
166971f
Fix stale verify_integrity method name and add TypeScript compat test…
Jul 4, 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
1,810 changes: 1,810 additions & 0 deletions .claude/superpowers/plans/2026-07-04-typescript-implementation.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# TypeScript Implementation Design

**Date:** 2026-07-04
**Status:** Draft — pending review
**Goal:** Add a TypeScript implementation of esque to the monorepo, published to npm as `esque-ts`, behaviorally identical to the JVM and Python implementations and validated by the existing black-box compatibility test harness.

## Decisions Made

| Decision | Choice | Rationale |
|----------|--------|-----------|
| ES client | Official `@elastic/elasticsearch` (^9) | Consistent with JVM and Python (both use official clients); handles auth mechanisms, retries, and typing. |
| npm package name | `esque-ts` | Mirrors the `esque-py` PyPI naming convention. |
| Snapshot publishing | Real npm registry, `dev` dist-tag | Prerelease versions (`X.Y.Z-dev.N`) are invisible to normal installs and semver ranges; closest match to the JVM `-SNAPSHOT` / Python TestPyPI flow without a second registry. |
| Toolchain | Lean Node-native: npm + `tsc` (strict) + `node:test` + Biome | Fewest moving parts; Biome is the ruff analog (one tool for lint + format); no bundler needed for a small library + CLI. |
| Module format | ESM-only (`"type": "module"`), Node >= 22 | Modern default; no CJS consumers expected for a CLI-first tool. |
| Directory | `implementations/typescript/` | Full platform name, consistent with `implementations/python/`. |

## Repository Placement

```
implementations/typescript/
├── package.json # name: esque-ts, type: module, bin: {"esque": "dist/cli.js"}, engines: {"node": ">=22"}
├── package-lock.json
├── tsconfig.json # strict: true, NodeNext modules, outDir dist/
├── biome.json # lint + format; line width 120 (matches ruff config)
├── src/
│ ├── configuration.ts # EsqueConfiguration
│ ├── esque.ts # Esque orchestrator + verifyStateIntegrity
│ ├── cli.ts # commander entrypoint (shebang line)
│ ├── migration/
│ │ ├── model.ts # MigrationFile, MigrationFileMetadata, MigrationFileContents,
│ │ │ # MigrationFileRequestDefinition, numeric version comparison
│ │ ├── template.ts # MigrationTemplateResolver: #{varName} validation + substitution
│ │ └── loader.ts # MigrationFileLoader: discovery, parsing, canonical checksum
│ └── elasticsearch/
│ ├── documents.ts # INDEX_DEFINITION, index name, wrapper-object document types
│ ├── operations.ts # RestClientOperations over @elastic/elasticsearch
│ └── lock.ts # ElasticsearchDocumentLock
└── tests/
├── model.test.ts # version ordering (1.9.0 < 1.10.0, padding)
├── checksum.test.ts # canonical checksum reference vectors
├── template.test.ts # validation + substitution across all request fields
└── integrity.test.ts # verifyStateIntegrity error scenarios via mock client
```

**Runtime dependencies:** `@elastic/elasticsearch` (^9), `commander`, `yaml`.
**Dev dependencies:** `typescript`, `@biomejs/biome`, `tsx`, `@types/node`.

## Behavioral Contract

The implementation follows the esque-new-language-implementation skill guide exactly: same nine
building blocks, same ES document wrapper-object shapes (`{"migration": {...}}`, `{"lock": {...}}`),
camelCase field names in ES, lock doc id `lock:<migrationKey>`, `.esque` index definition,
`refresh=true` on record creation, and identical CLI option surface. Only TypeScript-specific
design points are documented below.

## TypeScript-Specific Design

### Canonical checksum

`JSON.stringify` does not sort keys, so `loader.ts` includes a small canonical-serialization
helper:

1. Build `{"requests": [request.toCanonicalDict(), ...]}` from the **resolved** requests;
`toCanonicalDict()` returns only non-null fields with camelCase keys.
2. Recursively drop `null`/`undefined` values.
3. Serialize with keys sorted alphabetically at every level, compact separators (no spaces).
4. UTF-8 encode → `node:crypto` MD5 → `Buffer.readInt32BE(0)` (first 4 bytes as big-endian
signed 32-bit int).

`checksum.test.ts` pins the same reference vectors as the Python `test_checksum.py`, which
guarantees cross-implementation equality; the compatibility harness verifies it end-to-end.

### Async model and lifecycle

- All ES-touching methods are `async`; the CLI awaits `execute()`.
- `Esque` constructor is `(client: Client, configuration: EsqueConfiguration, properties: Record<string, string> = {})`
and only stores references / instantiates collaborators — no I/O at construction time
(required so unit tests can construct with a mock client).
- `Esque` implements `close()` (try unlock → swallow "not held" errors, warn on others; then
close the client) and `Symbol.asyncDispose` delegating to `close()` so `await using` works.
- `verifyStateIntegrity` and `verifyRecordIntegrity` are TS-`private` methods on `Esque`, using
`configuration.migrationKey` directly. Unit tests access them via index access
(`esque["verifyStateIntegrity"](...)`) on an instance built with a mock client — TS `private`
is compile-time only, so this works without `any` casts beyond the index expression.

### Version comparison

`model.ts` exports a comparator: split versions on `.`, compare segment-by-segment numerically,
pad the shorter version with zeros (`1.9.0 < 1.10.0`, `2.1 == 2.1.0` for ordering purposes).
Files sort by this comparator after loading.

### Distributed lock

- ES side identical to other implementations: `op_type=create` on doc id `lock:<migrationKey>`,
poll every 100ms via `setTimeout` until the deadline (`lockTimeoutMinutes`, default 5).
- `tryLock(timeoutMinutes): Promise<boolean>`, `unlock(): Promise<void>` (deletes the ES doc,
releases the local guard in `finally`).
- `doLock()` returns `false` on any error (no ConflictError differentiation yet — same known
TODO as the other implementations).
- The JVM wraps the ES lock in a local `ReentrantLock` for thread safety; Node is
single-threaded, so the local guard is a simple held-flag maintained for API parity and to
make `unlock()` without `tryLock()` an error.

### Error semantics

- CLI exits 1 on any error, message to `stderr` (matching JVM/Python).
- Template validation collects **all** missing variables before throwing.
- `checkMigrationIndexExists` treats 404 as `false`; `createMigrationIndex` treats 409
(already exists) as success.

## CLI

`commander`, identical options to Clikt/Click:

```
--es-url TEXT required
--migrations-dir TEXT required # CLI prepends "file:" before building EsqueConfiguration
--migration-key TEXT required
--migration-user TEXT optional
--lock-timeout-minutes N default 5
--property key=value repeatable; split on first "="
```

`package.json` declares `"bin": {"esque": "dist/cli.js"}`; `cli.ts` starts with
`#!/usr/bin/env node`. Consumers can run `npx esque-ts` or install globally.

## Compatibility Harness Registration

Add to `tests/implementations.yml`:

```yaml
typescript:
invocation: direct
command: ["npm", "exec", "--prefix", "implementations/typescript", "--", "tsx", "implementations/typescript/src/cli.ts"]
```

Running via `tsx` means the harness never depends on a possibly-stale `dist/` build — only
`npm ci` in `implementations/typescript/` is required beforehand (analogous to `uv run`'s
auto-sync for Python). The 17 parametrized scenarios run automatically against the new
implementation, and the non-parametrized `test_cross_implementation_record_equivalency` picks
it up too — taking the CI compat matrix from 35 to 52 tests (17 × 3 + 1).

Per existing project policy, different-language implementations are **not** interchangeable
against the same `migrationKey`; the harness tests each implementation independently against
the shared behavioral contract.

## Versioning

New `.github/version_typescript.sh`, mirroring `version_python.sh` but emitting strict SemVer:

- Exact tag `X.Y.Z` → `X.Y.Z` (release)
- Otherwise (tag + N commits) → `X.Y.Z-dev.N` (prerelease)
- No tags → `0.0.0-dev.<commit count>`

npm constraints honored: exactly three numeric segments, prerelease as `-dev.N` (PEP 440's
fourth-segment `.devN` is invalid SemVer), versions immutable once published. Note:
`X.Y.Z-dev.N` sorts *before* the released `X.Y.Z`; this is harmless because the `dev`
dist-tag and normal semver ranges shield consumers from prereleases, and it matches the
ordering semantics of PEP 440 `.devN`.

## CI

New `typescript` job in `.github/workflows/ci.yml`, following the `python` job pattern:

1. `actions/checkout` (with `fetch-depth: 0` + `fetch-tags` for git describe)
2. `actions/setup-node` — Node 24
3. `npm ci` (in `implementations/typescript/`)
4. Lint/format check: `npx biome ci src/ tests/`
5. Typecheck: `npx tsc --noEmit`
6. Unit tests: `node --import tsx --test tests/` (runs the `.test.ts` files directly, no build
step; same invocation used by the pre-commit hook and the `npm test` script)
7. Build: `npx tsc`
8. Version: patch `package.json` via
`npm version --no-git-tag-version "$(.github/version_typescript.sh)"`
9. Publish: `npm publish --tag dev` for non-release builds; `npm publish` (implicit `latest`)
on GitHub release events. Auth via new `NPM_TOKEN` repository secret.

`compatibility-tests` job: add `needs: typescript` and an `npm ci` step for
`implementations/typescript/` before running pytest.

## Repo Housekeeping

- **Pre-commit hook** (`.githooks/pre-commit`): insert `[3] TypeScript checks` (biome check,
`tsc --noEmit`, `node --test`) and renumber compat tests to `[4]`.
- **Dev container**: add Node 24 (devcontainer feature or apt setup).
- **CLAUDE.md**: add TypeScript to repository structure, build commands, code conventions, and
registered implementations; update compat test count (35 → 52); fix the stale claim that the
Python implementation uses httpx — it uses the official `elasticsearch` client
(`elasticsearch>=9.0.0`).

## Testing Strategy

Matches the Python precedent exactly:

- **Unit tests** (`node:test`, no ES): the four complex areas — version ordering, canonical
checksum, template resolution, integrity verification (with a mock ES client).
- **Integration**: the black-box compatibility harness in `tests/` is the integration layer.
No TypeScript-specific integration tests.

## Out of Scope

- Rollback/undo, FAILED migration records, ConflictError differentiation — existing known
TODOs shared by all implementations.
- CJS build output, Bun/Deno support.
- npm provenance/OIDC publishing (can be added later; requires workflow permission changes).
7 changes: 7 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ RUN curl -s "https://get.sdkman.io" | bash \
# install python things
RUN curl -LsSf https://astral.sh/uv/install.sh | sh

# install node things
ARG NODE_VERSION=24
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.5/install.sh | bash \
&& source ${HOME}/.nvm/nvm.sh \
&& nvm install ${NODE_VERSION} \
&& nvm alias default ${NODE_VERSION}

# install github cli
RUN DEBIAN_FRONTEND=noninteractive \
&& (type -p wget >/dev/null || (apt update && apt install wget -y)) \
Expand Down
10 changes: 7 additions & 3 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ fi

echo "Running pre-commit checks..."

echo "[1/3] JVM: format, lint, test"
echo "[1/4] JVM: format, lint, test"
(cd implementations/jvm && ./gradlew ktfmtCheck detekt test)

echo "[2/3] Python: format, lint, typecheck"
echo "[2/4] Python: format, lint, typecheck"
(cd implementations/python && uv run ruff check src/esque/ && uv run ruff format --check src/esque/ && uv run pyright src/esque/)

echo "[3/3] Compatibility tests"
# Requires a one-time `npm install` in implementations/typescript/ (no auto-sync like uv/gradlew).
echo "[3/4] TypeScript: format, lint, typecheck, test"
(cd implementations/typescript && npm run lint && npm run typecheck && npm test)

echo "[4/4] Compatibility tests"
(cd tests && uv run pytest . -q)

echo "Pre-commit checks passed."
13 changes: 13 additions & 0 deletions .github/version_typescript.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash
# Produces a SemVer version from git tags — mirrors version_python.sh but for npm.
# Exact tag X.Y.Z → X.Y.Z (release); otherwise → X.Y.Z-dev.N (prerelease).

GIT_DESCRIBE=$(git describe --tags 2>/dev/null)

if [[ $GIT_DESCRIBE =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "$GIT_DESCRIBE"
elif [[ $GIT_DESCRIBE =~ ^([0-9]+\.[0-9]+\.[0-9]+)-([0-9]+)-g[0-9a-f]+ ]]; then
echo "${BASH_REMATCH[1]}-dev.${BASH_REMATCH[2]}"
else
echo "0.0.0-dev.$(git rev-list --count HEAD 2>/dev/null || echo 0)"
fi
42 changes: 41 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,44 @@ jobs:
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}

typescript:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: 24
registry-url: https://registry.npmjs.org
- working-directory: implementations/typescript
run: npm ci
- working-directory: implementations/typescript
run: npx biome ci src tests
- working-directory: implementations/typescript
run: npm run typecheck
- working-directory: implementations/typescript
run: npm test
- working-directory: implementations/typescript
run: npm version --no-git-tag-version "$(../../.github/version_typescript.sh)"
- working-directory: implementations/typescript
run: npm run build
- working-directory: implementations/typescript
run: node dist/cli.js --help
- if: github.event_name != 'release'
working-directory: implementations/typescript
run: npm publish --tag dev
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- if: github.event_name == 'release'
working-directory: implementations/typescript
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

compatibility-tests:
runs-on: ubuntu-latest
needs: [jvm, python]
needs: [jvm, python, typescript]
steps:
- uses: actions/checkout@v7
with:
Expand All @@ -78,5 +113,10 @@ jobs:
with:
python-version: "3.14"
- run: uv sync --project implementations/python
- uses: actions/setup-node@v6
with:
node-version: 24
- working-directory: implementations/typescript
run: npm ci
- working-directory: tests
run: uv run pytest . -v
Loading