Skip to content
Draft
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
237 changes: 165 additions & 72 deletions .claude/skills/uts-to-kotlin/SKILL.md

Large diffs are not rendered by default.

628 changes: 628 additions & 0 deletions .claude/skills/uts-to-kotlin/references/objects-mapping.md

Large diffs are not rendered by default.

179 changes: 179 additions & 0 deletions .claude/skills/uts-to-kotlin/scripts/resolve_uts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""Resolve a UTS spec module directory to its ably-java test targets.

Deterministic helper for the uts-to-kotlin skill. Given a UTS spec *module*
directory (a directory directly under .../specification/uts/), it:

- validates the path and the module's tier structure,
- reads uts-package-mapping.json (next to this script's skill dir),
- resolves, per tier, the target output directory and Kotlin package, and
- lists the candidate spec files with their derived Kotlin class names.

Doing this in code (rather than asking the model to eyeball regexes, join
paths, and hand-convert snake_case -> PascalCase every run) keeps the skill's
selection phase byte-for-byte deterministic.

Usage:
resolve_uts.py <module-dir> # validate + resolve + list specs
resolve_uts.py <module-dir> --create NAME # add a mapping entry for this
# module (target base name NAME),
# then resolve

Always prints a single JSON object to stdout. On failure: {"ok": false,
"error": <CODE>, "message": ...} and a non-zero exit.
"""
import argparse
import json
import re
import sys
from pathlib import Path

SKILL_DIR = Path(__file__).resolve().parent.parent
MAPPING = SKILL_DIR / "uts-package-mapping.json"
TIERS = ("unit", "integration", "proxy")


def fail(code, message):
print(json.dumps({"ok": False, "error": code, "message": message}, indent=2))
sys.exit(1)


def class_name(md_path: Path) -> str:
"""objects_batch_test.md -> ObjectsBatchTest; instance.md -> InstanceTest."""
stem = md_path.stem
if stem.endswith("_test"):
stem = stem[: -len("_test")]
return "".join(part.capitalize() for part in stem.split("_") if part) + "Test"


def is_nonspec_doc(name: str) -> bool:
"""README/PLAN/*_SUMMARY markdown are docs, not test specs."""
if re.fullmatch(r"(README|PLAN)\.md", name, re.IGNORECASE):
return True
return name.upper().endswith("_SUMMARY.MD")


def list_specs(base: Path, exclude_proxy: bool = False):
"""List spec .md files under `base`, deterministically.

All exclusions are checked against the path **relative to base**, so they
can't be tripped by an ancestor directory in the checkout path (e.g. a
clone living under some `.../helpers/...` path).
"""
if not base.is_dir():
return []
specs = []
for p in sorted(base.rglob("*.md")):
rel_parts = p.relative_to(base).parts
if "helpers" in rel_parts:
continue
if exclude_proxy and "proxy" in rel_parts:
continue
if is_nonspec_doc(p.name):
continue
specs.append(p)
return specs


def package_for(target_dir: str) -> str:
marker = "src/test/kotlin/"
idx = target_dir.find(marker)
return target_dir[idx + len(marker):].replace("/", ".") if idx != -1 else ""


def main():
ap = argparse.ArgumentParser()
ap.add_argument("module_dir")
ap.add_argument(
"--create",
metavar="NAME",
help="add a mapping for this source module using NAME as the ably-java "
"module base name, then resolve",
)
args = ap.parse_args()

raw = args.module_dir.rstrip("/")
if not re.search(r"/uts/[^/]+$", raw):
fail("NOT_A_UTS_MODULE_PATH",
f"{raw!r} is not a module directory directly under uts/ "
f"(expected .../uts/<module>).")
module_dir = Path(raw)
if not module_dir.is_dir():
fail("DIR_NOT_FOUND", f"{raw!r} does not exist or is not a directory.")
if not (module_dir / "unit").is_dir() and not (module_dir / "integration").is_dir():
fail("NO_TIER_DIRS",
f"{raw!r} has no unit/ or integration/ sub-directory; "
f"not a valid UTS module.")

source_module = module_dir.name

if not MAPPING.is_file():
fail("MAPPING_NOT_FOUND", f"mapping file not found at {MAPPING}")
data = json.loads(MAPPING.read_text())
packages = data.setdefault("packages", {})
test_root = data.get("testRoot", "")

if args.create:
target = args.create
if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", target):
fail("BAD_TARGET_NAME",
f"--create target {target!r} must be a simple module base name "
f"(letters/digits/underscore, e.g. 'liveobjects') so it forms a "
f"valid path and Kotlin package.")
new_entry = {
"unit": f"unit/{target}",
"integration": f"integration/standard/{target}",
"proxy": f"integration/proxy/{target}",
}
# preserve a hand-maintained "notes" pointer when re-creating an existing entry
notes = packages.get(source_module, {}).get("notes")
if notes:
new_entry["notes"] = notes
packages[source_module] = new_entry
MAPPING.write_text(json.dumps(data, indent=2) + "\n")

mapped = source_module in packages
entry = packages.get(source_module, {})

# Per-module translation notes (ably-js -> ably-java type map etc.), declared by
# the module's "notes" field in the mapping (a path relative to this skill dir).
# Read it before translating when present. None when the module declares no notes,
# or the declared file is missing.
notes_rel = entry.get("notes")
notes_path = SKILL_DIR / notes_rel if notes_rel else None
translation_notes = str(notes_path) if (notes_path and notes_path.is_file()) else None

src = {
"unit": module_dir / "unit",
"integration": module_dir / "integration",
"proxy": module_dir / "integration" / "proxy",
}
specs = {
"unit": list_specs(src["unit"]),
"integration": list_specs(src["integration"], exclude_proxy=True),
"proxy": list_specs(src["proxy"]),
}

tiers_out = {}
for tier in TIERS:
target_dir = f"{test_root}/{entry[tier]}" if (mapped and tier in entry) else None
tiers_out[tier] = {
"present": src[tier].is_dir(),
"sourceDir": str(src[tier]),
"targetDir": target_dir,
"package": package_for(target_dir) if target_dir else None,
"specs": [{"file": str(p), "className": class_name(p)} for p in specs[tier]],
}

print(json.dumps({
"ok": True,
"sourceModule": source_module,
"mapped": mapped,
"testRoot": test_root,
"translationNotes": translation_notes,
"tiers": tiers_out,
}, indent=2))


if __name__ == "__main__":
main()
22 changes: 22 additions & 0 deletions .claude/skills/uts-to-kotlin/uts-package-mapping.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"_comment": "Maps each UTS spec module (a dir under specification/uts/) to its target test packages. Output dir = testRoot + '/' + tier entry; Kotlin package = that path after 'src/test/kotlin/' with '/' -> '.'. An optional 'notes' field points (relative to this skill dir) to a per-module ably-js -> ably-java translation reference, read before translating that module. Used by the uts-to-kotlin skill.",
"testRoot": "uts/src/test/kotlin/io/ably/lib/uts",
"packages": {
"realtime": {
"unit": "unit/realtime",
"integration": "integration/standard/realtime",
"proxy": "integration/proxy/realtime"
},
"objects": {
"unit": "unit/liveobjects",
"integration": "integration/standard/liveobjects",
"proxy": "integration/proxy/liveobjects",
"notes": "references/objects-mapping.md"
},
"rest": {
"unit": "unit/rest",
"integration": "integration/standard/rest",
"proxy": "integration/proxy/rest"
}
}
}
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ jobs:
distribution: 'temurin'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3
- run: ./gradlew checkWithCodenarc checkstyleMain checkstyleTest runUnitTests runLiveObjectsUnitTests :uts:test
- run: ./gradlew checkWithCodenarc checkstyleMain checkstyleTest runUnitTests runLiveObjectsUnitTests :uts:runUtsUnitTests
21 changes: 21 additions & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,24 @@ jobs:
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3

- run: ./gradlew runLiveObjectsIntegrationTests

check-uts:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: 'recursive'
persist-credentials: false

- name: Set up the JDK
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
with:
java-version: '17'
distribution: 'temurin'

- name: Set up Gradle
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3

- run: ./gradlew :uts:runUtsIntegrationTests
Loading
Loading