diff --git a/.github/PUBLISHING.md b/.github/PUBLISHING.md index 8f179ad..1c2ef08 100644 --- a/.github/PUBLISHING.md +++ b/.github/PUBLISHING.md @@ -3,6 +3,8 @@ This document explains how to publish ISL artifacts to Maven Central and GitHub Packages. ## Prerequisites +https://intuit-teams.slack.com/archives/C044PJN2NDR/p1749599979127989?thread_ts=1749145553.673049&cid=C044PJN2NDR + ### 1. Maven Central (Sonatype OSSRH) Setup @@ -16,15 +18,30 @@ To publish to Maven Central, you need: ```bash # Generate key -gpg --gen-key +gpg --full-generate-key +# Choose RSA (option 1), key size 3072 or 4096, and set a strong passphrase + +# List keys to get the key ID +gpg --list-secret-keys --keyid-format=long + +# Export the ASCII-armored secret key (replace KEY_ID with your key ID) +gpg --export-secret-keys --armor KEY_ID +``` + +**For Windows (PowerShell):** +```powershell +# Generate key +gpg --full-generate-key -# List keys to get the key ID (last 8 characters of the fingerprint) -gpg --list-keys +# List keys +gpg --list-secret-keys --keyid-format=long -# Export the key (replace KEY_ID with your key ID) -gpg --export-secret-keys KEY_ID | base64 +# Export and copy to clipboard +gpg --export-secret-keys --armor YOUR_KEY_ID | Set-Clipboard ``` +**Note:** You can use the ASCII-armored key directly in the `SIGNING_KEY` secret (starts with `-----BEGIN PGP PRIVATE KEY BLOCK-----`). Base64 encoding is optional. + ### 2. GitHub Repository Secrets Configure the following secrets in your GitHub repository (Settings → Secrets and variables → Actions): @@ -32,8 +49,7 @@ Configure the following secrets in your GitHub repository (Settings → Secrets #### Maven Central Secrets: - `OSSRH_USERNAME`: Your Sonatype JIRA username - `OSSRH_PASSWORD`: Your Sonatype JIRA password -- `SIGNING_KEY_ID`: Your GPG key ID (last 8 characters) -- `SIGNING_KEY`: Your base64-encoded GPG private key +- `SIGNING_KEY`: Your ASCII-armored GPG private key (or base64-encoded) - `SIGNING_PASSWORD`: Your GPG key passphrase #### GitHub Packages: @@ -68,7 +84,7 @@ Configure the following secrets in your GitHub repository (Settings → Secrets **Manual Trigger:** ```bash # Go to Actions → Publish to Maven Central → Run workflow -# Specify the version (e.g., 2.4.20) +# Specify the version (e.g., 1.1.2) ``` ### 3. Performance Benchmarks (`benchmark.yml`) @@ -87,8 +103,8 @@ Configure the following secrets in your GitHub repository (Settings → Secrets 1. **Create a Git tag:** ```bash - git tag -a v2.4.20 -m "Release version 2.4.20" - git push origin v2.4.20 + git tag -a v1.1.0 -m "Release version 1.1.0" + git push origin v1.1.0 ``` 2. **Create a GitHub Release:** @@ -105,7 +121,7 @@ Configure the following secrets in your GitHub repository (Settings → Secrets 1. **Update version in `build.gradle.kts`:** ```kotlin - version = "2.4.20" // Remove -SNAPSHOT for releases + version = "1.1.0" // Remove -SNAPSHOT for releases ``` 2. **Trigger workflow manually:** @@ -118,12 +134,12 @@ Configure the following secrets in your GitHub repository (Settings → Secrets The project uses semantic versioning: `MAJOR.MINOR.PATCH` -- **SNAPSHOT versions** (e.g., `2.4.20-SNAPSHOT`): +- **SNAPSHOT versions** (e.g., `1.1.0-SNAPSHOT`): - Published to Sonatype Snapshots repository - Used for development builds - Automatically overwritten with each publish -- **Release versions** (e.g., `2.4.20`): +- **Release versions** (e.g., `1.1.0`): - Published to Maven Central Staging - Requires manual promotion in Sonatype OSSRH - Immutable once published @@ -167,6 +183,17 @@ signing.secretKeyRingFile=/path/to/.gnupg/secring.gpg ## Troubleshooting +### Issue: "Could not read PGP secret key" or "checksum mismatch" +**Solution:** +1. Use the ASCII-armored format directly (recommended): + ```bash + gpg --export-secret-keys --armor YOUR_KEY_ID + ``` + Copy the entire output (including BEGIN/END markers) into the `SIGNING_KEY` secret +2. Ensure `SIGNING_PASSWORD` matches your GPG key passphrase +3. Verify the key exports correctly before adding to GitHub secrets +4. If using base64 encoding, ensure no extra whitespace or line breaks are introduced + ### Issue: "Could not find signing key" **Solution:** Ensure GPG key is properly exported and base64 encoded. Check `SIGNING_KEY` secret. @@ -186,14 +213,14 @@ Once published to Maven Central, users can add the dependency: ### Gradle (Kotlin DSL) ```kotlin dependencies { - implementation("com.intuit.isl:isl-transform:2.4.20") + implementation("com.intuit.isl:isl-transform:1.1.0") } ``` ### Gradle (Groovy) ```groovy dependencies { - implementation 'com.intuit.isl:isl-transform:2.4.20' + implementation 'com.intuit.isl:isl-transform:1.1.0' } ``` @@ -202,7 +229,7 @@ dependencies { com.intuit.isl isl-transform - 2.4.20 + 1.1.0 ``` diff --git a/.github/workflows/plugin-ci.yml b/.github/workflows/plugin-ci.yml new file mode 100644 index 0000000..4bc0365 --- /dev/null +++ b/.github/workflows/plugin-ci.yml @@ -0,0 +1,64 @@ +name: Plugin CI + +on: + push: + branches: [ main, plugin ] + paths: + - 'plugin/**' + - '.github/workflows/plugin-ci.yml' + pull_request: + branches: [ main ] + paths: + - 'plugin/**' + - '.github/workflows/plugin-ci.yml' + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node-version: [18.x, 20.x] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + cache-dependency-path: plugin/package-lock.json + + - name: Install dependencies + working-directory: plugin + run: npm ci + + - name: Lint code + working-directory: plugin + run: npm run lint || echo "No lint script configured" + continue-on-error: true + + - name: Compile TypeScript + working-directory: plugin + run: npm run compile + + - name: Run tests + working-directory: plugin + run: npm test || echo "No tests configured" + continue-on-error: true + + - name: Package extension + working-directory: plugin + run: npx vsce package + + - name: Upload build artifact + if: matrix.os == 'ubuntu-latest' && matrix.node-version == '18.x' + uses: actions/upload-artifact@v4 + with: + name: isl-language-support-build + path: plugin/*.vsix + retention-days: 7 + diff --git a/.github/workflows/publish-plugin.yml b/.github/workflows/publish-plugin.yml new file mode 100644 index 0000000..49c2aac --- /dev/null +++ b/.github/workflows/publish-plugin.yml @@ -0,0 +1,106 @@ +name: Publish VSCode Extension + +on: + push: + tags: + - 'plugin-v*.*.*' + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g., 1.0.0)' + required: true + islversion: + description: 'Version of ISL to include in the publish (e.g., 1.0.0)' + required: true + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Resolve ISL version + id: isl-version + run: | + if [ -n "${{ github.event.inputs.islversion }}" ]; then + echo "islVersion=${{ github.event.inputs.islversion }}" >> $GITHUB_OUTPUT + elif [[ "${{ github.ref }}" == refs/tags/plugin-v* ]]; then + ISL_VER="${GITHUB_REF#refs/tags/plugin-v}" + echo "islVersion=$ISL_VER" >> $GITHUB_OUTPUT + else + ISL_VER=$(grep "^version=" gradle.properties 2>/dev/null | cut -d'=' -f2 | tr -d '\r' || echo "1.1.0") + echo "islVersion=$ISL_VER" >> $GITHUB_OUTPUT + fi + echo "Using ISL version: $(grep islVersion $GITHUB_OUTPUT | cut -d'=' -f2)" + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'corretto' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build ISL runtime from Maven Central + run: ./gradlew :plugin:build-isl-runtime:buildIslRuntime -PislVersion=${{ steps.isl-version.outputs.islVersion }} --no-daemon + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: plugin/package-lock.json + + - name: Install dependencies + working-directory: plugin + run: npm ci + + - name: Compile TypeScript + working-directory: plugin + run: npm run compile + + - name: Run tests (if available) + working-directory: plugin + run: npm test || echo "No tests configured" + continue-on-error: true + + - name: Package extension + working-directory: plugin + run: npx vsce package + + - name: Publish to VSCode Marketplace + if: startsWith(github.ref, 'refs/tags/plugin-v') + working-directory: plugin + env: + VSCE_PAT: ${{ secrets.VSCE_TOKEN }} + run: npx vsce publish -p $VSCE_PAT + + - name: Publish to Open VSX Registry + if: startsWith(github.ref, 'refs/tags/plugin-v') + working-directory: plugin + env: + OVSX_PAT: ${{ secrets.OVSX_TOKEN }} + run: npx ovsx publish -p $OVSX_PAT + continue-on-error: true + + - name: Upload VSIX artifact + uses: actions/upload-artifact@v4 + with: + name: isl-language-support-vsix + path: plugin/*.vsix + retention-days: 90 + + - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/plugin-v') + uses: softprops/action-gh-release@v1 + with: + files: plugin/*.vsix + generate_release_notes: true + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2e66cda..b45dda0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,62 +1,336 @@ -name: Publish to Maven Central +name: Publish to Maven Central (Snapshot/Release) on: release: types: [created] + push: + branches: [ main, master ] workflow_dispatch: inputs: version: - description: 'Version to publish (e.g., 2.4.20)' - required: true + description: 'Version to publish (e.g., 1.1.0)' + required: false type: string jobs: - publish: - name: Publish to Maven Central + publish-snapshot: + name: Publish Snapshot runs-on: ubuntu-latest permissions: contents: read packages: write - + + # Run on pushes to main/master branches + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') + steps: - name: Checkout code uses: actions/checkout@v4 - + - name: Set up JDK 21 uses: actions/setup-java@v4 with: java-version: '21' distribution: 'corretto' - + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 - + with: + gradle-version: wrapper + + - name: Verify OSSRH credentials + env: + MAVEN_CENTRAL_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + run: | + if [ -z "$MAVEN_CENTRAL_USERNAME" ]; then + echo "❌ MAVEN_CENTRAL_USERNAME is not set!" + exit 1 + fi + if [ -z "$MAVEN_CENTRAL_PASSWORD" ]; then + echo "❌ MAVEN_CENTRAL_PASSWORD is not set!" + exit 1 + fi + echo "✓ OSSRH credentials are configured" + + - name: Determine and update version + id: version + run: | + # Read current version from gradle.properties + CURRENT_VERSION=$(grep "^version=" gradle.properties | cut -d'=' -f2 | tr -d '\r') + echo "Current version: $CURRENT_VERSION" + + # Automatic increment for snapshots + if [[ "$CURRENT_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-SNAPSHOT)?$ ]]; then + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + NEW_PATCH=$((PATCH + 1)) + NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}-SNAPSHOT" + echo "Incrementing patch version: $CURRENT_VERSION -> $NEW_VERSION" + else + echo "❌ Unable to parse version: $CURRENT_VERSION" + exit 1 + fi + + # Update gradle.properties with new version + sed -i "s/version=.*/version=$NEW_VERSION/" gradle.properties + echo "Updated gradle.properties with version: $NEW_VERSION" + + # Output the new version for use in other steps + echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT + + - name: Build artifacts + run: ./gradlew build --no-daemon --stacktrace -x test + + - name: Publish Snapshot to Maven Central + env: + MAVEN_CENTRAL_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.OSSRH_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.OSSRH_PASSWORD }} + run: ./gradlew publishToMavenCentral --no-daemon --stacktrace --info + + publish-release: + name: Publish Release to Maven Central + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'corretto' + - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Update version - if: github.event.inputs.version != '' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + gradle-version: wrapper + + - name: Determine and update version + id: version run: | - sed -i 's/version = ".*"/version = "${{ github.event.inputs.version }}"/' build.gradle.kts + # Read current version from gradle.properties + CURRENT_VERSION=$(grep "^version=" gradle.properties | cut -d'=' -f2 | tr -d '\r') + echo "Current version: $CURRENT_VERSION" + + if [ "${{ github.event.inputs.version }}" != "" ]; then + # Manual version specified + NEW_VERSION="${{ github.event.inputs.version }}" + echo "Using manually specified version: $NEW_VERSION" + elif [ "${{ github.event_name }}" = "release" ]; then + # Release event - create release version (remove -SNAPSHOT if present) + NEW_VERSION=$(echo "$CURRENT_VERSION" | sed 's/-SNAPSHOT$//') + echo "Creating release version: $NEW_VERSION" + else + # Automatic increment for snapshots + if [[ "$CURRENT_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-SNAPSHOT)?$ ]]; then + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + NEW_PATCH=$((PATCH + 1)) + NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}-SNAPSHOT" + echo "Incrementing patch version: $CURRENT_VERSION -> $NEW_VERSION" + else + echo "❌ Unable to parse version: $CURRENT_VERSION" + exit 1 + fi + fi + + # Update gradle.properties with new version + sed -i "s/version=.*/version=$NEW_VERSION/" gradle.properties + echo "Updated gradle.properties with version: $NEW_VERSION" + + # Output the new version for use in other steps + echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT - name: Build artifacts - run: ./gradlew build --no-daemon --stacktrace - - - name: Run tests - run: ./gradlew test --no-daemon --stacktrace + run: ./gradlew build --no-daemon --stacktrace -x test + # temp ignore to speed up the build + # - name: Run tests + # run: ./gradlew test --no-daemon --stacktrace + + - name: Setup PGP key for signing + run: | + # Write the PGP key to environment variable + # The key can be either base64-encoded or ASCII-armored + if echo "${{ secrets.SIGNING_KEY }}" | grep -q "BEGIN PGP"; then + # Key is already ASCII-armored + echo "SIGNING_KEY<> $GITHUB_ENV + echo "${{ secrets.SIGNING_KEY }}" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + else + # Key is base64-encoded, decode it + echo "${{ secrets.SIGNING_KEY }}" | tr -d '\n\r' | base64 --decode > signing_key.asc + echo "SIGNING_KEY<> $GITHUB_ENV + cat signing_key.asc >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + rm -f signing_key.asc + fi + - name: Publish to Maven Central env: - OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} - SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} + MAVEN_CENTRAL_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.OSSRH_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.OSSRH_PASSWORD }} SIGNING_KEY: ${{ secrets.SIGNING_KEY }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - run: ./gradlew publish --no-daemon --stacktrace - - - name: Publish to GitHub Packages + run: ./gradlew publishToMavenCentral --no-daemon --stacktrace --info + + - name: Generate release notes + run: | + VERSION="${{ steps.version.outputs.version }}" + PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || true) + if [ -z "$PREV_TAG" ]; then + COMMITS=$(git log --pretty=format:"- %s (%h)" | head -100) + else + COMMITS=$(git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" | head -100) + fi + { + echo "## What's changed" + echo "" + echo "$COMMITS" + echo "" + echo "---" + echo "" + echo "**Maven Central:** \`com.intuit.isl:isl-transform:${VERSION}\`, \`isl-validation\`, \`isl-cmd\`, \`isl-test\`" + } > release_notes.md + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.version.outputs.version }} + name: v${{ steps.version.outputs.version }} + body_path: release_notes.md + generate_release_notes: true + make_latest: true + skip_if_tag_exists: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Update CHANGELOG and push + run: | + VERSION="${{ steps.version.outputs.version }}" + DATE=$(date +%Y-%m-%d) + PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || true) + if [ -z "$PREV_TAG" ]; then + COMMITS=$(git log --pretty=format:"- %s (%h)" | head -80) + else + COMMITS=$(git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" | head -80) + fi + HEADER="## [${VERSION}] - ${DATE}" + SECTION="${HEADER}"$'\n\n'"${COMMITS}"$'\n\n' + if [ -f CHANGELOG.md ]; then + echo "$SECTION$(cat CHANGELOG.md)" > CHANGELOG.md + else + echo "# Changelog" > CHANGELOG.md + echo "" >> CHANGELOG.md + echo "All notable changes to this project will be documented in this file." >> CHANGELOG.md + echo "" >> CHANGELOG.md + echo "$SECTION" >> CHANGELOG.md + fi + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add CHANGELOG.md + git diff --staged --quiet || (git commit -m "chore: update CHANGELOG for v${VERSION}" && git push origin HEAD:${{ github.ref_name }}) env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gradlew publishAllPublicationsToGitHubPackagesRepository --no-daemon --stacktrace - continue-on-error: true + + publish-manual-snapshot: + name: Publish Manual Snapshot + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + # Run on manual dispatch without version specified (snapshot publishing) + if: github.event_name == 'workflow_dispatch' && github.event.inputs.version == '' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'corretto' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + gradle-version: wrapper + + - name: Verify OSSRH credentials + env: + MAVEN_CENTRAL_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + run: | + if [ -z "$MAVEN_CENTRAL_USERNAME" ]; then + echo "❌ MAVEN_CENTRAL_USERNAME is not set!" + exit 1 + fi + if [ -z "$MAVEN_CENTRAL_PASSWORD" ]; then + echo "❌ MAVEN_CENTRAL_PASSWORD is not set!" + exit 1 + fi + echo "✓ OSSRH credentials are configured" + + - name: Determine and update version + id: version + run: | + # Read current version from gradle.properties + CURRENT_VERSION=$(grep "^version=" gradle.properties | cut -d'=' -f2 | tr -d '\r') + echo "Current version: $CURRENT_VERSION" + + # Automatic increment for snapshots + if [[ "$CURRENT_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-SNAPSHOT)?$ ]]; then + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + NEW_PATCH=$((PATCH + 1)) + NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}-SNAPSHOT" + echo "Incrementing patch version: $CURRENT_VERSION -> $NEW_VERSION" + else + echo "❌ Unable to parse version: $CURRENT_VERSION" + exit 1 + fi + + # Update gradle.properties with new version + sed -i "s/version=.*/version=$NEW_VERSION/" gradle.properties + echo "Updated gradle.properties with version: $NEW_VERSION" + + # Output the new version for use in other steps + echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT + + - name: Build artifacts + run: ./gradlew build --no-daemon --stacktrace -x test + + - name: Publish Snapshot to Maven Central + env: + MAVEN_CENTRAL_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.OSSRH_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.OSSRH_PASSWORD }} + run: ./gradlew publishToMavenCentral --no-daemon --stacktrace --info diff --git a/.gitignore b/.gitignore index 4ee5884..5396ba5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,8 @@ out/ dist/ node_modules/ .vscode-test/ -*.vsix +# Allow plugin vsix packages +!plugin/*.vsix # Gradle .gradle/ @@ -110,4 +111,16 @@ jacoco.exec *~ # Kotlin -.kotlin/ \ No newline at end of file +.kotlin/ + + +# Outputs from tests +output-*.json + +# Test files for extensions +.islextensions +test-extensions.isl + + +# Plugin +# *.vsix diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2eac930 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +## [1.1.1] - 2026-02-25 + +- chore: update CHANGELOG for v1.1.1-SNAPSHOT (f77371b) +- Added support for isl-level testing (#10) (db7ec6a) +- chore: update CHANGELOG for v1.1.0 (f289549) + +## [1.1.1-SNAPSHOT] - 2026-02-13 + +- prepare for publishing (e4d2865) +- added link to playground (4ed5902) +- Playground (#8) (14dc5ae) +- Updated docs (#7) (40030d6) +- Updated benchmarks (#6) (4067b24) +- updated benchmarks (#4) (80efee8) +- Docs & benchmark improvements (#3) (55dce49) +- fixed logo (80a0c0a) +- scss (2e9ffc8) +- updated docs (34816e0) +- Updated docs (#2) (9b15c83) +- working on making the build work (#1) (0894063) +- Initial commit - ISL (Intuitive Scripting Language) (f55bd14) + +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/JAVADOC_REPORT.md b/JAVADOC_REPORT.md new file mode 100644 index 0000000..e5f4431 --- /dev/null +++ b/JAVADOC_REPORT.md @@ -0,0 +1,52 @@ +# JavaDoc Warnings Report + +## Summary + +JavaDoc warnings analysis for the ISL project. + +## Excluded Files + +The following ANTLR-generated files have been excluded from JavaDoc generation: + +1. `com/intuit/isl/antlr/IslLexer.java` - ANTLR-generated lexer (all warnings excluded) +2. `com/intuit/isl/antlr/IslParser.java` - ANTLR-generated parser (all warnings excluded) + +**Configuration:** Added to `isl-transform/build.gradle.kts`: +```kotlin +tasks.withType { + exclude("com/intuit/isl/antlr/**") + options.encoding = "UTF-8" +} +``` + +## Warnings Found (Before Exclusion) + +### IslLexer.java +- **Total warnings:** ~100+ warnings +- **Type:** All "no comment" warnings for: + - Public classes + - Public/protected fields + - Public static final fields + - Enum constants + - Public methods + +### IslParser.java +- **Total warnings:** Similar pattern to IslLexer.java +- **Type:** All "no comment" warnings for generated code + +## Other Java Files in Project + +The following Java files exist in the project (excluding ANTLR generated files): + +- All files in `com/intuit/isl/antlr/` directory are ANTLR-generated and excluded + +## Status + +✅ **RESOLVED:** All ANTLR-generated files are now excluded from JavaDoc generation. +✅ **NO OTHER WARNINGS:** After exclusion, no JavaDoc warnings remain. + +## Notes + +- ANTLR-generated files should not be documented with JavaDoc as they are auto-generated +- The exclusion pattern `com/intuit/isl/antlr/**` covers all ANTLR-generated files +- JavaDoc encoding is set to UTF-8 for proper character handling diff --git a/build.gradle.kts b/build.gradle.kts index 78295bd..a766a74 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,11 @@ plugins { kotlin("jvm") version "2.1.10" apply false id("org.jetbrains.kotlin.plugin.allopen") version "2.1.10" apply false + id("com.vanniktech.maven.publish") version "0.35.0" apply false } group = "com.intuit.isl" -version = "2.4.20-SNAPSHOT" +version = project.property("version") as String allprojects { repositories { @@ -12,13 +13,19 @@ allprojects { } } +allprojects { + group = "com.intuit.isl" + version = rootProject.version +} + subprojects { apply(plugin = "org.jetbrains.kotlin.jvm") apply(plugin = "jacoco") - apply(plugin = "maven-publish") + // Apply publishing plugin to library modules only - group = "com.intuit.isl" - version = rootProject.version + if (name in listOf("isl-transform", "isl-validation", "isl-cmd", "isl-test")) { + apply(plugin = "com.vanniktech.maven.publish") + } extensions.configure { sourceCompatibility = JavaVersion.VERSION_21 @@ -31,9 +38,9 @@ subprojects { } tasks.withType { - kotlinOptions { - jvmTarget = "21" - freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) + freeCompilerArgs.addAll("-Xjsr305=strict") } } @@ -51,81 +58,82 @@ subprojects { html.required.set(true) } } +} - configure { - publications { - create("maven") { - from(components["java"]) - - pom { - name.set(project.name) - description.set("ISL - JSON transformation scripting language") - url.set("https://github.com/intuit/isl") - - licenses { - license { - name.set("Apache License 2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0") - } - } - - developers { - developer { - id.set("intuit") - name.set("Intuit Inc.") - email.set("opensource@intuit.com") - } - } - - scm { - url.set("https://github.com/intuit/isl") - connection.set("scm:git:https://github.com/intuit/isl.git") - developerConnection.set("scm:git:https://github.com/intuit/isl.git") - } - } +// ---- Publishing (Maven Central + signing) ---- +// Maven Central: plugin build service needs mavenCentralUsername/Password. Set via: +// - Env: MAVEN_CENTRAL_USERNAME, MAVEN_CENTRAL_PASSWORD (or OSSRH_*); CI should also set ORG_GRADLE_PROJECT_mavenCentralUsername/Password. +// - Root gradle.properties: mavenCentralUsername, mavenCentralPassword (we set project.ext from these). +// Signing: env SIGNING_KEY (or SIGNING_KEY_FILE) and SIGNING_PASSWORD (or SIGNING_PASSWORD_FILE) + +val publishModules = listOf("isl-transform", "isl-validation", "isl-cmd", "isl-test") + +configure(subprojects.filter { it.name in publishModules }) { + afterEvaluate { + val mavenUser = System.getenv("MAVEN_CENTRAL_USERNAME") ?: System.getenv("OSSRH_USERNAME") ?: rootProject.findProperty("mavenCentralUsername")?.toString() + val mavenPass = System.getenv("MAVEN_CENTRAL_PASSWORD") ?: System.getenv("OSSRH_PASSWORD") ?: rootProject.findProperty("mavenCentralPassword")?.toString() + val hasMavenCreds = !mavenUser.isNullOrBlank() && !mavenPass.isNullOrBlank() + if (hasMavenCreds) { + project.ext.set("mavenCentralUsername", mavenUser) + project.ext.set("mavenCentralPassword", mavenPass) + } + + val signKey = System.getenv("SIGNING_KEY") ?: System.getenv("SIGNING_KEY_FILE")?.let { file(it).readText(Charsets.UTF_8).trim() } + val signPass = System.getenv("SIGNING_PASSWORD") ?: System.getenv("SIGNING_PASSWORD_FILE")?.let { file(it).readText(Charsets.UTF_8).trim() } ?: "" + val hasSigning = !signKey.isNullOrBlank() + + if (hasMavenCreds && hasSigning) { + apply(plugin = "signing") + extensions.configure { + val keyContent = signKey!!.trim() + val key = if (keyContent.startsWith("-----BEGIN PGP")) keyContent + else String(java.util.Base64.getDecoder().decode(keyContent.replace(Regex("\\s+"), ""))).trim() + useInMemoryPgpKeys(key.replace("\r\n", "\n").replace("\r", "\n").trim().let { if (it.endsWith("\n")) it else "$it\n" }, signPass) } } - repositories { - mavenLocal() - - // Maven Central via Sonatype OSSRH - maven { - name = "OSSRH" - url = uri( - if (version.toString().endsWith("SNAPSHOT")) - "https://oss.sonatype.org/content/repositories/snapshots/" - else - "https://oss.sonatype.org/service/local/staging/deploy/maven2/" - ) - credentials { - username = System.getenv("OSSRH_USERNAME") ?: findProperty("ossrhUsername") as String? - password = System.getenv("OSSRH_PASSWORD") ?: findProperty("ossrhPassword") as String? + extensions.configure { + coordinates(group.toString(), project.name, version.toString()) + pom { + name.set("ISL") + description.set("ISL - JSON transformation scripting language") + url.set("https://github.com/intuit/isl") + licenses { license { name.set("Apache License 2.0"); url.set("https://www.apache.org/licenses/LICENSE-2.0") } } + developers { developer { id.set("intuit"); name.set("Intuit Inc."); email.set("opensource@intuit.com") } } + scm { + url.set("https://github.com/intuit/isl") + connection.set("scm:git:https://github.com/intuit/isl.git") + developerConnection.set("scm:git:https://github.com/intuit/isl.git") } } - - // GitHub Packages - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/intuit/isl") - credentials { - username = System.getenv("GITHUB_ACTOR") ?: findProperty("githubActor") as String? - password = System.getenv("GITHUB_TOKEN") ?: findProperty("githubToken") as String? - } + if (hasMavenCreds) { + publishToMavenCentral() + if (hasSigning) signAllPublications() } } } - - // Signing configuration for Maven Central - if (System.getenv("SIGNING_KEY_ID") != null || project.hasProperty("signing.keyId")) { - apply(plugin = "signing") - extensions.configure { - if (System.getenv("SIGNING_KEY") != null) { - val signingKey = System.getenv("SIGNING_KEY") - val signingPassword = System.getenv("SIGNING_PASSWORD") - useInMemoryPgpKeys(signingKey, signingPassword) - } - sign(extensions.getByType().publications["maven"]) +} + +tasks.register("buildIslRuntimeLocal") { + group = "build" + description = "Build isl-cmd fat JAR and copy to plugin/lib for extension use" + dependsOn(":isl-cmd:shadowJar") + from(project(":isl-cmd").tasks.named("shadowJar").map { (it as org.gradle.api.tasks.bundling.Jar).archiveFile }) + into(file("plugin/lib")) + rename { "isl-cmd-all.jar" } +} + +tasks.register("publishToMavenCentral") { + group = "publishing" + description = "Publish all modules to Maven Central" + publishModules.forEach { name -> + project(name).tasks.findByName("publishToMavenCentral")?.let { dependsOn(it) } + } + doFirst { + if (taskDependencies.getDependencies(this).isEmpty()) { + throw GradleException( + "Maven Central credentials required: set MAVEN_CENTRAL_USERNAME and MAVEN_CENTRAL_PASSWORD (or OSSRH_*), or mavenCentralUsername/Password in root gradle.properties. For local only: gradlew publishToMavenLocal" + ) } } } diff --git a/docs/_data/navigation.yml b/docs/_data/navigation.yml index 88cbf12..7703e65 100644 --- a/docs/_data/navigation.yml +++ b/docs/_data/navigation.yml @@ -84,12 +84,6 @@ docs: url: /dev/hosting - title: "Performance Benchmarks" url: /dev/benchmark-report - - title: "Release Process" - url: /dev/release - - title: "Contributing" - url: /dev/contributing - - title: "Playground Integration" - url: /dev/playground-integration - title: More children: @@ -99,6 +93,10 @@ docs: url: /changelog - title: "Roadmap" url: /roadmap + - title: "Contributing" + url: /dev/contributing + - title: "Release Process" + url: /dev/release - title: "Support" url: /support diff --git a/docs/assets/css/main.scss b/docs/assets/css/main.scss index b0c0d08..5379cce 100644 --- a/docs/assets/css/main.scss +++ b/docs/assets/css/main.scss @@ -95,6 +95,14 @@ pre[class*="language-"] { } } +/* Override Prism colors - change from #db4c69 to #9cdcfe */ +// code.highlighter-rouge.languange-plaintext, +body { + :not(pre) > code[class*="language-"] { + color: #9cdcfe !important; + } +} + /* Override Prism colors - change from #db4c69 to #9cdcfe */ code.highlighter-rouge.languange-plaintext, body :not(pre) > code[class*="language-"] { diff --git a/docs/assets/js/playground-auto-buttons.js b/docs/assets/js/playground-auto-buttons.js index 0e52a95..499f263 100644 --- a/docs/assets/js/playground-auto-buttons.js +++ b/docs/assets/js/playground-auto-buttons.js @@ -21,6 +21,19 @@ } } + /** + * Checks if a code block is explicitly marked as JSON + */ + function hasJsonClass(preElement) { + // Check pre element classes + const preClasses = preElement.className || ''; + if (preClasses.includes('language-json') || preClasses.includes('highlighter-json')) { + return true; + } + + return false; + } + /** * Checks if a code block is explicitly marked as ISL */ @@ -103,26 +116,13 @@ iterations++; // Check if this is a
 with JSON code
-      if (currentElement.tagName === 'PRE') {
+      if (currentElement.tagName === 'PRE' && hasJsonClass(currentElement)) {
         const codeElement = currentElement.querySelector('code');
         if (codeElement) {
           const text = codeElement.textContent.trim();
           // Simple check if it looks like JSON
           if (text.startsWith('{') || text.startsWith('[')) {
-            // Check if there's an "Input JSON" label before this
-            let labelElement = currentElement.previousElementSibling;
-            let labelChecks = 0;
-            
-            while (labelElement && labelChecks < 3) {
-              labelChecks++;
-              const labelText = labelElement.textContent || '';
-              
-              if (/\w*input\w*/i.test(labelText)) {
-                return text;
-              }
-              
-              labelElement = labelElement.previousElementSibling;
-            }
+            return text;
           }
         }
       }
diff --git a/docs/changelog.md b/docs/changelog.md
index d892c68..6c45667 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -3,7 +3,7 @@ title: Changelog
 nav_order: 100
 ---
 
-## [Unreleased] - Performance Benchmarks
+## [1.1.0] - First public release
 
 ### Performance Metrics
 
@@ -85,5 +85,4 @@ Added comprehensive JMH (Java Microbenchmark Harness) benchmarking framework to
 
 ---
 
-**2.4.20** - First public release
 
diff --git a/docs/cli.md b/docs/cli.md
index ee0c6a3..6555d9a 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -97,7 +97,7 @@ For development, you can run directly with Gradle:
 
 ## Basic Usage
 
-The ISL CLI has three main commands:
+The ISL CLI has four main commands:
 
 ### 1. Transform Command
 
@@ -138,7 +138,38 @@ isl validate script.isl
 # If invalid, shows error details and exit code 1
 ```
 
-### 3. Info Command
+### 3. Test Command
+
+Run ISL unit tests. Discovers `.isl` files containing `@test` or `@setup` annotations and executes them.
+
+```bash
+# Run tests in current directory (default: **/*.isl)
+isl test
+
+# Run tests in a specific path (directory, file, or glob)
+isl test tests/
+isl test tests/sample.isl
+
+# Custom glob pattern
+isl test tests/ --glob "**/*.test.isl"
+
+# Write results to JSON file
+isl test -o results.json
+```
+
+**Options:**
+
+| Option | Description |
+|--------|-------------|
+| `path` | Directory, file, or glob to search (default: current directory) |
+| `--glob PATTERN` | Glob to filter files when path is a directory (default: `**/*.isl`) |
+| `-o, --output FILE` | Write test results to JSON file |
+
+Exit code: 0 if all tests pass, 1 if any fail.
+
+See [Unit Testing](../ext/unit-testing/index.md) for writing tests, assertions, and loading fixtures.
+
+### 4. Info Command
 
 Display version and system information:
 
@@ -176,6 +207,27 @@ isl info
 debug=true isl transform script.isl -i input.json
 ```
 
+### Logging from ISL Scripts
+
+When running transforms or tests from the CLI, you can log messages from your ISL scripts:
+
+```isl
+@.Log.Info("Processing started")
+@.Log.Info("Item count:", $count)
+@.Log.Warn("Unexpected value:", $value)
+@.Log.Error("Failed:", $error)
+@.Log.Debug("Debug info")   // Only outputs when -Ddebug=true
+```
+
+| Function | Output | When |
+|----------|--------|------|
+| `@.Log.Debug(...)` | stdout | Only when `-Ddebug=true` |
+| `@.Log.Info(...)` | stdout | Always |
+| `@.Log.Warn(...)` | stderr | Always |
+| `@.Log.Error(...)` | stderr | Always |
+
+All functions accept multiple arguments (strings, variables, expressions); they are joined with spaces. JSON objects are pretty-printed.
+
 ## Working with Input Data
 
 ### Using Input Files
diff --git a/docs/examples/index.md b/docs/examples/index.md
index 4d45661..223088e 100644
--- a/docs/examples/index.md
+++ b/docs/examples/index.md
@@ -6,8 +6,6 @@ description: "Common ISL transformation patterns for JSON data manipulation incl
 excerpt: "Common ISL transformation patterns for JSON data manipulation including field mapping, array transformations, nested objects, and more."
 ---
 
-# ISL Transformation Examples
-
 This guide demonstrates common JSON transformation patterns using ISL. Each example shows how to handle typical data transformation scenarios.
 
 ## Table of Contents
@@ -29,7 +27,7 @@ This guide demonstrates common JSON transformation patterns using ISL. Each exam
 
 **Use Case:** Copy and rename fields from input to output
 
-**Documentation:** [Variables](/isl/language/variables/) | [Objects](/isl/language/objects/)
+**Documentation:** [Variables](/isl/language/variables/), [Objects](/isl/language/objects/)
 
 **Input:**
 ```json
@@ -64,7 +62,7 @@ This guide demonstrates common JSON transformation patterns using ISL. Each exam
 
 **Use Case:** Rename multiple fields and reorganize structure
 
-**Documentation:** [Objects](/isl/language/objects/) | [Variables](/isl/language/variables/)
+**Documentation:** [Objects](/isl/language/objects/), [Variables](/isl/language/variables/)
 
 **Input:**
 ```json
@@ -106,7 +104,7 @@ This guide demonstrates common JSON transformation patterns using ISL. Each exam
 
 **Use Case:** Transform each item in an array
 
-**Documentation:** [Loops](/isl/language/loops/) | [Built-in Modifiers](/isl/language/modifiers/)
+**Documentation:** [Loops](/isl/language/loops/), [Built-in Modifiers](/isl/language/modifiers/)
 
 **Input:**
 ```json
@@ -159,7 +157,7 @@ or using `|map ( )`
 
 **Use Case:** Flatten nested structure into flat object
 
-**Documentation:** [Objects](/isl/language/objects/) | [Variables](/isl/language/variables/)
+**Documentation:** [Objects](/isl/language/objects/), [Variables](/isl/language/variables/)
 
 **Input:**
 ```json
@@ -203,7 +201,7 @@ or using `|map ( )`
 
 **Use Case:** Create nested structure from flat data
 
-**Documentation:** [Objects](/isl/language/objects/) | [Variables](/isl/language/variables/)
+**Documentation:** [Objects](/isl/language/objects/), [Variables](/isl/language/variables/)
 
 **Input:**
 ```json
@@ -252,7 +250,7 @@ or using `|map ( )`
 
 **Use Case:** Include fields based on conditions
 
-**Documentation:** [Conditions](/isl/language/conditions/) | [Objects](/isl/language/objects/)
+**Documentation:** [Conditions](/isl/language/conditions/), [Objects](/isl/language/objects/)
 
 **Input:**
 ```json
@@ -290,7 +288,7 @@ or using `|map ( )`
 
 **Use Case:** Convert array of key-value pairs to object
 
-**Documentation:** [Functions](/isl/language/functions/) | [Built-in Modifiers](/isl/language/modifiers/) | [Loops](/isl/language/loops/)
+**Documentation:** [Functions](/isl/language/functions/), [Built-in Modifiers](/isl/language/modifiers/), [Loops](/isl/language/loops/)
 
 **Input:**
 ```json
@@ -306,12 +304,15 @@ or using `|map ( )`
 **ISL Transformation:**
 ```isl
 fun run($input) {
-  $result: $input | to.object;  // convert any [{key/value}] to object
+  $result: $input.attributes | to.object;  // convert any [{key/value}] to object
   
-  // alternatively use the foreach
-  foreach $attr in $input.attributes
-    $result.`$attr.key`: $attr.value;
-  endfor
+  // alternatively use the foreach - not as efficient
+  // foreach $attr in $input.attributes
+  //  $result = {
+  //      ...$result,
+  //      `${ $attr.key }`: $attr.value
+  //  }
+  // endfor
   
   return $result;
 }
@@ -332,7 +333,7 @@ fun run($input) {
 
 **Use Case:** Convert object to a Key/Value array
 
-**Documentation:** [Functions](/isl/language/functions/) | [Built-in Modifiers](/isl/language/modifiers/)
+**Documentation:** [Functions](/isl/language/functions/), [Built-in Modifiers](/isl/language/modifiers/)
 
 **Input:**
 ```json
@@ -381,7 +382,7 @@ fun run($input) {
 
 **Use Case:** Filter array and transform matching items
 
-**Documentation:** [Loops](/isl/language/loops/) | [Built-in Modifiers](/isl/language/modifiers/) | [Conditions](/isl/language/conditions/)
+**Documentation:** [Loops](/isl/language/loops/), [Built-in Modifiers](/isl/language/modifiers/), [Conditions](/isl/language/conditions/)
 
 **Input:**
 ```json
@@ -422,7 +423,7 @@ fun run($input) {
 
 **Use Case:** Combine data from multiple input sources
 
-**Documentation:** [Objects](/isl/language/objects/) | [Variables](/isl/language/variables/)
+**Documentation:** [Objects](/isl/language/objects/), [Variables](/isl/language/variables/)
 
 **Input:**
 ```json
@@ -507,7 +508,7 @@ fun run($input) {
 
 **Use Case:** Real-world transformation of an order from external API format to internal format
 
-**Documentation:** [Loops](/isl/language/loops/) | [Math Expressions](/isl/language/math/) | [Dates & Times](/isl/types/dates/) | [Built-in Modifiers](/isl/language/modifiers/)
+**Documentation:** [Loops](/isl/language/loops/), [Math Expressions](/isl/language/math/), [Dates & Times](/isl/types/dates/), [Built-in Modifiers](/isl/language/modifiers/)
 
 **Input:**
 ```json
@@ -598,7 +599,7 @@ fun run($input) {
 
 ## Date Processing
 
-**Documentation:** [Dates & Times](/isl/types/dates/) | [Built-in Modifiers](/isl/language/modifiers/)
+**Documentation:** [Dates & Times](/isl/types/dates/), [Built-in Modifiers](/isl/language/modifiers/)
 
 ### Parsing Dates with Multiple Formats
 
@@ -741,7 +742,7 @@ fun run($input) {
 
 **Use Case:** Convert dates from one format to another
 
-**Documentation:** [Dates & Times](/isl/types/dates/) | [Built-in Modifiers](/isl/language/modifiers/)
+**Documentation:** [Dates & Times](/isl/types/dates/), [Built-in Modifiers](/isl/language/modifiers/)
 
 **Input:**
 ```json
diff --git a/docs/ext/unit-testing/annotations.md b/docs/ext/unit-testing/annotations.md
index 05e015d..63d6a1b 100644
--- a/docs/ext/unit-testing/annotations.md
+++ b/docs/ext/unit-testing/annotations.md
@@ -5,33 +5,116 @@ grand_parent: Advanced Topics
 nav_order: 3
 ---
 
-## Test
+# Test Annotations
 
-To create a unit test, we need to utilise the `@test` annotation to denote that the function
-is a unit test.
+## @test
 
-```kotlin
+Marks a function as a unit test. The function runs as a test case when tests are executed.
+
+### Basic form
+
+```isl
 @test
 fun test_addNumbers() {
- ...
+    $sum: 1 + 2;
+    @.Assert.equal(3, $sum);
+}
+```
+
+When no parameters are given, the function name is used as the test name.
+
+### Custom display name
+
+```isl
+@test("Addition of positive numbers")
+fun test_addNumbers() {
+    @.Assert.equal(3, 1 + 2);
 }
 ```
 
-In the test function, we can write regular ISL code, along with additional functions specific for testing capabilities.
+### Name and group
+
+```isl
+@test("Check total", "math")
+fun test_total() {
+    @.Assert.equal(10, 5 + 5);
+}
+```
+
+### Object form
+
+```isl
+@test({ name: "Grouped test", group: "math" })
+fun test_grouped() {
+    $value: 30;
+    @.Assert.equal(30, $value);
+}
+```
+
+Use the object form when you need both a custom name and group.
+
+### Parameter summary
+
+| Form | Example | Result |
+|------|---------|--------|
+| No params | `@test` | Test name = function name |
+| String | `@test("My test")` | Test name = "My test" |
+| Two strings | `@test("Name", "group")` | Custom name and group |
+| Object | `@test({ name: "x", group: "y" })` | Custom name and group |
+
+## @setup
 
-## Setup
+Marks a function to run **before each test** in the same file. Use it for shared initialization.
 
-We can also use the `@setup` annotation to denote actions we wish to repeat for each unit tests (e.g. setup of something specific)
+- At most **one** `@setup` function per file
+- Runs before every `@test` function in that file
+- Does not run as a test itself
 
-```kotlin
+```isl
 @setup
 fun setup() {
- // Perform actions required before executing test
+    $sharedState: { count: 0 };
+    // This runs before each test
 }
 
+@test
+fun test_first() {
+    // setup() already ran
+    @.Assert.equal(1, 1);
+}
 
 @test
-fun test_addNumbers() {
- ...
+fun test_second() {
+    // setup() runs again before this test
+    @.Assert.equal(2, 2);
+}
+```
+
+## File structure
+
+A typical test file:
+
+```isl
+// Optional: imports
+import Helper from "../helper.isl";
+
+@setup
+fun setup() {
+    // Runs before each test
+}
+
+@test
+fun test_basic() {
+    // Test code
+}
+
+@test("Custom name")
+fun test_withName() {
+    // Test code
+}
+
+@test({ name: "Edge case", group: "validation" })
+fun test_edgeCase() {
+    // Test code
 }
 ```
diff --git a/docs/ext/unit-testing/assertions.md b/docs/ext/unit-testing/assertions.md
index 4eef7c0..99ed4b1 100644
--- a/docs/ext/unit-testing/assertions.md
+++ b/docs/ext/unit-testing/assertions.md
@@ -5,100 +5,116 @@ grand_parent: Advanced Topics
 nav_order: 2
 ---
 
-In order to verify values within the unit tests, we can utilise the Assertion framework.
+# Assertions
 
-These are accessible under the `@.Assert` namespace.
+Assertions verify values in unit tests. They are available under the `@.Assert` namespace. All assertion methods accept an optional third parameter `message` for custom failure output.
 
-## Equal
+## Equality
 
-### Description
+### equal
 
-Verifies whether or not values are equal.
+Verifies that two values are equal (deep equality for objects and arrays; property order is ignored for objects).
 
-### Syntax
+**Syntax:** `@.Assert.equal(expected, actual, message?)`
 
-`@.Assert.Equal($expectedValue, $actualValue, $msg)`
-
-- `$expectedValue`: Expected value.
-- `$actualValue`: Actual value to verify against.
-- `$msg`:  Optional error message to show if assertion fails.
-
-### Example
-
-```kotlin
+```isl
 @test
-fun assert_equals() {
- $var1 : 20
- @.Assert.Equal(40, $var1, "Values don't match.");
+fun test_equal() {
+    $var1: 20;
+    @.Assert.equal(20, $var1);
+
+    $obj1: { a: 1, b: 2 };
+    $obj2: { b: 2, a: 1 };
+    @.Assert.equal($obj1, $obj2);  // Objects equal despite property order
 }
 ```
 
-## NotEqual
-
-### Description
-
-Verifies whether or not values are not equal.
-
-### Syntax
+### notEqual
 
-`@.Assert.NotEqual($expectedValue, $actualValue, $msg)`
+Verifies that two values are not equal.
 
-- `$expectedValue`: Expected value.
-- `$actualValue`: Actual value to verify against.
-- `$msg`: Optional error message to show if assertion fails.
+**Syntax:** `@.Assert.notEqual(expected, actual, message?)`
 
-### Example
-
-```kotlin
+```isl
 @test
-fun assert_equals() {
- $var1 : 20
- @.Assert.NotEqual(40, $var1, "Values match.");
+fun test_notEqual() {
+    $var1: 20;
+    @.Assert.notEqual(40, $var1, "Values should differ");
 }
 ```
 
-## NotNull
+## Null Checks
 
-### Description
+### notNull
 
-Verifies whether or not value is not null.
+Verifies that a value is not null.
 
-### Syntax
+**Syntax:** `@.Assert.notNull(value, message?)`
 
-`@.Assert.NotNull($value, $msg)`
+```isl
+@test
+fun test_notNull() {
+    $var1: 42;
+    @.Assert.notNull($var1, "Value should not be null");
+}
+```
 
-- `$value`: Expected value.
-- `$msg`: Optional error message to show if assertion fails.
+### isNull
 
-### Example
+Verifies that a value is null.
 
-```kotlin
+**Syntax:** `@.Assert.isNull(value, message?)`
+
+```isl
 @test
-fun assert_equals() {
- $var1 : null
- @.Assert.NotNull($var1, "Value is null");
+fun test_isNull() {
+    $var1: null;
+    @.Assert.isNull($var1, "Value should be null");
 }
 ```
 
-## IsNull
+## Comparisons
+
+| Assertion | Description |
+|-----------|-------------|
+| `@.Assert.lessThan(a, b)` | a < b |
+| `@.Assert.lessThanOrEqual(a, b)` | a <= b |
+| `@.Assert.greaterThan(a, b)` | a > b |
+| `@.Assert.greaterThanOrEqual(a, b)` | a >= b |
 
-### Description
+## String and Pattern
 
-Verifies whether or not value is null.
+| Assertion | Description |
+|-----------|-------------|
+| `@.Assert.matches(pattern, value)` | value matches regex pattern |
+| `@.Assert.notMatches(pattern, value)` | value does not match pattern |
+| `@.Assert.contains(expected, actual)` | actual contains expected |
+| `@.Assert.notContains(expected, actual)` | actual does not contain expected |
+| `@.Assert.startsWith(prefix, value)` | value starts with prefix |
+| `@.Assert.notStartsWith(prefix, value)` | value does not start with prefix |
+| `@.Assert.endsWith(suffix, value)` | value ends with suffix |
+| `@.Assert.notEndsWith(suffix, value)` | value does not end with suffix |
 
-### Syntax
+## Membership and Type
 
-`@.Assert.IsNull($value, $msg)`
+| Assertion | Description |
+|-----------|-------------|
+| `@.Assert.in(value, collection)` | value is in collection |
+| `@.Assert.notIn(value, collection)` | value is not in collection |
+| `@.Assert.isType(value, type)` | value is of type (e.g. `number`, `string`, `array`, `node`, `date`) |
+| `@.Assert.isNotType(value, type)` | value is not of type |
 
-- `$value`: Expected value.
-- `$msg`: Optional error message to show if assertion fails.
+## Custom Failure Message
 
-### Example
+All assertions accept an optional third parameter for a custom message:
 
-```kotlin
+```isl
 @test
-fun assert_equals() {
- $var1 : null
- @.Assert.NotNull($var1, "Value not null");
+fun test_withMessage() {
+    $var1: 1;
+    $var2: 2;
+    @.Assert.equal($var1, $var2, "Expected 1 to equal 2 - values mismatch");
 }
 ```
+
+When the assertion fails, the custom message is included in the output.
diff --git a/docs/ext/unit-testing/index.md b/docs/ext/unit-testing/index.md
index 6e0ef59..f350efe 100644
--- a/docs/ext/unit-testing/index.md
+++ b/docs/ext/unit-testing/index.md
@@ -5,17 +5,170 @@ nav_order: 3
 has_children: true
 ---
 
-**WIP**
+# ISL Unit Testing
 
-This section is dedicated to the unit testing framework in ISL.
+The ISL unit testing framework lets you write and run tests entirely in ISL. Tests live alongside your transformation code, use the same syntax, and can verify behavior without learning a separate testing language.
 
-The goal of this framework is to add reliability to ISL code, by allowing creation of tests
-to ensure critical behaviour within the script is maintained.
+## Quick Start
 
-The goal of the framework is to be able to create unit tests entirely within the ISL langauge,
-ensuring developers can make changes to the ISL tests when necessary, without the learning curve of
-learning a new testing methodology.
+1. Create an `.isl` file with `@test`-annotated functions:
 
-The syntax and structure of the ISL testing framework is inspired by JUnit.
+```isl
+@test
+fun test_simpleAssertion() {
+    $value: 42;
+    @.Assert.equal(42, $value);
+}
+```
 
-More reading on the implementation can be found here: [link](https://docs.google.com/document/d/1E0wlMy5XrKL0lpgtu6IKqQRdbBUIz3f2gLZb5tR064g/edit?usp=sharing)
+2. Run tests from the command line:
+
+```bash
+isl test
+# or specify a path
+isl test tests/
+isl test tests/sample.isl
+isl test tests/calculator.tests.yaml   # YAML-driven suite
+```
+
+3. Or run tests programmatically from Kotlin/Java (see [Test Setup](setup.md)).
+
+## Two Ways to Define Tests
+
+- **Annotation-based** – In `.isl` files with `@setup` and `@test` (see [Test Annotations](annotations.md)).
+- **YAML-driven** – In `*.tests.yaml` files: specify `setup.islSource`, optional mocks, and a list of tests with `functionName`, `input`, and `expected`. No ISL test code required. See [YAML-Driven Test Suites](yaml-tests.md) for the full format, including `assertOptions` and `mockSource`/`mocks`.
+
+## What You Can Test
+
+- **Transformations** – Call your ISL functions and assert on the output
+- **Conditions** – Verify branching logic, edge cases
+- **Modifiers** – Test `| trim`, `| map`, `| filter`, etc.
+- **External integrations** – Use [mocking](mocking.md) to replace `@.Call.Api` and similar
+
+## File Format and Structure
+
+Tests are written in standard `.isl` files. A test file typically contains:
+
+- One optional `@setup` function (runs before each test)
+- One or more `@test` functions (each is a test case)
+
+```isl
+@setup
+fun setup() {
+    $x: 1;  // Shared setup runs before each test
+}
+
+@test
+fun test_basic() {
+    @.Assert.equal(1, 1);
+}
+
+@test("Custom display name")
+fun test_withName() {
+    @.Assert.equal(2, 2);
+}
+
+@test({ name: "Grouped test", group: "math" })
+fun test_grouped() {
+    @.Assert.equal(3, 3);
+}
+```
+
+File discovery:
+
+- **CLI**: `isl test` finds all `.isl` files (by default `**/*.isl`) containing `@setup` or `@test`
+- **API**: You pass the list of files to `TransformTestPackageBuilder`
+
+## Attributes (Annotations)
+
+| Attribute | Description |
+|-----------|-------------|
+| `@test` | Marks a function as a test. Runs as a test case. |
+| `@test("Name")` | Same, with a custom display name |
+| `@test(name, group)` | Custom name and group for organization |
+| `@test({ name: "x", group: "y" })` | Object form for name and group |
+| `@setup` | Marks a function to run before each test in the file (at most one per file) |
+
+See [Test Annotations](annotations.md) for details.
+
+## Assertions
+
+Use `@.Assert` to verify values:
+
+| Assertion | Description |
+|-----------|-------------|
+| `@.Assert.equal(expected, actual, message?)` | Deep equality (objects, arrays, primitives) |
+| `@.Assert.notEqual(expected, actual, message?)` | Values must differ |
+| `@.Assert.notNull(value, message?)` | Value must not be null |
+| `@.Assert.isNull(value, message?)` | Value must be null |
+| `@.Assert.contains(expected, actual)` | actual contains expected |
+| `@.Assert.matches(pattern, value)` | value matches regex |
+| `@.Assert.startsWith(prefix, value)` | value starts with prefix |
+| ... | See [Assertions](assertions.md) for the full list |
+
+## Loading Test Fixtures
+
+Use `@.Load.From(fileName)` to load JSON, YAML, or CSV files relative to the current ISL file:
+
+```isl
+@test
+fun test_withFixture() {
+    $data = @.Load.From("fixtures/input.json")
+    @.Assert.equal("expected", $data.name)
+
+    $config = @.Load.From("config.yaml")
+    @.Assert.equal(10, $config.count)
+
+    $rows = @.Load.From("fixtures/data.csv")
+    @.Assert.equal(2, $rows | length)
+}
+```
+
+Supported formats: `.json`, `.yaml`, `.yml`, `.csv` (all converted to JSON). See [Loading Fixtures](loading.md).
+
+## How to Run Tests
+
+### Command Line (Recommended)
+
+```bash
+isl test [path] [options]
+```
+
+- `path`: Directory, single file (e.g. `sample.isl` or `suite.tests.yaml`), or default: current directory
+- `--glob PATTERN`: Filter .isl files when path is a directory (YAML suites use `**/*.tests.yaml` when not set)
+- `-f, --function NAME`: Run only tests whose function name matches; use `file:function` for a specific file (e.g. `sample.isl:test_customer` or `calculator.tests.yaml:add`)
+- `-o, --output FILE`: Write results to JSON
+
+### Programmatic (Kotlin/Java)
+
+See [Test Setup](setup.md) for adding the `isl-test` dependency and running tests from code.
+
+## Mocking
+
+Mock external functions (e.g. `@.Call.Api`) so tests don't hit real services:
+
+```isl
+@test
+fun test_withMock() {
+    @.Mock.Func("Call.Api", { status: 200, body: "ok" })
+    $result = @.Call.Api("https://example.com")
+    @.Assert.equal(200, $result.status)
+}
+```
+
+See [Mocking](mocking.md) for parameter matching, indexed (sequential) returns, loading mocks from files, captures, and annotation mocks.
+
+## Test Output
+
+- **CLI**: Prints pass/fail per test, with failure messages and locations
+- **JSON**: Use `-o results.json` for machine-readable output
+- **Exit code**: 1 if any test failed, 0 if all passed
+
+## Next Steps
+
+- [Test Setup](setup.md) – CLI usage, dependencies, programmatic API
+- [YAML-Driven Test Suites](yaml-tests.md) – `*.tests.yaml` format, assertOptions, mocks
+- [Test Annotations](annotations.md) – `@test` and `@setup` in detail
+- [Assertions](assertions.md) – Full assertion reference
+- [Loading Fixtures](loading.md) – `@.Load.From` for JSON/YAML/CSV
+- [Mocking](mocking.md) – Mock functions and annotations
diff --git a/docs/ext/unit-testing/loading.md b/docs/ext/unit-testing/loading.md
new file mode 100644
index 0000000..ad346c3
--- /dev/null
+++ b/docs/ext/unit-testing/loading.md
@@ -0,0 +1,118 @@
+---
+title: Loading Fixtures
+parent: Unit Testing
+grand_parent: Advanced Topics
+nav_order: 5
+---
+
+# Loading Test Fixtures
+
+Use `@.Load.From(fileName)` to load JSON, YAML, or CSV files as test data. Paths are resolved **relative to the directory of the current ISL file**.
+
+## Syntax
+
+```isl
+$data = @.Load.From("fileName")
+```
+
+- `fileName` – Path relative to the current file (e.g. `fixtures/data.json`, `../shared/config.yaml`)
+
+## Supported Formats
+
+| Extension | Description |
+|-----------|-------------|
+| `.json` | Parsed as JSON |
+| `.yaml`, `.yml` | Parsed as YAML, converted to JSON |
+| `.csv` | Parsed as CSV; first row = headers, converted to array of objects |
+
+All formats are returned as JSON (objects or arrays) for use in assertions.
+
+## Examples
+
+### JSON
+
+**fixtures/user.json:**
+```json
+{"name": "Alice", "age": 30, "active": true}
+```
+
+**tests/user.isl:**
+```isl
+@test
+fun test_loadJson() {
+    $user = @.Load.From("fixtures/user.json")
+    @.Assert.equal("Alice", $user.name)
+    @.Assert.equal(30, $user.age)
+    @.Assert.equal(true, $user.active)
+}
+```
+
+### YAML
+
+**fixtures/config.yaml:**
+```yaml
+key: value
+nested:
+  count: 10
+  items: [a, b, c]
+```
+
+**tests/config.isl:**
+```isl
+@test
+fun test_loadYaml() {
+    $config = @.Load.From("fixtures/config.yaml")
+    @.Assert.equal("value", $config.key)
+    @.Assert.equal(10, $config.nested.count)
+    @.Assert.equal(3, $config.nested.items | length)
+}
+```
+
+### CSV
+
+**fixtures/data.csv:**
+```csv
+id,name,score
+1,Alice,100
+2,Bob,85
+```
+
+**tests/data.isl:**
+```isl
+@test
+fun test_loadCsv() {
+    $rows = @.Load.From("fixtures/data.csv")
+    @.Assert.equal(2, $rows | length)
+    @.Assert.equal("Alice", $rows[0].name)
+    @.Assert.equal(100, $rows[0].score)
+    @.Assert.equal("Bob", $rows[1].name)
+}
+```
+
+CSV is parsed with the first row as headers. Each subsequent row becomes an object with those headers as keys.
+
+## Path Resolution
+
+Paths are relative to the **directory of the current ISL file**:
+
+| Current file | `fileName` | Resolved path |
+|--------------|------------|---------------|
+| `tests/sample.isl` | `fixtures/data.json` | `tests/fixtures/data.json` |
+| `tests/sample.isl` | `../shared/config.yaml` | `shared/config.yaml` |
+| `tests/unit/sample.isl` | `../../fixtures/data.json` | `fixtures/data.json` |
+
+## Availability
+
+`@.Load.From` is available only in the **test context**:
+
+- ✅ When running tests via `isl test`
+- ✅ When running via `TransformTestPackage` with `basePath` passed to `TransformTestPackageBuilder`
+- ❌ In regular transform execution (e.g. `isl transform`)
+
+If `basePath` is not set (e.g. programmatic use without it), `Load.From` throws a clear error.
+
+## Error Handling
+
+- **File not found**: Throws with the resolved path
+- **Unsupported format**: Throws for extensions other than `.json`, `.yaml`, `.yml`, `.csv`
+- **Parse error**: Invalid JSON/YAML/CSV throws during parsing
diff --git a/docs/ext/unit-testing/mocking.md b/docs/ext/unit-testing/mocking.md
index 7a6f939..70c8976 100644
--- a/docs/ext/unit-testing/mocking.md
+++ b/docs/ext/unit-testing/mocking.md
@@ -1,9 +1,9 @@
 ---
-title: Mocking
+
+## title: Mocking
 parent: Unit Testing
 grand_parent: Advanced Topics
 nav_order: 4
----
 
 ## Introduction
 
@@ -102,6 +102,97 @@ fun assert_mock() {
 }
 ```
 
+### Indexed mocking (sequential returns)
+
+When a function is called multiple times with the same parameters, you can return different values per call by appending `#1`, `#2`, `#3`, etc. to the function name. Each index corresponds to the Nth invocation.
+
+- **Standard behaviour** – `@.Mock.Func("Data.GetData", value)` returns the same value on every call.
+- **Indexed behaviour** – `@.Mock.Func("Data.GetData#1", value1)` returns `value1` on the first call, `@.Mock.Func("Data.GetData#2", value2)` on the second, and so on.
+
+On exhaustion (when the function is called more times than defined), the mock fails with a clear error.
+
+#### Example
+
+```kotlin
+@test
+fun assert_indexed_mock() {
+ @.Mock.Func("Data.GetData#1", [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 } ])
+ @.Mock.Func("Data.GetData#2", [ { id: 1 }, { id: 2 }, { id: 3 } ])
+ @.Mock.Func("Data.GetData#3", [])
+
+ $r1 : @.Data.GetData()
+ $r2 : @.Data.GetData()
+ $r3 : @.Data.GetData()
+
+ @.Assert.equal(5, $r1 | length)
+ @.Assert.equal(3, $r2 | length)
+ @.Assert.equal(0, $r3 | length)
+}
+```
+
+Indexed mocking works for both `@.Mock.Func` and `@.Mock.Annotation`. When using `@.Mock.GetFuncCaptures` or `@.Mock.GetAnnotationCaptures`, you can pass the base name (with or without `#index`); captures are associated with the base function.
+
+### Loading mocks from a file
+
+Use `@.Mock.Load(relativeFileName)` to load mocks from a YAML or JSON file. The path is resolved relative to the directory of the current ISL file (same as `@.Load.From`).
+
+The file format mirrors the existing mock functions:
+
+```yaml
+func:
+  - name: "Data.GetData#1"
+    return: [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 } ]
+  - name: "Data.GetData#2"
+    return: [ { id: 1 }, { id: 2 }, { id: 3 } ]
+  - name: "Data.GetData#3"
+    return: []
+  - name: "Api.Call"
+    return: { status: 200, body: "ok" }
+    params: [ "https://example.com" ]   # optional parameter matching
+
+  # ISL mock: define an ISL function and run it as the mock (compiled on the fly)
+  - name: "mask"
+    isl: |
+         fun mask( $value ) {
+             return `xxxxxx$value`;
+         }
+
+  # Pipe modifier: "Modifier.[Name]" mocks "| name" (e.g. $x | mask2)
+  - name: "Modifier.mask2"
+    isl: |
+         fun mask2( $value ) {
+             return `xxxxxx$value`;
+         }
+
+annotation:
+  - name: "mask2"
+    result: "masked"
+  # - name: "cache#1"
+  #   return: "cached-value"
+```
+
+- `func` and `annotation` are arrays of mock entries.
+- Each entry has `name` (required), and either `return`/`result` (static value) or `isl` (ISL snippet).
+- **Pipe modifiers** (`| name`): to mock a pipe modifier like `$x | mask2`, add a **func** entry with name `Modifier.[Name]` (e.g. `Modifier.mask2`). The mock receives the left-hand value as the first parameter.
+- **Annotations** (`@name` on a function): use the `annotation` array with the annotation name (e.g. `mask2`). This mocks the decorator when you write `@mask2 fun foo() { ... }`; it does not mock the pipe `| mask2`.
+- `**isl**`: Multiline ISL source defining one or more functions. The first function is compiled and executed as the mock; when the mock is called, that function runs with the call's parameters bound to its arguments. Use this for dynamic or computed mock behaviour.
+- Optionally `params` (array of values to match) for parameter-based matching.
+- Use `#1`, `#2`, etc. in the name for indexed (sequential) returns.
+- Supports `.yaml`, `.yml`, and `.json` files.
+
+#### Example
+
+```kotlin
+@test
+fun test_with_loaded_mocks() {
+ @.Mock.Load("mocks/api-mocks.yaml")
+ $r1 : @.Data.GetData()
+ $r2 : @.Data.GetData()
+ @.Assert.equal(5, $r1 | length)
+ @.Assert.equal(3, $r2 | length)
+}
+```
+
 ### Capturing parameter inputs
 
 We can also obtain all of the parameters passed into the function during the test. The parameters are stored in chronological order.
@@ -142,6 +233,30 @@ $allPublished = @.Mock.GetFuncCaptures('Event.Publish')
 
 ## Functions
 
+### Load
+
+#### Description
+
+Loads mocks from a YAML or JSON file. The path is relative to the directory of the current ISL file.
+
+#### Syntax
+
+`@.Mock.Load($fileName)`
+
+- `$fileName`: Relative path to the mock file (e.g. `"mocks/api.yaml"`). Supports `.yaml`, `.yml`, and `.json`.
+- `Returns`: null
+
+#### Example
+
+```kotlin
+@test
+fun test_with_mocks() {
+ @.Mock.Load("mocks/api-mocks.yaml")
+ $result = @.Api.Call("https://example.com")
+ @.Assert.equal(200, $result.status)
+}
+```
+
 ### Func
 
 #### Description
@@ -152,10 +267,10 @@ Creates a mock of an ISL function.
 
 `@.Mock.Func($funcToMock, $returnValue, ...$paramsToMatch)`
 
-- `$funcToMock`: ISL function to mock.
+- `$funcToMock`: ISL function to mock. Use `Function.Name#1`, `Function.Name#2`, etc. for indexed (sequential) returns per call.
 - `$returnValue`: Return value of mock.
-- `$paramsToMatch`:  Optional error message to show if assertion fails.
-- `Returns`: Unique id of the mock.
+- `$paramsToMatch`:  Optional parameters to match. When omitted, matches any parameters.
+- `Returns`: Unique id of the mock (null for default mocks).
 
 #### Example
 
@@ -186,10 +301,10 @@ Currently has no functionality, but can be used to:
 
 `@.Mock.Annotation($funcToMock, $returnValue, ...$paramsToMatch)`
 
-- `$funcToMock`: ISL annotation to mock.
+- `$funcToMock`: ISL annotation to mock. Use `AnnotationName#1`, `AnnotationName#2`, etc. for indexed (sequential) returns per call.
 - `$returnValue`: Return value of mock.
-- `$paramsToMatch`:  Optional error message to show if assertion fails.
-- `Returns`: Unique id of the mock.
+- `$paramsToMatch`:  Optional parameters to match. When omitted, matches any parameters.
+- `Returns`: Unique id of the mock (null for default mocks).
 
 #### Example
 
@@ -404,3 +519,4 @@ fun test_function() {
  // ]
 }
 ```
+
diff --git a/docs/ext/unit-testing/setup.md b/docs/ext/unit-testing/setup.md
index 61d339e..a967634 100644
--- a/docs/ext/unit-testing/setup.md
+++ b/docs/ext/unit-testing/setup.md
@@ -5,52 +5,132 @@ grand_parent: Advanced Topics
 nav_order: 1
 ---
 
-## Setting up your first test
+# Test Setup
 
-In order to utilise the library, add the following to your `pom.xml`
+## Running Tests via CLI (Recommended)
+
+The easiest way to run ISL tests is with the `isl test` command:
+
+```bash
+# Run tests in current directory (discovers **/*.isl with @test or @setup)
+isl test
+
+# Run tests in a specific directory
+isl test tests/
+
+# Run a specific file
+isl test tests/sample.isl
+
+# Custom glob pattern (applies to .isl files; YAML suites use **/*.tests.yaml when not set)
+isl test tests/ --glob "**/*.test.isl"
+
+# Run only specific test function(s) (by function name or file:function)
+isl test . -f add -f test_customer
+isl test . -f calculator.tests.yaml:add -f sample.isl:test_simpleAssertion
+
+# Write results to JSON
+isl test -o results.json
+```
+
+The CLI discovers:
+
+- **.isl files** containing `@setup` or `@test` annotations (default glob: `**/*.isl`)
+- **\*.tests.yaml** (or \*.tests.yml) YAML-driven test suites (default glob: `**/*.tests.yaml`)
+
+Both are run when you pass a directory (e.g. `isl test .`).
+
+## Running Tests Programmatically
+
+### Maven
+
+Add the `isl-test` dependency:
 
 ```xml
 
-   com.intuit.isl
-   isl-test
-   ... 
+    com.intuit.isl
+    isl-test
+    1.1.19+
 
 ```
 
-To evoke the unit test files, run the following:
-
-### Kotlin
+### Gradle (Kotlin DSL)
 
 ```kotlin
-val builder = TransformPackageBuilder()
-
-val scripts : List
+dependencies {
+    implementation("com.intuit.isl:isl-test:1.1.19+")
+}
+```
 
-val transformPackage = builder.build(fileInfo)
+### Kotlin Example
 
-val transformTestPackage = TransformTestPackage(transformPackage)
+```kotlin
+import com.intuit.isl.runtime.FileInfo
+import com.intuit.isl.test.TransformTestPackageBuilder
+import java.nio.file.Path
+import java.nio.file.Paths
+
+// Build test package from files
+val basePath = Paths.get(".").toAbsolutePath()
+val fileInfos = listOf(
+    FileInfo("tests/sample.isl", Path.of("tests/sample.isl").toFile().readText())
+).toMutableList()
+
+val testPackage = TransformTestPackageBuilder().build(
+    fileInfos,
+    findExternalModule = null,
+    basePath = basePath  // Required for @.Load.From
+)
 
 // Run all tests
-val testResults = testPackage.runAllTests()
+val results = testPackage.runAllTests()
+
+// Run a specific test
+val singleResult = testPackage.runTest("tests/sample.isl", "test_simpleAssertion")
 
-// Run a specific test function within defined test file.
-val individualTestResult = testPackage.runTest("test.isl", "test")
+// Check results
+results.testResults.forEach { tr ->
+    println("${tr.testName}: ${if (tr.success) "PASS" else "FAIL"} ${tr.message ?: ""}")
+}
 ```
 
-### Java
+### Java Example
 
 ```java
-TransformPackageBuilder builder = new TransformPackageBuilder();
+import com.intuit.isl.runtime.FileInfo;
+import com.intuit.isl.test.TransformTestPackageBuilder;
+import com.intuit.isl.test.annotations.TestResultContext;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.ArrayList;
+
+Path basePath = Paths.get(".").toAbsolutePath();
+List fileInfos = new ArrayList<>();
+fileInfos.add(new FileInfo("tests/sample.isl", Files.readString(Path.of("tests/sample.isl"))));
+
+var builder = new TransformTestPackageBuilder();
+var testPackage = builder.build(fileInfos, null, basePath);
+
+TestResultContext results = testPackage.runAllTests();
+
+// Run specific test
+TestResultContext singleResult = testPackage.runTest("tests/sample.isl", "test_simpleAssertion");
+```
 
-List scripts;
+## basePath and Load.From
 
-TransformPackage transformPackage = builder.build(fileInfo)
+When running tests programmatically, pass `basePath` to `TransformTestPackageBuilder.build()` so that `@.Load.From(fileName)` can resolve relative paths correctly. If `basePath` is `null`, `Load.From` will throw when used.
 
-TransformTestPackage transformTestPackage = new TransformTestPackage(transformPackage)
+## Test Result Structure
 
-// Run all tests
-TestResultContext testResults = testPackage.runAllTests()
+`TestResultContext` contains:
 
-// Run a specific test function within defined test file.
-TestResultContext individualTestResult = testPackage.runTest("test.isl", "test")
-```
+- `testResults` – List of `TestResult` with:
+  - `testFile` – Source file
+  - `functionName` – Function name
+  - `testName` – Display name (from `@test` or function name)
+  - `testGroup` – Group (from `@test` or file name)
+  - `success` – Whether the test passed
+  - `message` – Failure message if any
+  - `errorPosition` – File/line/column if available
diff --git a/docs/ext/unit-testing/yaml-tests.md b/docs/ext/unit-testing/yaml-tests.md
new file mode 100644
index 0000000..94c312d
--- /dev/null
+++ b/docs/ext/unit-testing/yaml-tests.md
@@ -0,0 +1,280 @@
+---
+title: YAML-Driven Test Suites
+parent: Unit Testing
+grand_parent: Advanced Topics
+nav_order: 2
+---
+
+# YAML-Driven Test Suites
+
+You can define unit tests in **\*.tests.yaml** (or \*.tests.yml) files without writing ISL test code. A YAML suite specifies the ISL module to test, optional mocks, and a list of test cases with `functionName`, `input`, and `expected` result. The runner invokes each function and compares the result to `expected` using configurable comparison options.
+
+## When to Use YAML Suites
+
+- **Data-heavy tests** – Many input/expected pairs without custom logic
+- **Non-ISL authors** – QA or product can add tests by editing YAML
+- **Shared mocks** – Reuse `mockSource` and inline `mocks` across many tests
+- **CI / tooling** – Generate or parse `*.tests.yaml` from other systems
+
+You can mix YAML suites with [annotation-based tests](annotations.md) in the same project; `isl test .` runs both.
+
+## File and Discovery
+
+- **Naming**: `*.tests.yaml` or `*.tests.yml` (e.g. `calculator.tests.yaml`).
+- **Discovery**: When you run `isl test `, the CLI finds all such files under the path (default glob: `**/*.tests.yaml`).
+- **Single file**: `isl test path/to/suite.tests.yaml` runs only that suite.
+
+All paths inside the YAML file (`islSource`, `mockSource`) are **relative to the directory containing the `.tests.yaml` file**.
+
+## Suite Structure
+
+```yaml
+category: my-group-name          # optional; used as test group in output
+setup:
+  islSource: mymodule.isl       # ISL file to test (required)
+  mockSource: optional.yaml     # optional; see Mocks below
+  mocks:                        # optional inline mocks; see Mocks below
+    func: [ ... ]
+assertOptions:                  # optional; see Assert options below
+  nullSameAsMissing: true
+tests:                          # or islTests (same meaning)
+  - name: test display name
+    functionName: myFunction
+    input: 42                   # or object for multiple params
+    expected: { result: 42 }
+```
+
+- **category** – Label for the suite in results (e.g. `[ISL Result] my-group-name`). If omitted, the suite file name (without extension) is used.
+- **setup** – Required. Contains `islSource` and optionally `mockSource` and `mocks`.
+- **assertOptions** – Optional. Controls how `expected` is compared to the function result. Can be set at suite level and overridden per test.
+- **tests** / **islTests** – List of test entries. Either key is accepted.
+
+## Setup
+
+### islSource (required)
+
+The ISL file to load and run. Path is relative to the directory of the `.tests.yaml` file.
+
+```yaml
+setup:
+  islSource: calculator.isl
+```
+
+### mockSource (optional)
+
+Mock definitions loaded from file(s), in the same format as `@.Mock.Load`. Paths are relative to the suite directory.
+
+- **Single file**: `mockSource: ../mocks/sample-mocks.yaml`
+- **Multiple files** (loaded in order; later overrides earlier):  
+  `mockSource: [common.yaml, overrides.yaml]`
+
+Supported extensions: `.json`, `.yaml`, `.yml`.
+
+### mocks (optional, inline)
+
+Inline mocks applied **after** `mockSource`, so they override or add to file-based mocks. Same structure as in `@.Mock.Load`: `func` and/or `annotation` arrays.
+
+```yaml
+setup:
+  islSource: service.isl
+  mockSource: ../mocks/sample-mocks.yaml
+  mocks:
+    func:
+      - name: "Api.Call"
+        return: { status: 200, body: "overridden" }
+```
+
+All mocks are additive; parameter lists differentiate overloads.
+
+## Test Entries
+
+Each entry under `tests` (or `islTests`) has:
+
+| Field | Required | Description |
+|-------|----------|-------------|
+| **name** | Yes | Display name in results |
+| **functionName** | Yes | ISL function to call |
+| **input** | No | Input to the function. Single value for single-param; object with param names as keys for multiple params |
+| **ignore** | No | JSON paths to ignore when comparing expected vs actual (exact path match). Use dot notation; array indices as `[0]`, `[1]`, etc. |
+| **expected** | No | Expected return value (JSON). Omitted or `null` means expect `null` |
+| **byPassAnnotations** | No | If `true`, bypass annotation processing (optional) |
+| **assertOptions** | No | Override suite `assertOptions` for this test only. Same formats as suite (object, comma-separated list, or array of option names) |
+
+### Input format
+
+- **Single-parameter function**: `input` can be a scalar or object (passed as that one argument).
+- **Multi-parameter function**: `input` must be an object; keys are parameter names (with or without `$`). Values are passed as the corresponding arguments.
+
+```yaml
+# Single param
+- name: double a number
+  functionName: double
+  input: 7
+  expected: 14
+
+# Multiple params
+- name: add two numbers
+  functionName: add
+  input:
+    a: 2
+    b: 3
+  expected: 5
+```
+
+### Ignoring JSON paths (ignore)
+
+To skip comparison at specific paths (e.g. dynamic or non-deterministic fields), set **ignore** above **expected**. Paths use dot notation; array indices use `[0]`, `[1]`, etc.
+
+```yaml
+- name: response with ignored fields
+  functionName: callApi
+  input: { id: 1 }
+  ignore:
+    - providerResponses.error.detail
+    - providerResponses.items[0].uid
+  expected:
+    status: 200
+    providerResponses:
+      error: {}
+      items:
+        - { name: "first" }
+```
+
+Only the listed paths are ignored (exact match); the rest of the object is compared as usual.
+
+When a test fails, the failure output includes **Result Differences** (expected vs actual and per-path diffs). If the test entry has **ignore** set, that output also lists **Ignored path(s)** so you can see which paths were skipped during comparison.
+
+## Assert Options (assertOptions)
+
+Assert options control how the actual function result is compared to `expected`. By default, comparison is strict (exact match). You can relax it at the **suite** level and optionally **per test**.
+
+### Where to set assertOptions
+
+- **Suite level**: under the root key `assertOptions`. Applies to all tests in the suite unless a test overrides.
+- **Per test**: under a test entry as `assertOptions`. Overrides the suite options for that test only.
+
+### Formats
+
+You can write `assertOptions` in three ways:
+
+**1. Object (explicit booleans):**
+
+```yaml
+assertOptions:
+  nullSameAsMissing: true
+  ignoreExtraFieldsInActual: true
+```
+
+**2. Comma-separated list of option names:**
+
+```yaml
+assertOptions: nullSameAsMissing, nullSameAsEmptyArray, missingSameAsEmptyArray, ignoreExtraFieldsInActual, numbersEqualIgnoreFormat
+```
+
+**3. Array of option names:**
+
+```yaml
+assertOptions:
+  - nullSameAsMissing
+  - nullSameAsEmptyArray
+  - missingSameAsEmptyArray
+  - ignoreExtraFieldsInActual
+  - numbersEqualIgnoreFormat
+```
+
+### Option reference
+
+All options default to `false` (strict comparison). Supported options:
+
+| Option | Default | Description |
+|--------|---------|-------------|
+| **nullSameAsMissing** | `false` | Treat `null` and missing (absent key) as equal |
+| **nullSameAsEmptyArray** | `false` | Treat `null` and empty array `[]` as equal |
+| **missingSameAsEmptyArray** | `false` | Treat missing (absent key) and empty array `[]` as equal |
+| **ignoreExtraFieldsInActual** | `false` | Only compare keys present in `expected`; ignore extra keys in actual |
+| **numbersEqualIgnoreFormat** | `false` | Compare numbers by numeric value only (e.g. `1234`, `1234.0`, `1234.00` are equal) |
+
+### Example: suite and per-test override
+
+```yaml
+category: api
+setup:
+  islSource: api.isl
+assertOptions:
+  ignoreExtraFieldsInActual: true
+  numbersEqualIgnoreFormat: true
+tests:
+  - name: strict comparison for this test
+    functionName: getExact
+    input: 1
+    expected: { id: 1, name: "x" }
+    assertOptions: {}   # or omit; use only suite options
+
+  - name: allow extra fields and null as missing
+    functionName: getPartial
+    input: 2
+    expected: { id: 2 }
+    assertOptions:
+      nullSameAsMissing: true
+      ignoreExtraFieldsInActual: true
+```
+
+## Running YAML Suites
+
+### Command line
+
+```bash
+# Run all tests (YAML suites + .isl tests under current dir)
+isl test .
+
+# Run a single YAML suite
+isl test path/to/calculator.tests.yaml
+
+# Run only tests whose function name matches
+isl test . -f add -f double
+
+# Run a specific test in a specific suite (suiteFile:functionName)
+isl test . -f calculator.tests.yaml:add
+```
+
+The `-f` / `--function` filter applies to both annotation-based tests and YAML suites. For YAML, you can use `functionName` or `suiteFile:functionName` (e.g. `calculator.tests.yaml:add`).
+
+### Output
+
+- Pass/fail per test with the entry’s `name` (and `functionName` in brackets when different).
+- On failure, a comparison message shows expected vs actual and, when available, path-level differences (e.g. `$.field.[0].key`). If the test uses **ignore**, the failure output also lists the ignored path(s).
+- Use `-o results.json` for machine-readable results.
+
+## Full Example
+
+**calculator.isl** (snippet):
+
+```isl
+fun add($a, $b) { $a + $b }
+fun double($x) { $x * 2 }
+```
+
+**calculator.tests.yaml**:
+
+```yaml
+category: calculator
+setup:
+  islSource: calculator.isl
+tests:
+  - name: add two numbers
+    functionName: add
+    input: { a: 2, b: 3 }
+    expected: 5
+  - name: double a number
+    functionName: double
+    input: 7
+    expected: 14
+```
+
+Run: `isl test calculator.tests.yaml` or `isl test .`
+
+## See also
+
+- [Test Setup](setup.md) – CLI discovery, `-f`, `-o`
+- [Mocking](mocking.md) – Mock format for `mockSource` and `mocks`
+- [Test Annotations](annotations.md) – `@test` / `@setup` in .isl files
diff --git a/docs/img/favicon.svg b/docs/img/favicon.svg
index 7e9660e..58bbd5b 100644
--- a/docs/img/favicon.svg
+++ b/docs/img/favicon.svg
@@ -4,18 +4,18 @@
       
       
       
       
       
       
+            fill="#2563FF"/>
       
       
       
diff --git a/docs/index.md b/docs/index.md
index aec052e..9ce17bd 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -124,6 +124,7 @@ Will output:
 - Support for [parsing XML](./types/xml.md#xml-processing) and [outputting XML](./types/xml.md#xml-output), [parsing CSV](./types/csv.md#csv-processing) or yaml.
 - Support for advanced String Interpolation `Hi there $name. Today is ${ @.Date.Now() | to.string("yyyy MM dd") }. `.
 - Support for [`find`, `match` and `replace` using Regular Expressions](./language/modifiers.md#regex-processing).
+- [Unit Testing](./ext/unit-testing/index.md) – Write and run tests in ISL with `@test`, assertions, fixtures (`@.Load.From`), and mocking.
 
 ## Learning And Support
 
diff --git a/docs/java.start.md b/docs/java.start.md
index afe60aa..85e2565 100644
--- a/docs/java.start.md
+++ b/docs/java.start.md
@@ -26,7 +26,7 @@ This guide shows you how to embed ISL in your Java/Kotlin project.
 
 ```kotlin
 dependencies {
-    implementation("com.intuit.isl:isl-transform:2.4.20-SNAPSHOT")
+    implementation("com.intuit.isl:isl-transform:1.1.0-SNAPSHOT")
 }
 ```
 
diff --git a/docs/overview.md b/docs/overview.md
index 53e2b5f..7edf4e7 100644
--- a/docs/overview.md
+++ b/docs/overview.md
@@ -80,7 +80,7 @@ Will output:
 
 
 
-# Structure
+## Structure
 ISL Code is structured as any programming language in multiple sections:
 
 1. `import` of other ISL files
diff --git a/docs/roadmap.md b/docs/roadmap.md
index b4088da..00fa33c 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -5,4 +5,8 @@ nav_order: 101
 
 What's upcoming for ISL:
 
-- [WIP] Improved Transformation Performance.
+- Support for variable level dynamic properties 
+    ```isl
+    $input.`$key` = $value
+    ```
+- Improved Transformation Performance.
diff --git a/gradle.properties b/gradle.properties
index b6893a4..ffa2274 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -7,9 +7,5 @@ org.gradle.caching=true
 kotlin.code.style=official
 
 # Version
-version=2.4.20-SNAPSHOT
-
-# Optional: Nexus credentials (can also be set via environment variables)
-# nexusUsername=your-username
-# nexusPassword=your-password
+version=1.1.0
 
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 1af9e09..aaaabb3 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
 networkTimeout=10000
 validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
diff --git a/isl-cmd/README.md b/isl-cmd/README.md
index 0db4119..94a06cb 100644
--- a/isl-cmd/README.md
+++ b/isl-cmd/README.md
@@ -66,12 +66,53 @@ isl transform script.isl --function processData -i input.json
 
 ### Validate Command
 
-Check if a script is syntactically valid:
+Check if a script is syntactically valid (supports relative imports like `../customer.isl`):
 
 ```bash
 isl validate script.isl
 ```
 
+### Test Command
+
+Run ISL tests from:
+
+- **.isl files** with `@setup` and `@test` annotations
+- **\*.tests.yaml files** (YAML-driven unit test suites)
+
+```bash
+# Run all tests in current directory (both .isl and *.tests.yaml)
+isl test .
+
+# Run a specific YAML suite
+isl test path/to/suite.tests.yaml
+
+# Run specific test functions
+isl test . -f test_customer -f test_simpleAssertion
+
+# Target a specific file:function
+isl test . -f sample.isl:test_customer
+```
+
+**YAML test suite format** (e.g. `mymodule.tests.yaml`):
+
+```yaml
+category: name of test group
+setup:
+  islSource: name of ISL file to test (e.g. mymodule.isl)
+  mockSource: optional mock file(s) — single path (e.g. mymocks.yaml) or array (e.g. [commonMocks.yaml, otherMocks.yaml]); loaded in order, each overrides the previous; same format as @.Mock.Load; paths relative to suite directory
+  mocks: optional inline mocks (func/annotation arrays); applied after mockSource so they override
+assertOptions: optional — assertion comparison options for the whole suite (object, or comma-separated/array of: nullSameAsMissing, nullSameAsEmptyArray, missingSameAsEmptyArray, ignoreExtraFieldsInActual, numbersEqualIgnoreFormat)
+tests:   # or islTests
+  - name: unit test name
+    functionName: function to call
+    byPassAnnotations: false   # optional
+    input: 42                   # single value for single-param; or object with param names as keys
+    expected: { "result": 42 }   # expected JSON result
+    assertOptions: optional     # override suite assertOptions for this test only (same formats)
+```
+
+Paths in `setup` (`islSource`, `mockSource` file names) are relative to the directory containing the `.tests.yaml` file. For full details (assertOptions reference, input format, filtering) see the [Unit Testing — YAML-Driven Test Suites](../docs/ext/unit-testing/yaml-tests.md) doc.
+
 ### Info Command
 
 Show version and system information:
@@ -313,6 +354,21 @@ isl transform step3.isl -i temp2.json -o final-result.json
 
 - `debug=true` - Enable debug output and stack traces
 
+### Command Comparison: transform, validate, test
+
+All three commands use the same **module resolution** via `IslModuleResolver`, so relative imports (e.g. `import Customer from "../customer.isl"`) work consistently:
+
+| Aspect | transform | validate | test |
+|--------|-----------|----------|------|
+| **Input** | Single script file | Single script file | Directory or file (discovers .isl with @test) |
+| **Compilation** | `IslModuleResolver.compileSingleFile()` | Same | `TransformTestPackageBuilder` with `createModuleResolver()` (uses `IslModuleResolver.resolveExternalModule`) |
+| **Module resolution** | Relative to script dir | Same | Relative to search base; checks discovered files first |
+| **Execution** | Runs specified function (default: `run`) | Runs `run` to validate | Runs @test functions (with @setup) |
+| **Context** | OperationContext + vars, input, Log | Empty OperationContext | TestOperationContext + Log, Assert, Mock |
+| **Output** | JSON/YAML result | Success message | Test results (pass/fail) |
+
+**Shared behavior:** All commands resolve `../module.isl` and `./module.isl` relative to the current script's directory. Test additionally resolves against already-discovered test files.
+
 ## Development
 
 ### Running in Development
diff --git a/isl-cmd/build.gradle.kts b/isl-cmd/build.gradle.kts
index f748278..74f6be8 100644
--- a/isl-cmd/build.gradle.kts
+++ b/isl-cmd/build.gradle.kts
@@ -17,6 +17,7 @@ dependencies {
     // ISL modules
     implementation(project(":isl-transform"))
     implementation(project(":isl-validation"))
+    implementation(project(":isl-test"))
     
     // Kotlin
     implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
@@ -47,7 +48,7 @@ dependencies {
 // Configure Shadow JAR for fat JAR creation
 tasks.shadowJar {
     archiveBaseName.set("isl")
-    archiveClassifier.set("")
+    archiveClassifier.set("all")
     archiveVersion.set(project.version.toString())
     
     manifest {
@@ -74,6 +75,7 @@ tasks.register("runIsl") {
     description = "Run ISL CLI with arguments"
     classpath = sourceSets.main.get().runtimeClasspath
     mainClass.set("com.intuit.isl.cmd.IslCommandLineKt")
+    workingDir = project.findProperty("runWorkingDir")?.toString()?.takeIf { it.isNotBlank() }?.let { file(it) } ?: rootProject.projectDir
     
     // Allow passing arguments: ./gradlew :isl-cmd:runIsl --args="script.isl"
     if (project.hasProperty("args")) {
@@ -81,6 +83,11 @@ tasks.register("runIsl") {
     }
 }
 
+// Application run task: use invocation directory when set (e.g. from isl.bat/isl.sh)
+tasks.named("run").configure {
+    workingDir = project.findProperty("runWorkingDir")?.toString()?.takeIf { it.isNotBlank() }?.let { file(it) } ?: rootProject.projectDir
+}
+
 // Configure JAR manifest
 tasks.jar {
     manifest {
@@ -92,3 +99,5 @@ tasks.jar {
     }
 }
 
+// Publishing is configured automatically by the maven publish plugin
+
diff --git a/isl-cmd/examples/README.md b/isl-cmd/examples/README.md
new file mode 100644
index 0000000..792a391
--- /dev/null
+++ b/isl-cmd/examples/README.md
@@ -0,0 +1,43 @@
+# ISL CLI Examples
+
+Sample scripts and tests for the ISL command-line interface.
+
+## Contents
+
+| File | Description |
+|------|-------------|
+| `hello.isl` | Simple greeting transform |
+| `transform.isl` | Data transformation with filter/reduce |
+| `tests/calculator.isl` | Pure functions for unit testing (add, double, greet, echo) |
+| `tests/service.isl` | Functions that use mocks (Api.Call, Config.GetLimit, Data.GetItems) |
+| `tests/calculator.tests.yaml` | YAML-driven unit tests for `calculator.isl` |
+| `tests/service.tests.yaml` | YAML-driven unit tests for `service.isl` (uses `../mocks/sample-mocks.yaml`) |
+| `tests/sample.isl` | Annotation-based tests (`@setup`, `@test`) with `@.Mock.Load` |
+| `mocks/sample-mocks.yaml` | Mock definitions (same format as `@.Mock.Load`) |
+
+## Running examples
+
+From the **examples** directory (e.g. after `cd isl-cmd/examples`):
+
+```bash
+# Transform
+isl transform hello.isl -i hello-input.json --pretty
+isl transform transform.isl -i data.json --pretty
+
+# Validate
+isl validate transform.isl
+
+# Run all tests (YAML suites + annotation-based .isl tests)
+isl test .
+```
+
+To run only a specific YAML suite:
+
+```bash
+isl test tests/calculator.tests.yaml
+isl test tests/service.tests.yaml
+```
+
+## YAML test format
+
+`*.tests.yaml` suites use: `category`, `setup.islSource`, optional `setup.mockSource` (single path or array) and `setup.mocks` (inline), optional suite-level `assertOptions`, and `tests` (or `islTests`) with `name`, `functionName`, `input`, `expected`, and optional per-test `assertOptions`. See [../README.md](../README.md#test-command) for a short reference and [../docs/ext/unit-testing/yaml-tests.md](../../docs/ext/unit-testing/yaml-tests.md) for the full format and assertOptions reference.
diff --git a/isl-cmd/examples/mocks/sample-mocks.yaml b/isl-cmd/examples/mocks/sample-mocks.yaml
new file mode 100644
index 0000000..7cb14db
--- /dev/null
+++ b/isl-cmd/examples/mocks/sample-mocks.yaml
@@ -0,0 +1,73 @@
+# Sample mock file for use with @.Mock.Load("mocks/sample-mocks.yaml")
+# Path is relative to the directory of the ISL file that loads it.
+#
+# Use in tests or scripts:
+#   @.Mock.Load("mocks/sample-mocks.yaml")
+#
+# Format: func and optional annotation arrays; each entry has name, result (or isl), and optional params.
+# Use "result" (or "return") for a static return value, or "isl" to define an ISL function that runs as the mock.
+#
+# Pipe modifiers: to mock "| name" (e.g. $x | mask2), use func with name "Modifier.[Name]" (e.g. Modifier.mask2).
+# Annotations: to mock "@name" on a function (e.g. @mask2 fun foo()), use the annotation array with name "name".
+
+func:
+  # Simple mock: same return every time
+  - name: "Api.Call"
+    result: { status: 200, body: "ok" }
+
+  # ISL mock: compile and run an ISL function in context (receives call parameters as function arguments)
+  - name: "mask"
+    isl: |
+         fun mask( $value ) {
+             return `xxxxxx$value`;
+         }
+
+  # Pipe modifier mock: "Modifier.[Name]" makes "| name" work (e.g. $x | mask2)
+  - name: "Modifier.mask2"
+    isl: |
+         fun mask2( $value ) {
+             return `xxxxxx$value`;
+         }
+
+  # Parameter-based mock: return only when arguments match (1 param)
+  - name: "Service.GetUser"
+    result: { id: 1, name: "Alice", role: "admin" }
+    params: ["user-1"]
+
+  # Two-parameter mock: match (a, b) and return computed result
+  - name: "Math.Compute"
+    result: 42
+    params: [10, 32]
+  - name: "Math.Compute"
+    result: 100
+    params: [50, 50]
+
+  # Three-parameter mock: e.g. lookup(table, key, id)
+  - name: "Lookup.Find"
+    result: { id: "u-1", name: "Alice", role: "admin" }
+    params: ["users", "id", "u-1"]
+  - name: "Lookup.Find"
+    result: null
+    params: ["users", "id", "unknown"]
+
+  # Indexed mocks: different return per call (#1 first call, #2 second, etc.)
+  - name: "Data.GetItems#1"
+    result: [ { id: 1 }, { id: 2 }, { id: 3 } ]
+  - name: "Data.GetItems#2"
+    result: [ { id: 4 }, { id: 5 } ]
+  - name: "Data.GetItems#3"
+    result: []
+
+  # Scalar and null returns
+  - name: "Config.GetLimit"
+    result: 100
+  - name: "Feature.IsEnabled"
+    result: true
+
+# Annotation mocks: for @name on a function (e.g. @mask2 fun foo() { ... })
+# These mock the annotation decorator; use "Modifier.name" in func to mock the pipe "| name".
+annotation:
+  - name: "mask2"
+    result: "masked"
+  # - name: "cache#1"
+  #   result: "cached-value"
diff --git a/isl-cmd/examples/run-examples.bat b/isl-cmd/examples/run-examples.bat
index 1ee617e..7551770 100644
--- a/isl-cmd/examples/run-examples.bat
+++ b/isl-cmd/examples/run-examples.bat
@@ -1,16 +1,26 @@
 @echo off
 REM Example usage scripts for ISL CLI on Windows
 
+cd /d "%~dp0"
+
 echo === ISL CLI Examples ===
 echo.
 
-REM Check if JAR exists
-set JAR=..\build\libs\isl-2.4.20-SNAPSHOT.jar
+REM Read version from gradle.properties
+for /f "tokens=1,2 delims==" %%a in (..\..\gradle.properties) do (
+    if "%%a"=="version" set VERSION=%%b
+)
+
+REM Shadow JAR has classifier -all
+set JAR=..\build\libs\isl-%VERSION%-all.jar
+if not exist "%JAR%" set JAR=..\build\libs\isl-%VERSION%.jar
 if not exist "%JAR%" (
     echo Building ISL CLI...
     cd ..
     call gradlew.bat shadowJar
     cd examples
+    set JAR=..\build\libs\isl-%VERSION%-all.jar
+    if not exist "%JAR%" set JAR=..\build\libs\isl-%VERSION%.jar
 )
 
 set ISL=java -jar %JAR%
@@ -43,5 +53,11 @@ echo 5. Show Info
 echo    Command: isl info
 %ISL% info
 echo.
+echo.
+
+echo 6. Run Tests (ISL + YAML suites)
+echo    Command: isl test .
+%ISL% test .
+echo.
 
 
diff --git a/isl-cmd/examples/run-examples.sh b/isl-cmd/examples/run-examples.sh
index fd8ed99..cc60274 100644
--- a/isl-cmd/examples/run-examples.sh
+++ b/isl-cmd/examples/run-examples.sh
@@ -8,13 +8,21 @@ echo ""
 # Ensure we're in the right directory
 cd "$(dirname "$0")"
 
-# Check if JAR exists
-JAR="../build/libs/isl-2.4.20-SNAPSHOT.jar"
+# Read version from gradle.properties
+VERSION=$(grep "^version=" "../../gradle.properties" | cut -d'=' -f2 | tr -d '\r')
+
+# Shadow JAR has classifier -all
+JAR="../build/libs/isl-$VERSION-all.jar"
+if [ ! -f "$JAR" ]; then
+  JAR="../build/libs/isl-$VERSION.jar"
+fi
 if [ ! -f "$JAR" ]; then
     echo "Building ISL CLI..."
     cd ..
     ./gradlew shadowJar
     cd examples
+    JAR="../build/libs/isl-$VERSION-all.jar"
+    [ ! -f "$JAR" ] && JAR="../build/libs/isl-$VERSION.jar"
 fi
 
 ISL="java -jar $JAR"
@@ -47,5 +55,11 @@ echo "5. Show Info"
 echo "   Command: isl info"
 $ISL info
 echo ""
+echo ""
+
+echo "6. Run Tests (ISL + YAML suites)"
+echo "   Command: isl test ."
+$ISL test .
+echo ""
 
 
diff --git a/isl-cmd/examples/tests/calculator.isl b/isl-cmd/examples/tests/calculator.isl
new file mode 100644
index 0000000..508a926
--- /dev/null
+++ b/isl-cmd/examples/tests/calculator.isl
@@ -0,0 +1,18 @@
+// Sample ISL module for unit testing (no mocks required).
+// Run with: isl test .  (discovers calculator.tests.yaml)
+
+fun add($a, $b) {
+  return {{ $a + $b }};
+}
+
+fun double($x) {
+  return {{ $x * 2 }};
+}
+
+fun greet($name) {
+  return `Hello, ${ $name }`;
+}
+
+fun echo($value) {
+  return $value;
+}
diff --git a/isl-cmd/examples/tests/calculator.tests.yaml b/isl-cmd/examples/tests/calculator.tests.yaml
new file mode 100644
index 0000000..1c79bb8
--- /dev/null
+++ b/isl-cmd/examples/tests/calculator.tests.yaml
@@ -0,0 +1,43 @@
+# YAML-driven unit tests for calculator.isl
+# Run: isl test .   or   isl test calculator.tests.yaml
+
+category: calculator
+setup:
+  islSource: calculator.isl
+
+tests:
+  - name: add two numbers
+    functionName: add
+    input:
+      a: 2
+      b: 3
+    expected: 5
+
+  - name: add negatives
+    functionName: add
+    input:
+      a: -10
+      b: 5
+    expected: -5
+
+  - name: double a number
+    functionName: double
+    input: 7
+    expected: 14
+
+  - name: double zero
+    functionName: double
+    input: 0
+    expected: 0
+
+  - name: greet returns message
+    functionName: greet
+    input: "World"
+    expected: "Hello, World"
+
+  - name: echo returns input
+    functionName: echo
+    input: { foo: "bar", n: 42 }
+    expected:
+      foo: "bar"
+      n: 42
diff --git a/isl-cmd/examples/tests/sample.isl b/isl-cmd/examples/tests/sample.isl
new file mode 100644
index 0000000..6fd65ec
--- /dev/null
+++ b/isl-cmd/examples/tests/sample.isl
@@ -0,0 +1,19 @@
+// Annotation-based tests (@setup and @test) – discovered by: isl test .
+// Uses @.Mock.Load for mocks; assertions via @.Assert.
+
+@setup
+fun setupTests() {
+  @.Mock.Load("../mocks/sample-mocks.yaml")
+}
+
+@test
+fun testApiCallMock() {
+  $r : @.Api.Call("https://example.com")
+  @.Assert.Equal({ status: 200, body: "ok" }, $r)
+}
+
+@test
+fun testConfigLimit() {
+  $limit : @.Config.GetLimit()
+  @.Assert.Equal(100, $limit)
+}
diff --git a/isl-cmd/examples/tests/service.isl b/isl-cmd/examples/tests/service.isl
new file mode 100644
index 0000000..73002c8
--- /dev/null
+++ b/isl-cmd/examples/tests/service.isl
@@ -0,0 +1,35 @@
+// Sample ISL module that calls an external "Api.Call" (mocked in tests).
+// Run with: isl test service.tests.yaml
+
+fun run($url) {
+  $r : @.Api.Call($url);
+  return $r;
+}
+
+fun getConfigLimit() {
+  $limit : @.Config.GetLimit();
+  return $limit;
+}
+
+fun getItems() {
+  $first : @.Data.GetItems();
+  $second : @.Data.GetItems();
+  $third : @.Data.GetItems();
+  return {
+    first: $first,
+    second: $second,
+    third: $third
+  };
+}
+
+// Two-parameter mocked function
+fun computeSum($a, $b) {
+  $r : @.Math.Compute($a, $b);
+  return $r;
+}
+
+// Three-parameter mocked function
+fun lookup($table, $key, $id) {
+  $r : @.Lookup.Find($table, $key, $id);
+  return $r;
+}
diff --git a/isl-cmd/examples/tests/service.tests.yaml b/isl-cmd/examples/tests/service.tests.yaml
new file mode 100644
index 0000000..66b69b4
--- /dev/null
+++ b/isl-cmd/examples/tests/service.tests.yaml
@@ -0,0 +1,65 @@
+# YAML-driven unit tests for service.isl (uses mocks)
+# Run: isl test .   or   isl test service.tests.yaml
+#
+# mockSource is loaded first; mocks below override or add.
+
+category: service
+setup:
+  islSource: service.isl
+  mockSource: ../mocks/sample-mocks.yaml
+  mocks:
+    func:
+      - name: "Api.Call"
+        return: { status: 200, body: "overridden" }
+
+tests:
+  - name: run returns mocked Api.Call response
+    functionName: run
+    input: "https://example.com"
+    expected:
+      status: 200
+      body: "overridden"
+
+  - name: getConfigLimit returns mocked scalar
+    functionName: getConfigLimit
+    expected: 100
+
+  - name: getItems returns indexed mock results
+    functionName: getItems
+    expected:
+      first: [ { id: 1 }, { id: 2 }, { id: 3 } ]
+      second: [ { id: 4 }, { id: 5 } ]
+      third: []
+
+  - name: computeSum two params match first mock (10, 32)
+    functionName: computeSum
+    input:
+      a: 10
+      b: 32
+    expected: 42
+
+  - name: computeSum two params match second mock (50, 50)
+    functionName: computeSum
+    input:
+      a: 50
+      b: 50
+    expected: 100
+
+  - name: lookup three params returns user
+    functionName: lookup
+    input:
+      table: "users"
+      key: "id"
+      id: "u-1"
+    expected:
+      id: "u-1"
+      name: "Alice"
+      role: "admin"
+
+  - name: lookup three params returns null for unknown
+    functionName: lookup
+    input:
+      table: "users"
+      key: "id"
+      id: "unknown"
+    expected: null
diff --git a/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/IslCommandLine.kt b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/IslCommandLine.kt
index 96ad028..478cfa1 100644
--- a/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/IslCommandLine.kt
+++ b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/IslCommandLine.kt
@@ -18,7 +18,8 @@ import kotlin.system.exitProcess
     subcommands = [
         TransformCommand::class,
         ValidateCommand::class,
-        InfoCommand::class
+        InfoCommand::class,
+        TestCommand::class
     ]
 )
 class IslCommandLine : Runnable {   
@@ -28,6 +29,7 @@ class IslCommandLine : Runnable {
 }
 
 fun main(args: Array) {
+    Transformer.getIslInfo()  // Log ISL and Jackson versions at startup
     val exitCode = CommandLine(IslCommandLine()).execute(*args)
     exitProcess(exitCode)
 }
diff --git a/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/IslModuleResolver.kt b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/IslModuleResolver.kt
new file mode 100644
index 0000000..d71fca1
--- /dev/null
+++ b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/IslModuleResolver.kt
@@ -0,0 +1,109 @@
+package com.intuit.isl.cmd
+
+import com.intuit.isl.runtime.FileInfo
+import com.intuit.isl.runtime.TransformCompilationException
+import com.intuit.isl.runtime.TransformPackageBuilder
+import java.nio.file.Path
+import java.nio.file.Paths
+
+/**
+ * Shared module resolution for ISL commands (transform, validate, test).
+ * Resolves relative imports (e.g. `import Customer from "../customer.isl"`) consistently
+ * across all commands that compile ISL scripts.
+ *
+ * When [resolvedPaths] is provided, the directory of the **importing** module is taken from
+ * where that module was actually loaded (so e.g. coreUtils.isl is resolved relative to
+ * lib/isl_common_utils/ when imported from bankingUtils.isl in that folder), not relative to basePath.
+ */
+object IslModuleResolver {
+
+    /**
+     * Resolves an import path relative to the importing module's directory.
+     * @param basePath Base path (script directory or search root)
+     * @param fromModule Current module name/path as known by the runtime (e.g. "lib/isl_common_utils/bankingUtils.isl")
+     * @param dependentModule Import path from the script (e.g. "coreUtils.isl" or "../other.isl")
+     * @param resolvedPaths Optional map of module name -> absolute path where that module was loaded.
+     *                      When provided, fromDir is taken from the path of fromModule if present,
+     *                      and each resolved dependency is recorded so nested imports resolve correctly.
+     * @return File contents or null if not found
+     */
+    fun resolveExternalModule(
+        basePath: Path,
+        fromModule: String,
+        dependentModule: String,
+        resolvedPaths: MutableMap? = null
+    ): String? {
+        val base = basePath.toAbsolutePath().normalize()
+        val fromDir = when {
+            resolvedPaths != null && resolvedPaths.containsKey(fromModule) -> resolvedPaths[fromModule]!!.parent
+            else -> base.resolve(fromModule).parent ?: base
+        }
+        if (TestRunFlags.shouldShowScriptLogs()) println("[ISL resolve] fromModule=$fromModule, dependentModule=$dependentModule, fromDir=$fromDir")
+        val candidateNames = if (dependentModule.endsWith(".isl", ignoreCase = true)) {
+            listOf(dependentModule)
+        } else {
+            listOf("$dependentModule.isl", "$dependentModule.ISL")
+        }
+        for (name in candidateNames) {
+            val candidatePath = fromDir.resolve(name).normalize().toAbsolutePath()
+            val file = candidatePath.toFile()
+            if (file.exists() && file.isFile) {
+                resolvedPaths?.set(dependentModule, candidatePath)
+                return file.readText()
+            }
+        }
+        val moduleBaseName = if (dependentModule.endsWith(".isl", ignoreCase = true)) {
+            dependentModule.dropLast(4)
+        } else {
+            dependentModule
+        }
+        val found = base.toFile().walkTopDown()
+            .filter { it.isFile && it.extension.equals("isl", true) }
+            .find { it.nameWithoutExtension.equals(moduleBaseName, true) }
+        if (found != null && resolvedPaths != null) resolvedPaths[dependentModule] = found.toPath().toAbsolutePath().normalize()
+        return found?.readText()
+    }
+
+    /**
+     * Creates a findExternalModule that records where each module was loaded so nested imports
+     * resolve relative to the importing file's actual directory.
+     * @param basePath Base path for resolution when a module is not yet in the map
+     * @param resolvedPaths Map to fill: module name -> path where it was loaded. Prime with initial module(s) before use.
+     * @return BiFunction suitable for TransformPackageBuilder
+     */
+    fun createModuleFinder(
+        basePath: Path,
+        resolvedPaths: MutableMap
+    ): java.util.function.BiFunction {
+        return java.util.function.BiFunction { fromModule, dependentModule ->
+            resolveExternalModule(basePath, fromModule, dependentModule, resolvedPaths)
+                ?: throw TransformCompilationException(
+                    "Could not find module '$dependentModule' (imported from $fromModule). Searched relative to ${resolvedPaths[fromModule]?.parent ?: basePath.resolve(fromModule).parent}"
+                )
+        }
+    }
+
+    /**
+     * Creates a findExternalModule for TransformPackageBuilder when compiling a single ISL file.
+     * Uses resolution history so nested imports (e.g. lib/foo.isl importing bar.isl) resolve relative to the importing file.
+     */
+    fun buildPackageForSingleFile(scriptFile: java.io.File, scriptContent: String): Pair> {
+        if (TestRunFlags.shouldShowScriptLogs()) println("[ISL load] initial file: ${scriptFile.absolutePath}")
+        val basePath = scriptFile.parentFile?.toPath()?.normalize() ?: Paths.get(".").toAbsolutePath().normalize()
+        val moduleName = scriptFile.name
+        val fileInfo = FileInfo(moduleName, scriptContent)
+        val resolvedPaths = mutableMapOf()
+        resolvedPaths[moduleName] = scriptFile.toPath().toAbsolutePath().normalize()
+        val findExternalModule = createModuleFinder(basePath, resolvedPaths)
+        return fileInfo to findExternalModule
+    }
+
+    /**
+     * Compiles a single ISL file with dependent module resolution.
+     * Returns the TransformPackage for the compiled module.
+     */
+    fun compileSingleFile(scriptFile: java.io.File, scriptContent: String): com.intuit.isl.runtime.TransformPackage {
+        val (fileInfo, findExternalModule) = buildPackageForSingleFile(scriptFile, scriptContent)
+        return TransformPackageBuilder().build(mutableListOf(fileInfo), findExternalModule)
+    }
+}
diff --git a/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/LogExtensions.kt b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/LogExtensions.kt
new file mode 100644
index 0000000..43c51f7
--- /dev/null
+++ b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/LogExtensions.kt
@@ -0,0 +1,86 @@
+package com.intuit.isl.cmd
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.SerializationFeature
+import com.intuit.isl.common.FunctionExecuteContext
+import com.intuit.isl.common.IOperationContext
+import com.intuit.isl.utils.ConvertUtils
+import com.intuit.isl.utils.JsonConvert
+
+/**
+ * Log extension functions for ISL scripts when running transforms or tests from the command line.
+ *
+ * Usage in ISL:
+ *   @.Log.Info("Processing item", $count)
+ *   @.Log.Warn("Unexpected value:", $value)
+ *   @.Log.Error("Failed:", $error)
+ *   @.Log.Debug("Debug info")  // Only outputs when -Ddebug=true
+ */
+object LogExtensions {
+    fun registerExtensions(context: IOperationContext) {
+        context.registerExtensionMethod("Log.Debug", LogExtensions::debug)
+        context.registerExtensionMethod("Log.Info", LogExtensions::info)
+        context.registerExtensionMethod("Log.Warn", LogExtensions::warn)
+        context.registerExtensionMethod("Log.Error", LogExtensions::error)
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    private fun debug(context: FunctionExecuteContext): Any? {
+        if (System.getProperty("debug") != "true") return null
+        val message = formatMessage(context.parameters)
+        val loc = logLocation(context)
+        println("[ISL Log $loc] $message")
+        return null
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    private fun info(context: FunctionExecuteContext): Any? {
+        val message = formatMessage(context.parameters)
+        val loc = logLocation(context)
+        println("[ISL Log $loc] $message")
+        return null
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    private fun warn(context: FunctionExecuteContext): Any? {
+        val message = formatMessage(context.parameters)
+        val loc = logLocation(context)
+        System.err.println("[ISL Log $loc] $message")
+        return null
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    private fun error(context: FunctionExecuteContext): Any? {
+        val message = formatMessage(context.parameters)
+        val loc = logLocation(context)
+        System.err.println("[ISL Log $loc] $message")
+        return null
+    }
+
+    private fun logLocation(context: FunctionExecuteContext): String {
+        val pos = context.command.token.position
+        return "[${pos.file}]:${pos.line}"
+    }
+
+    private fun formatMessage(parameters: Array<*>): String {
+        if (parameters.isEmpty()) return ""
+        return parameters.joinToString(" ") { param ->
+            when (param) {
+                null -> "null"
+                is JsonNode -> param.toPrettyString()
+                else -> ConvertUtils.tryToString(param) ?: param.toString()
+            }
+        }
+    }
+
+    private fun JsonNode.toPrettyString(): String {
+        return try {
+            JsonConvert.mapper
+                .copy()
+                .enable(SerializationFeature.INDENT_OUTPUT)
+                .writeValueAsString(this)
+        } catch (_: Exception) {
+            this.toString()
+        }
+    }
+}
diff --git a/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/TestCommand.kt b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/TestCommand.kt
new file mode 100644
index 0000000..5407834
--- /dev/null
+++ b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/TestCommand.kt
@@ -0,0 +1,462 @@
+package com.intuit.isl.cmd
+
+import com.fasterxml.jackson.databind.SerializationFeature
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.intuit.isl.runtime.FileInfo
+import com.intuit.isl.runtime.TransformCompilationException
+import com.intuit.isl.test.TransformTestPackageBuilder
+import com.intuit.isl.test.annotations.TestResult
+import com.intuit.isl.test.annotations.TestResultContext
+import picocli.CommandLine.Command
+import picocli.CommandLine.Option
+import picocli.CommandLine.Parameters
+import java.io.File
+import java.nio.file.FileSystems
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.io.path.extension
+import kotlin.io.path.isRegularFile
+import kotlin.io.path.nameWithoutExtension
+import kotlin.system.exitProcess
+
+/**
+ * Command to execute ISL tests.
+ * Discovers and runs:
+ * - .isl files containing @setup or @test annotations
+ * - *.tests.yaml files (YAML-driven unit test suites with setup.islSource, mockSource, mocks, and tests with functionName, input, expected)
+ */
+@Command(
+    name = "test",
+    aliases = ["tests"],
+    description = ["Execute ISL tests from the specified path or current folder. Runs .isl files with @setup/@test and *.tests.yaml suites. Examples: isl test .  |  isl test tests/  |  isl test calculator.tests.yaml  |  isl test . -f add"]
+)
+class TestCommand : Runnable {
+
+    @Parameters(
+        index = "0",
+        arity = "0..1",
+        description = ["Path to search for tests: directory, single file, or glob. Examples: . (current dir), tests/, calculator.tests.yaml. Default: current directory"]
+    )
+    var path: File? = null
+
+    @Option(
+        names = ["--glob"],
+        description = ["Glob for .isl files when path is a directory (e.g. **/*.isl). YAML suites (*.tests.yaml) use **/*.tests.yaml when not set"]
+    )
+    var globPattern: String? = null
+
+    @Option(
+        names = ["-o", "--output"],
+        description = ["Write results to a JSON file for parsing by other tools"]
+    )
+    var outputFile: File? = null
+
+    @Option(
+        names = ["-f", "--function"],
+        description = ["Run only the specified test function(s). Can be specified multiple times. Use 'file:function' to target a specific file (e.g. sample.isl:test_customer)"]
+    )
+    var functions: Array = emptyArray()
+
+    @Option(
+        names = ["-v", "--verbose"],
+        description = ["Show detailed logs (search, loading, mocks, per-test progress). Without this, only test name, result, and a summary are shown"]
+    )
+    var verbose: Boolean = false
+
+    @Option(
+        names = ["--report"],
+        arity = "0..1",
+        paramLabel = "FILE",
+        description = ["Write a Markdown test report to FILE. FILE is optional: if given, the report is written there (can be used with or without -o/--output). Summary at top, detailed results below. Example: --report test-report.md"]
+    )
+    var reportFile: File? = null
+
+    override fun run() {
+        TestRunFlags.setTestVerbose(verbose)
+        try {
+            val basePath = (path?.absoluteFile ?: File(System.getProperty("user.dir"))).toPath().normalize()
+            val searchBase = if (basePath.toFile().isDirectory) basePath else basePath.parent
+            if (verbose) {
+                when {
+                    basePath.toFile().isFile -> println("[ISL Search] Searching: ${basePath.toAbsolutePath()}")
+                    else -> {
+                        val islGlob = globPattern ?: "**/*.isl"
+                        println("[ISL Search] Searching: ${basePath.toAbsolutePath()} (ISL: $islGlob, YAML: **/*.tests.yaml)")
+                    }
+                }
+            }
+            val testFiles = discoverTestFiles(basePath)
+            val yamlSuites = discoverYamlTestSuites(basePath)
+            if (testFiles.isEmpty() && yamlSuites.isEmpty()) {
+                System.err.println(red("[ISL Error] No test files found (looking for .isl with @setup/@test, or *.tests.yaml). Try: isl test   or  isl test path/to/suite.tests.yaml"))
+                exitProcess(1)
+            }
+            val result = TestResultContext()
+            val contextCustomizers: List<(com.intuit.isl.common.IOperationContext) -> Unit> = listOf(
+                { ctx -> LogExtensions.registerExtensions(ctx) },
+                { ctx -> TestExtensions.registerExtensions(ctx) }
+            )
+            val functionFilter = functions.map { it.trim() }.filter { it.isNotEmpty() }.toSet()
+
+            if (testFiles.isNotEmpty()) {
+                if (verbose) println("[ISL Loading] Found ${testFiles.size} ISL test file(s)")
+                val fileInfos = testFiles.map { (filePath, content) ->
+                    val moduleName = searchBase.relativize(filePath).toString().replace("\\", "/")
+                    FileInfo(moduleName, content)
+                }.toMutableList()
+                val findExternalModule = createModuleResolver(testFiles, searchBase)
+                try {
+                    @Suppress("UNCHECKED_CAST")
+                    val testPackage = TransformTestPackageBuilder().build(
+                        fileInfos,
+                        findExternalModule as java.util.function.BiFunction,
+                        searchBase,
+                        contextCustomizers
+                    )
+                    if (functionFilter.isEmpty()) {
+                        testPackage.runAllTests(result)
+                    } else {
+                        testPackage.runFilteredTests(result) { file, func ->
+                            functionFilter.any { filter ->
+                                when {
+                                    filter.contains(":") -> {
+                                        val parts = filter.split(":", limit = 2)
+                                        val fileMatch = parts[0].equals(file, true) ||
+                                            parts[0].equals(file.removeSuffix(".isl"), true)
+                                        fileMatch && parts[1].equals(func, true)
+                                    }
+                                    else -> filter.equals(func, true)
+                                }
+                            }
+                        }
+                    }
+                } catch (e: Exception) {
+                    result.testResults.addAll(createErrorResult(e, fileInfos).testResults)
+                }
+            }
+
+            if (yamlSuites.isNotEmpty()) {
+                if (verbose) println("[ISL Loading] Found ${yamlSuites.size} YAML test suite(s)")
+                for (yamlPath in yamlSuites) {
+                    val suiteBase = if (yamlPath.toFile().isFile) yamlPath.parent else yamlPath
+                    YamlUnitTestRunner.runSuite(yamlPath, suiteBase, result, contextCustomizers, functionFilter, verbose)
+                }
+            }
+
+            if (result.testResults.isEmpty()) {
+                System.err.println(red("[ISL Error] No tests ran. Check path and --function filter."))
+                exitProcess(1)
+            }
+            // Always print results to console first; file output is in addition to, not instead of, logs
+            reportResults(result, verbose)
+            outputFile?.let { writeResultsToJson(result, it) }
+            reportFile?.let { writeReportMarkdown(result, it) }
+            val failedCount = result.testResults.count { !it.success }
+            if (failedCount > 0) {
+                exitProcess(1)
+            }
+        } catch (e: Exception) {
+            System.err.println(red("[ISL Error] Error: ${e.message}"))
+            if (System.getProperty("debug") == "true") {
+                e.printStackTrace()
+            }
+            exitProcess(1)
+        } finally {
+            TestRunFlags.clear()
+        }
+    }
+
+    private fun discoverTestFiles(basePath: Path): List> {
+        val islFiles = when {
+            basePath.toFile().isFile -> {
+                if (basePath.toString().endsWith(".isl", true)) listOf(basePath) else emptyList()
+            }
+            basePath.toFile().isDirectory -> {
+                val pattern = globPattern ?: "**/*.isl"
+                val matcher = FileSystems.getDefault().getPathMatcher("glob:$pattern")
+                Files.walk(basePath)
+                    .use { stream ->
+                        stream
+                            .filter { it.isRegularFile() && it.extension.equals("isl", true) }
+                            .filter { path ->
+                                val relative = basePath.relativize(path)
+                                val normalized = relative.toString().replace("\\", "/")
+                                globPattern == null || matcher.matches(FileSystems.getDefault().getPath(normalized))
+                            }
+                            .toList()
+                    }
+            }
+            else -> emptyList()
+        }
+        return islFiles
+            .mapNotNull { path ->
+                val content = path.toFile().readText()
+                if (content.contains("@setup") || content.contains("@test")) {
+                    path to content
+                } else null
+            }
+    }
+
+    private fun discoverYamlTestSuites(basePath: Path): List {
+        return when {
+            basePath.toFile().isFile -> {
+                if (basePath.toString().endsWith(".tests.yaml", true) || basePath.toString().endsWith(".tests.yml", true)) {
+                    listOf(basePath)
+                } else emptyList()
+            }
+            basePath.toFile().isDirectory -> {
+                val pattern = globPattern ?: "**/*.tests.yaml"
+                val matcher = FileSystems.getDefault().getPathMatcher("glob:$pattern")
+                Files.walk(basePath)
+                    .use { stream ->
+                        stream
+                            .filter { it.isRegularFile() }
+                            .filter { path ->
+                                val ext = path.extension.lowercase()
+                                (ext == "yaml" || ext == "yml") && path.nameWithoutExtension.endsWith(".tests", ignoreCase = true)
+                            }
+                            .filter { path ->
+                                val relative = basePath.relativize(path)
+                                val normalized = relative.toString().replace("\\", "/")
+                                globPattern == null || matcher.matches(FileSystems.getDefault().getPath(normalized))
+                            }
+                            .toList()
+                    }
+            }
+            else -> emptyList()
+        }
+    }
+
+    private fun createModuleResolver(testFiles: List>, searchBase: Path): java.util.function.BiFunction {
+        val fileByModuleName = testFiles.associate { (filePath, content) ->
+            val moduleName = searchBase.relativize(filePath).toString().replace("\\", "/").removeSuffix(".isl")
+            moduleName to content
+        }
+        val fileByFullName = testFiles.associate { (filePath, content) ->
+            searchBase.relativize(filePath).toString().replace("\\", "/") to content
+        }
+        val resolvedPaths = mutableMapOf()
+        testFiles.forEach { (filePath, _) ->
+            val fullName = searchBase.relativize(filePath).toString().replace("\\", "/")
+            resolvedPaths[fullName] = filePath.toAbsolutePath().normalize()
+        }
+        return java.util.function.BiFunction { fromModule: String, dependentModule: String ->
+            fileByFullName[dependentModule]
+                ?: fileByModuleName[dependentModule]
+                ?: IslModuleResolver.resolveExternalModule(searchBase, fromModule, dependentModule, resolvedPaths)
+                ?: throw TransformCompilationException(
+                    "Could not find module '$dependentModule' (imported from $fromModule). Searched relative to ${resolvedPaths[fromModule]?.parent ?: searchBase.resolve(fromModule).parent}"
+                )
+        }
+    }
+
+    private fun reportResults(result: TestResultContext, verbose: Boolean) {
+        val passed = result.testResults.count { it.success }
+        val failed = result.testResults.count { !it.success }
+        val total = result.testResults.size
+
+        //if (verbose) {
+            val byGroup = result.testResults.groupBy { it.testGroup ?: it.testFile }
+            byGroup.forEach { (group, tests) ->
+                println("[ISL Result]   $group")
+                tests.forEach { tr ->
+                    val displayName = if (tr.testName != tr.functionName) "${tr.testName} (${tr.functionName})" else tr.testName
+                    if (tr.success) {
+                        println("[ISL Result]     ${green("[PASS]")} $displayName")
+                    } else {
+                        println("[ISL Result]     ${red("[FAIL]")} $displayName")
+                        tr.message?.let { println("[ISL Result]         ${red(it)}") }
+                        tr.errorPosition?.let { pos ->
+                            val loc = "${pos.file}:${pos.line}:${pos.column}"
+                            println("[ISL Result]         ${red("at $loc")}")
+                        }
+                    }
+                }
+            }
+            println("[ISL Result] ---")
+            val resultsLine = "Results: $passed passed, $failed failed, $total total"
+            println(if (failed > 0) red("[ISL Result] $resultsLine") else "[ISL Result] $resultsLine")
+        //} else {
+            
+            // Nice summary
+          //  printSummary(passed, failed, total)
+        //}
+    }
+
+    private fun printSummary(passed: Int, failed: Int, total: Int) {
+        val summaryText = if (failed == 0) {
+            "All tests passed ($total total)"
+        } else {
+            "$failed failed, $passed passed ($total total)"
+        }
+        val summary = if (failed == 0) {
+            "${green("All tests passed")} ($total total)"
+        } else {
+            "${red("$failed failed")}, $passed passed ($total total)"
+        }
+        val contentWidth = summaryText.length.coerceAtLeast(28)
+        val line = "─".repeat(contentWidth + 4)
+        val padding = " ".repeat((contentWidth - summaryText.length).coerceAtLeast(0))
+        println()
+        println("┌$line┐")
+        println("│  $summary$padding  │")
+        println("└$line┘")
+    }
+
+    /** Use ANSI color only when stdout is a TTY (e.g. terminal). When piped (e.g. from VS Code Test Explorer), output is plain text. */
+    private fun useColor(): Boolean = System.console() != null
+
+    private fun green(text: String) = if (useColor()) "\u001B[32m$text\u001B[0m" else text
+    private fun red(text: String) = if (useColor()) "\u001B[31m$text\u001B[0m" else text
+
+    private fun createErrorResult(e: Exception, fileInfos: List): TestResultContext {
+        val (message, position) = when (e) {
+            is TransformCompilationException -> e.message to e.position
+            is com.intuit.isl.runtime.TransformException -> e.message to e.position
+            is com.intuit.isl.runtime.IslException -> e.message to e.position
+            else -> e.message to null
+        }
+        val firstFile = fileInfos.firstOrNull()?.name ?: "unknown"
+        val errorResult = TestResult(
+            testFile = position?.file ?: firstFile,
+            functionName = "compilation",
+            testName = "compilation",
+            testGroup = firstFile,
+            success = false,
+            message = message ?: e.toString(),
+            errorPosition = position
+        )
+        return TestResultContext(mutableListOf(errorResult))
+    }
+
+    private fun writeResultsToJson(result: TestResultContext, file: File) {
+        val passed = result.testResults.count { it.success }
+        val failed = result.testResults.count { !it.success }
+        val results = result.testResults.map { tr ->
+            mapOf(
+                "testFile" to tr.testFile,
+                "functionName" to tr.functionName,
+                "testName" to tr.testName,
+                "testGroup" to tr.testGroup,
+                "success" to tr.success,
+                "message" to tr.message,
+                "errorPosition" to (tr.errorPosition?.let { pos ->
+                    mapOf(
+                        "file" to pos.file,
+                        "line" to pos.line,
+                        "column" to pos.column
+                    )
+                })
+            )
+        }
+        val output = mapOf(
+            "passed" to passed,
+            "failed" to failed,
+            "total" to result.testResults.size,
+            "success" to (failed == 0),
+            "results" to results
+        )
+        val mapper = jacksonObjectMapper().enable(SerializationFeature.INDENT_OUTPUT)
+        file.writeText(mapper.writeValueAsString(output))
+        println("[ISL Output] Results written to: ${file.absolutePath}")
+    }
+
+    private fun writeReportMarkdown(result: TestResultContext, reportFile: File) {
+        val passed = result.testResults.count { it.success }
+        val failed = result.testResults.count { !it.success }
+        val total = result.testResults.size
+        val success = failed == 0
+
+        val md = buildString {
+            // Title and summary at top
+            appendLine("# ISL Test Report")
+            appendLine()
+            appendLine("**Generated:** ${java.time.Instant.now().atZone(java.time.ZoneId.systemDefault()).format(java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME)}")
+            appendLine()
+            appendLine("## Summary")
+            appendLine()
+            appendLine("| | Count |")
+            appendLine("|---|------|")
+            appendLine("| **Total** | $total |")
+            appendLine("| **Passed** | $passed |")
+            appendLine("| **Failed** | $failed |")
+            appendLine("| **Status** | ${if (success) "✅ All passed" else "❌ $failed failed"} |")
+            appendLine()
+            appendLine("---")
+            appendLine()
+            appendLine("## Detailed Results")
+            appendLine()
+
+            val byGroup = result.testResults.groupBy { it.testGroup ?: it.testFile }
+            for ((group, tests) in byGroup) {
+                appendLine("### $group")
+                appendLine()
+                for (tr in tests) {
+                    val fileLabel = "`${tr.testFile.replace("`", "\\`")}`"
+                    val displayName = if (tr.testName != tr.functionName) "${tr.testName} (`${tr.functionName}`)" else tr.testName
+                    val line = "$fileLabel — $displayName"
+                    if (tr.success) {
+                        appendLine("- ✅ **$line**")
+                    } else {
+                        appendLine("- ❌ **$line**")
+                        val hasExpectedActual = tr.expectedJson != null && tr.actualJson != null
+                        tr.message?.let { msg ->
+                            appendLine("  - *${escapeMarkdownInline(msg.lines().first().trim())}*")
+                            if (hasExpectedActual) {
+                                appendLine()
+                                appendLine("  **Expected:**")
+                                appendLine("  ```json")
+                                prettyJson(tr.expectedJson!!).lines().forEach { appendLine("  $it") }
+                                appendLine("  ```")
+                                appendLine()
+                                appendLine("  **Actual:**")
+                                appendLine("  ```json")
+                                prettyJson(tr.actualJson!!).lines().forEach { appendLine("  $it") }
+                                appendLine("  ```")
+                                val diffs = tr.comparisonDiffs
+                                if (!diffs.isNullOrEmpty()) {
+                                    appendLine()
+                                    appendLine("  **Differences:**")
+                                    for (d in diffs) {
+                                        appendLine()
+                                        appendLine("  **Expected:**")
+                                        appendLine("  ```")
+                                        appendLine("  ${d.path} = ${d.expectedValue}")
+                                        appendLine("  ```")
+                                        appendLine("  **Actual:**")
+                                        appendLine("  ```")
+                                        appendLine("  ${d.path} = ${d.actualValue}")
+                                        appendLine("  ```")
+                                    }
+                                }
+                            } else if (msg.lines().size > 1) {
+                                appendLine("  ```")
+                                msg.lines().take(20).forEach { appendLine(it) }
+                                if (msg.lines().size > 20) appendLine("  ...")
+                                appendLine("  ```")
+                            }
+                        }
+                        tr.errorPosition?.let { pos ->
+                            appendLine("  - `${pos.file}:${pos.line}:${pos.column}`")
+                        }
+                    }
+                }
+                appendLine()
+            }
+        }
+        reportFile.writeText(md)
+        println("[ISL Output] Report written to: ${reportFile.absolutePath}")
+    }
+
+    private fun escapeMarkdownInline(s: String): String =
+        s.replace("\\", "\\\\").replace("`", "\\`").replace("*", "\\*").replace("_", "\\_")
+
+    private fun prettyJson(json: String): String {
+        return try {
+            val tree = jacksonObjectMapper().readTree(json)
+            jacksonObjectMapper().enable(SerializationFeature.INDENT_OUTPUT).writeValueAsString(tree)
+        } catch (_: Exception) {
+            json
+        }
+    }
+}
diff --git a/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/TestExtensions.kt b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/TestExtensions.kt
new file mode 100644
index 0000000..4b691be
--- /dev/null
+++ b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/TestExtensions.kt
@@ -0,0 +1,30 @@
+package com.intuit.isl.cmd
+
+import com.intuit.isl.common.FunctionExecuteContext
+import com.intuit.isl.common.IOperationContext
+
+/**
+ * Exception thrown by @.Test.Exit(...) to signal an early test exit with a result.
+ * The YAML test runner catches this and uses [result] as the test result for comparison with expected.
+ */
+class TestExitException(val result: Any?) : RuntimeException("Test.Exit with result")
+
+/**
+ * Test extension functions for ISL scripts when running tests from the command line.
+ *
+ * Usage in ISL:
+ *   @.Test.Exit()           // exit with null result
+ *   @.Test.Exit($value)     // exit with $value as the test result
+ *
+ * When the runner catches this, the result is compared with the test's expected value.
+ */
+object TestExtensions {
+    fun registerExtensions(context: IOperationContext) {
+        context.registerExtensionMethod("Test.Exit", TestExtensions::exit)
+    }
+
+    private fun exit(context: FunctionExecuteContext): Nothing {
+        val result = context.firstParameter
+        throw TestExitException(result)
+    }
+}
diff --git a/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/TestRunFlags.kt b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/TestRunFlags.kt
new file mode 100644
index 0000000..229130d
--- /dev/null
+++ b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/TestRunFlags.kt
@@ -0,0 +1,26 @@
+package com.intuit.isl.cmd
+
+/**
+ * Thread-local flags for the current test run. Set by [TestCommand] so that
+ * [LogExtensions], [IslModuleResolver], and [YamlUnitTestRunner] can reduce
+ * output when -verbose is not passed.
+ */
+object TestRunFlags {
+    private val verbose = ThreadLocal()
+
+    /** Call at start of test run: pass true for -verbose, false for quiet. */
+    fun setTestVerbose(verbose: Boolean) {
+        this.verbose.set(verbose)
+    }
+
+    /** True when test run is in verbose mode. */
+    fun isVerbose(): Boolean = verbose.get() == true
+
+    /** Show script logs (@.Log.Info etc.) only when not in a quiet test run. */
+    fun shouldShowScriptLogs(): Boolean = verbose.get() != false
+
+    /** Call when test run finishes (e.g. in finally). */
+    fun clear() {
+        verbose.remove()
+    }
+}
diff --git a/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/TransformCommand.kt b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/TransformCommand.kt
index a645bdd..8914990 100644
--- a/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/TransformCommand.kt
+++ b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/TransformCommand.kt
@@ -7,7 +7,6 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
 import com.fasterxml.jackson.module.kotlin.readValue
 import com.intuit.isl.common.OperationContext
-import com.intuit.isl.runtime.TransformCompiler
 import com.intuit.isl.utils.JsonConvert
 import kotlinx.coroutines.runBlocking
 import picocli.CommandLine.Command
@@ -58,7 +57,7 @@ class TransformCommand : Runnable {
     
     @Option(
         names = ["-f", "--format"],
-        description = ["Output format: json, yaml, pretty-json (default: json)"]
+        description = ["Output format: json, yaml, pretty-json (default: json, or inferred from -o file extension)"]
     )
     var format: String = "json"
     
@@ -76,6 +75,15 @@ class TransformCommand : Runnable {
     
     override fun run() {
         try {
+            // Infer output format from output file extension when not explicitly set
+            if (outputFile != null && format == "json") {
+                when (outputFile!!.extension.lowercase()) {
+                    "yaml", "yml" -> format = "yaml"
+                    "json" -> { /* keep json */ }
+                    else -> { /* keep json */ }
+                }
+            }
+            
             // Validate script file
             if (!scriptFile.exists()) {
                 System.err.println("Error: Script file not found: ${scriptFile.absolutePath}")
@@ -126,16 +134,24 @@ class TransformCommand : Runnable {
                 variables["input"] = inputData
             }
             
-            // Execute transformation
-            val compiler = TransformCompiler()
-            val transformer = compiler.compileIsl("script", scriptContent)
+            // Add $context with input file info when -i was used
+            if (inputFile != null) {
+                variables["context"] = mapOf("inputFileName" to inputFile!!.name)
+            }
             
-            // Create operation context with variables
+            // Execute transformation using shared module resolution (supports relative imports like ../customer.isl)
+            val transformPackage = IslModuleResolver.compileSingleFile(scriptFile, scriptContent)
+            val transformer = transformPackage.getModule(scriptFile.name)
+                ?: throw IllegalStateException("Compiled module '${scriptFile.name}' not found in package")
+
+            // Create operation context with variables and CLI extensions (e.g. Log)
             val context = OperationContext()
+            LogExtensions.registerExtensions(context)
             variables.forEach { (key, value) ->
                 val varName = if (key.startsWith("$")) key else "$$key"
-                val varValue = JsonConvert.convert(value);
-                println("Setting variable " + varName + " to " + varValue );
+                val varValue = JsonConvert.convert(value)
+                val valuePreview = varValue.toString().let { if (it.length > 10) it.take(10) + "..." else it }
+                println("Setting variable $varName to $valuePreview")
                 context.setVariable(varName, varValue)
             }
             
@@ -148,7 +164,7 @@ class TransformCommand : Runnable {
             
             if (outputFile != null) {
                 outputFile!!.writeText(output)
-                println("Output written to: ${outputFile!!.absolutePath}")
+                // When -o/--output is set, result goes only to the file (no console output)
             } else {
                 println(output)
             }
diff --git a/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/ValidateCommand.kt b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/ValidateCommand.kt
index 3747dc6..835918f 100644
--- a/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/ValidateCommand.kt
+++ b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/ValidateCommand.kt
@@ -1,49 +1,49 @@
 package com.intuit.isl.cmd
 
-import com.intuit.isl.common.OperationContext
-import com.intuit.isl.runtime.TransformCompiler
-import kotlinx.coroutines.runBlocking
 import picocli.CommandLine.Command
 import picocli.CommandLine.Parameters
 import java.io.File
 import kotlin.system.exitProcess
 
 /**
- * Command to validate ISL scripts without executing them
+ * Command to validate ISL scripts without executing them.
+ * Compiles the script and lists loaded files and detected functions.
+ * Uses the same module resolution as transform and test commands (supports relative imports).
  */
 @Command(
     name = "validate",
     description = ["Validate an ISL script without executing it"]
 )
 class ValidateCommand : Runnable {
-    
+
     @Parameters(
         index = "0",
         description = ["ISL script file to validate"]
     )
     lateinit var scriptFile: File
-    
+
     override fun run() {
         try {
             if (!scriptFile.exists()) {
                 System.err.println("Error: Script file not found: ${scriptFile.absolutePath}")
                 exitProcess(1)
             }
-            
+
             val scriptContent = scriptFile.readText()
-            val compiler = TransformCompiler()
-            
-            // Try to parse and compile the script
-            val transformer = compiler.compileIsl(scriptFile.name, scriptContent)
-            
-            // Try to execute with empty params to validate
-            val context = OperationContext()
-            runBlocking {
-                transformer.runTransformAsync("run", context)
-            }
-            
+            val transformPackage = IslModuleResolver.compileSingleFile(scriptFile, scriptContent)
+
             println("> Script is valid: ${scriptFile.name}")
-            
+            println("  Files loaded:")
+            for (moduleName in transformPackage.modules) {
+                println("    $moduleName")
+            }
+            println("  Functions by module:")
+            for (moduleName in transformPackage.modules) {
+                val transformer = transformPackage.getModule(moduleName) ?: continue
+                val functionNames = transformer.module.functions.map { it.name }.sorted()
+                println("    $moduleName: ${functionNames.joinToString(", ").ifEmpty { "(none)" }}")
+            }
+
         } catch (e: Exception) {
             System.err.println("✗ Validation failed: ${e.message}")
             if (System.getProperty("debug") == "true") {
diff --git a/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/YamlUnitTestRunner.kt b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/YamlUnitTestRunner.kt
new file mode 100644
index 0000000..6e3aa1f
--- /dev/null
+++ b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/YamlUnitTestRunner.kt
@@ -0,0 +1,415 @@
+package com.intuit.isl.cmd
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.node.ObjectNode
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
+import com.fasterxml.jackson.module.kotlin.KotlinFeature
+import com.fasterxml.jackson.module.kotlin.kotlinModule
+import com.fasterxml.jackson.module.kotlin.readValue
+import com.intuit.isl.runtime.FileInfo
+import com.intuit.isl.runtime.TransformCompilationException
+import com.intuit.isl.runtime.TransformException
+import com.intuit.isl.runtime.TransformPackage
+import com.intuit.isl.runtime.TransformPackageBuilder
+import com.intuit.isl.test.TestOperationContext
+import com.intuit.isl.test.annotations.ComparisonDiff
+import com.intuit.isl.test.annotations.TestResult
+import com.intuit.isl.test.annotations.TestResultContext
+import com.intuit.isl.test.mocks.MockFunction
+import com.intuit.isl.utils.JsonConvert
+import java.nio.file.Path
+import kotlin.io.path.extension
+import kotlin.io.path.nameWithoutExtension
+
+/** Empty = run all tests; non-empty = run only tests whose functionName (or "suiteFile:functionName") matches. */
+typealias FunctionFilter = Set
+
+object YamlUnitTestRunner {
+
+    private val yamlMapper = com.fasterxml.jackson.databind.ObjectMapper(YAMLFactory()).apply {
+        registerModule(
+            kotlinModule {
+                enable(KotlinFeature.NullIsSameAsDefault)
+                enable(KotlinFeature.NullToEmptyMap)
+                enable(KotlinFeature.NullToEmptyCollection)
+            }
+        )
+    }
+
+    fun parseSuite(yamlContent: String): YamlUnitTestSuite = yamlMapper.readValue(yamlContent)
+
+    /**
+     * Runs a single *.tests.yaml suite and appends results to [resultContext].
+     * [yamlPath] is the path to the .tests.yaml file; [basePath] is the directory containing it (used for resolving islSource and mockSource).
+     * When [functionFilter] is non-empty, only test entries whose functionName matches (or "suiteFile:functionName") are run.
+     */
+    fun runSuite(
+        yamlPath: Path,
+        basePath: Path,
+        resultContext: TestResultContext,
+        contextCustomizers: List<(com.intuit.isl.common.IOperationContext) -> Unit>,
+        functionFilter: FunctionFilter = emptySet(),
+        verbose: Boolean = false
+    ) {
+        val suite = parseSuite(yamlPath.toFile().readText())
+        val setup = suite.setup
+        val islPath = basePath.resolve(setup.islSource).normalize()
+        val islFile = islPath.toFile()
+        if (!islFile.exists() || !islFile.isFile) {
+            resultContext.testResults.add(
+                TestResult(
+                    testFile = yamlPath.toString(),
+                    functionName = "setup",
+                    testName = suite.category ?: yamlPath.nameWithoutExtension,
+                    testGroup = suite.category ?: yamlPath.nameWithoutExtension,
+                    success = false,
+                    message = "ISL file not found: $islPath (islSource: ${setup.islSource})"
+                )
+            )
+            return
+        }
+        val islContent = islFile.readText()
+        val moduleName = basePath.relativize(islPath).toString().replace("\\", "/")
+        val fileInfos = mutableListOf(FileInfo(moduleName, islContent))
+        val resolvedPaths = mutableMapOf()
+        resolvedPaths[moduleName] = islPath.toAbsolutePath().normalize()
+        val findExternalModule = IslModuleResolver.createModuleFinder(basePath, resolvedPaths)
+        val transformPackage: TransformPackage = try {
+            TransformPackageBuilder().build(fileInfos, findExternalModule)
+        } catch (e: Exception) {
+            resultContext.testResults.add(
+                TestResult(
+                    testFile = moduleName,
+                    functionName = "compilation",
+                    testName = suite.category ?: yamlPath.nameWithoutExtension,
+                    testGroup = suite.category ?: yamlPath.nameWithoutExtension,
+                    success = false,
+                    message = (e as? TransformCompilationException)?.message ?: e.toString(),
+                    errorPosition = (e as? TransformCompilationException)?.position
+                )
+            )
+            return
+        }
+
+        val groupName = suite.category ?: yamlPath.nameWithoutExtension
+        val suiteFileName = yamlPath.fileName.toString()
+
+        val testsToRun = if (functionFilter.isEmpty()) suite.entries else {
+            suite.entries.filter { entry ->
+                functionFilter.any { filter ->
+                    when {
+                        filter.contains(":") -> {
+                            val parts = filter.split(":", limit = 2)
+                            val fileMatch = parts[0].equals(suiteFileName, true) ||
+                                parts[0].equals(yamlPath.nameWithoutExtension, true)
+                            fileMatch && parts[1].equals(entry.functionName, true)
+                        }
+                        else -> filter.equals(entry.functionName, true)
+                    }
+                }
+            }
+        }
+
+        val opts = suite.assertOptions ?: AssertOptions()
+        testsToRun.forEachIndexed { index, entry ->
+            val testResult = runOneTest(
+                transformPackage = transformPackage,
+                moduleName = moduleName,
+                entry = entry,
+                setup = setup,
+                basePath = basePath,
+                groupName = groupName,
+                yamlPath = yamlPath,
+                contextCustomizers = contextCustomizers,
+                assertOptions = opts,
+                printMockSummary = verbose && (index == 0),
+                verbose = verbose
+            )
+            resultContext.testResults.add(testResult)
+        }
+    }
+
+    private fun runOneTest(
+        transformPackage: TransformPackage,
+        moduleName: String,
+        entry: YamlUnitTestEntry,
+        setup: YamlTestSetup,
+        basePath: Path,
+        groupName: String,
+        yamlPath: Path,
+        contextCustomizers: List<(com.intuit.isl.common.IOperationContext) -> Unit>,
+        assertOptions: AssertOptions = AssertOptions(),
+        printMockSummary: Boolean = false,
+        verbose: Boolean = false
+    ): TestResult {
+        val testFileName = basePath.relativize(yamlPath).toString().replace("\\", "/")
+        val context = TestOperationContext.create(
+            testResultContext = TestResultContext(),
+            currentFile = moduleName,
+            basePath = basePath,
+            mockFileName = setup.mockSourceDisplayName(),
+            testFileName = testFileName,
+            contextCustomizers = contextCustomizers
+        )
+
+        try {
+            // 1. Load mockSource file(s) in order (each can override the previous)
+            for (mockFileEntry in setup.mockSourceFiles()) {
+                val mockPath = basePath.resolve(mockFileEntry).normalize()
+                val mockFile = mockPath.toFile()
+                if (mockFile.exists() && mockFile.isFile) {
+                    val ext = mockFile.extension.lowercase()
+                    val root = when (ext) {
+                        "json" -> JsonConvert.mapper.readTree(mockFile)
+                        "yaml", "yml" -> com.fasterxml.jackson.databind.ObjectMapper(YAMLFactory()).readTree(mockFile)
+                        else -> throw IllegalArgumentException("Mock file must be .json, .yaml, .yml; got: $mockFileEntry")
+                    }
+                    if (root.isObject) MockFunction.applyMocksFromNode(context, root as ObjectNode, mockFileEntry)
+                }
+            }
+            // 2. Apply inline mocks after mockSource (all mocks are additive; params differentiate)
+            setup.mocksAsObject()?.let { MockFunction.applyMocksFromNode(context, it, testFileName) }
+
+            if (printMockSummary) {
+                val names = (context.mockExtensions.mockExtensions.keys +
+                    context.mockExtensions.mockAnnotations.keys +
+                    context.mockExtensions.mockStatementExtensions.keys).sorted()
+                val n = names.size
+                if (n > 0) println("[ISL Mock] Mocked M $n function(s): ${names.joinToString(", ")}")
+            }
+
+            // 3. Set input variables (param names from function, or from input map keys)
+            val paramNames = getFunctionParamNames(transformPackage, moduleName, entry.functionName)
+            setInputVariables(context, entry.input, paramNames)
+
+            // 4. Run the function (or capture result from @.Test.Exit(...))
+            if (verbose) println("[ISL Mock] Running ${entry.functionName}")
+            val fullName = TransformPackage.toFullFunctionName(moduleName, entry.functionName)
+            val result = try {
+                transformPackage.runTransformNew(fullName, context)
+            } catch (e: TestExitException) {
+                val r = e.result
+                when {
+                    r == null -> null
+                    r is JsonNode && r.isNull -> null
+                    else -> JsonConvert.convert(r)
+                }
+            } catch (e: TransformException) {
+                val testExit = e.cause as? TestExitException
+                if (testExit != null) {
+                    val r = testExit.result
+                    when {
+                        r == null -> null
+                        r is JsonNode && r.isNull -> null
+                        else -> JsonConvert.convert(r)
+                    }
+                } else {
+                    throw e
+                }
+            }
+
+            // 5. Compare with expected (deep compare; on failure report exact field diffs)
+            val expected = entry.expected
+            val opts = entry.assertOptions ?: assertOptions
+            val ignorePaths = (entry.ignore.orEmpty()).map { normalizeComparePath(it) }.toSet()
+            val (success, diffs) = if (expected == null) {
+                (result == null) to emptyList()
+            } else {
+                jsonDeepCompare(expected, result, assertOptions = opts, ignorePaths = ignorePaths)
+            }
+            val message = if (!success && expected != null) {
+                buildComparisonFailureMessage(expected, result, diffs, entry.ignore.orEmpty())
+            } else null
+            val expectedJson = if (!success && expected != null) JsonConvert.mapper.writeValueAsString(expected) else null
+            val actualJson = if (!success) (result?.let { JsonConvert.mapper.writeValueAsString(it) } ?: "null") else null
+            val comparisonDiffs = if (!success && diffs.isNotEmpty()) diffs.map { ComparisonDiff(it.path, it.expectedValue, it.actualValue) } else null
+
+            return TestResult(
+                testFile = yamlPath.toString(),
+                functionName = entry.functionName,
+                testName = entry.name,
+                testGroup = groupName,
+                success = success,
+                message = message,
+                expectedJson = expectedJson,
+                actualJson = actualJson,
+                comparisonDiffs = comparisonDiffs
+            )
+        } catch (e: Exception) {
+            val (msg, pos) = when (e) {
+                is TransformCompilationException -> e.message to e.position
+                is TransformException -> e.message to e.position
+                is com.intuit.isl.runtime.IslException -> e.message to e.position
+                else -> e.message to null
+            }
+            return TestResult(
+                testFile = yamlPath.toString(),
+                functionName = entry.functionName,
+                testName = entry.name,
+                testGroup = groupName,
+                success = false,
+                message = msg ?: e.toString(),
+                errorPosition = pos
+            )
+        }
+    }
+
+    private fun getFunctionParamNames(pkg: TransformPackage, moduleName: String, functionName: String): List {
+        val module = pkg.getModule(moduleName)?.module ?: return emptyList()
+        val func = module.getFunction(functionName) ?: return emptyList()
+        return func.token.arguments.map { it.name }
+    }
+
+    private fun setInputVariables(context: TestOperationContext, input: Any?, paramNames: List) {
+        if (input == null) return
+        if (paramNames.size == 1) {
+            val varName = if (paramNames.first().startsWith("$")) paramNames.first() else "$${paramNames.first()}"
+            context.setVariable(varName, JsonConvert.convert(input))
+            return
+        }
+        if (input is Map<*, *>) {
+            @Suppress("UNCHECKED_CAST")
+            val map = input as Map
+            for ((key, value) in map) {
+                val varName = if (key.startsWith("$")) key else "$$key"
+                context.setVariable(varName, JsonConvert.convert(value))
+            }
+        }
+    }
+
+    /** Path + expected/actual value at a difference; path uses $root.field.[0].key format. */
+    private data class JsonDiff(val path: String, val expectedValue: String, val actualValue: String)
+
+    /**
+     * Normalizes a user-facing JSON path to the format used during comparison ($.key.[0].field).
+     * User may write "providerResponses.items[0].uid" or "providerResponses.error.detail".
+     */
+    private fun normalizeComparePath(userPath: String): String {
+        val t = userPath.trim()
+        if (t.isEmpty()) return "$"
+        val withRoot = if (t.startsWith("$")) t else "$.$t"
+        return withRoot.replace(Regex("(? = emptySet()
+    ): Pair> {
+        if (path in ignorePaths) return true to emptyList()
+        // actual is null (missing or literal null)
+        if (actual == null) {
+            if (expected.isNull) return true to emptyList()
+            if (assertOptions.nullSameAsEmptyArray && expected.isArray && expected.size() == 0) return true to emptyList()
+            return false to listOf(JsonDiff(path, formatJsonValue(expected), "null"))
+        }
+        // expected is null
+        if (expected.isNull) {
+            if (actual.isNull) return true to emptyList()
+            if (assertOptions.nullSameAsEmptyArray && actual.isArray && actual.size() == 0) return true to emptyList()
+            return false to listOf(JsonDiff(path, "null", formatJsonValue(actual)))
+        }
+        if (expected.isNumber && actual.isNumber) {
+            val eq = if (assertOptions.numbersEqualIgnoreFormat) {
+                expected.decimalValue().compareTo(actual.decimalValue()) == 0
+            } else {
+                expected.decimalValue() == actual.decimalValue()
+            }
+            return if (eq) true to emptyList()
+            else false to listOf(JsonDiff(path, formatJsonValue(expected), formatJsonValue(actual)))
+        }
+        if (expected.nodeType != actual.nodeType) {
+            return false to listOf(JsonDiff(path, formatJsonValue(expected), formatJsonValue(actual)))
+        }
+        when {
+            expected.isObject -> {
+                if (!actual.isObject) {
+                    return false to listOf(JsonDiff(path, formatJsonValue(expected), formatJsonValue(actual)))
+                }
+                val allKeys = if (assertOptions.ignoreExtraFieldsInActual) {
+                    expected.fieldNames().asSequence().toList()
+                } else {
+                    (expected.fieldNames().asSequence().toSet() + actual.fieldNames().asSequence().toSet()).toList()
+                }
+                val acc = mutableListOf()
+                for (k in allKeys) {
+                    val expectedChild = expected.get(k)
+                    val actualChild = actual.get(k)
+                    val subPath = if (path == "$") "$$k" else "$path.$k"
+                    when {
+                        expectedChild == null && actualChild == null -> {}
+                        expectedChild == null -> {
+                            // extra in actual (only when not ignoreExtraFieldsInActual)
+                            if (assertOptions.missingSameAsEmptyArray && actualChild != null && actualChild.isArray && actualChild.size() == 0) {}
+                            else acc.add(JsonDiff(subPath, "missing", formatJsonValue(actualChild!!)))
+                        }
+                        actualChild == null -> {
+                            // missing in actual
+                            if (assertOptions.nullSameAsMissing && expectedChild.isNull) {}
+                            else if (assertOptions.missingSameAsEmptyArray && expectedChild.isArray && expectedChild.size() == 0) {}
+                            else acc.add(JsonDiff(subPath, formatJsonValue(expectedChild), "missing"))
+                        }
+                        else -> {
+                            val (ok, subDiffs) = jsonDeepCompare(expectedChild, actualChild, subPath, assertOptions, ignorePaths)
+                            if (!ok) acc.addAll(subDiffs)
+                        }
+                    }
+                }
+                if (!assertOptions.ignoreExtraFieldsInActual && expected.size() != actual.size()) {
+                    acc.add(JsonDiff(path, "object size ${expected.size()}", "object size ${actual.size()}"))
+                }
+                return (acc.isEmpty()) to acc
+            }
+            expected.isArray -> {
+                if (!actual.isArray) {
+                    return false to listOf(JsonDiff(path, formatJsonValue(expected), formatJsonValue(actual)))
+                }
+                val acc = mutableListOf()
+                val size = minOf(expected.size(), actual.size())
+                for (i in 0 until size) {
+                    val indexPath = if (path == "$") "$[$i]" else "$path.[$i]"
+                    val (ok, subDiffs) = jsonDeepCompare(expected.get(i), actual.get(i), indexPath, assertOptions, ignorePaths)
+                    if (!ok) acc.addAll(subDiffs)
+                }
+                if (expected.size() != actual.size()) {
+                    acc.add(JsonDiff(path, "array size ${expected.size()}", "array size ${actual.size()}"))
+                }
+                return (acc.isEmpty()) to acc
+            }
+            else -> {
+                val eq = expected.equals(actual)
+                return if (eq) true to emptyList()
+                else false to listOf(JsonDiff(path, formatJsonValue(expected), formatJsonValue(actual)))
+            }
+        }
+    }
+
+    private fun formatJsonValue(node: JsonNode): String =
+        JsonConvert.mapper.writeValueAsString(node)
+
+    private fun buildComparisonFailureMessage(
+        expected: JsonNode,
+        actual: JsonNode?,
+        diffs: List,
+        ignoredPaths: List = emptyList()
+    ): String {
+        val fullExpected = JsonConvert.mapper.writeValueAsString(expected)
+        val fullActual = actual?.let { JsonConvert.mapper.writeValueAsString(it) } ?: "null"
+        val header = "Expected: $fullExpected\nActual: $fullActual"
+        if (diffs.isEmpty()) return header
+        val diffLines = diffs.joinToString("\n") { d ->
+            "Expected: ${d.path} = ${d.expectedValue}\nActual: ${d.path} = ${d.actualValue}\r\n"
+        }
+        val ignoredSection = if (ignoredPaths.isEmpty()) ""
+        else "\n[ISL Assert] Ignored path(s):\n${ignoredPaths.joinToString("\n") { "  $it" }}\n"
+        return "[ISL Assert] Result Differences:\n$header$ignoredSection\n[ISL Assert] Difference(s):\n$diffLines\n"
+    }
+}
diff --git a/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/YamlUnitTestSuite.kt b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/YamlUnitTestSuite.kt
new file mode 100644
index 0000000..b5850e8
--- /dev/null
+++ b/isl-cmd/src/main/kotlin/com/intuit/isl/cmd/YamlUnitTestSuite.kt
@@ -0,0 +1,149 @@
+package com.intuit.isl.cmd
+
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.JsonDeserializer
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize
+import com.fasterxml.jackson.databind.node.ObjectNode
+import com.intuit.isl.utils.JsonConvert
+import java.io.IOException
+
+/**
+ * Options for comparing expected vs actual in YAML test assertions.
+ * All default to false (strict comparison).
+ *
+ * In YAML, assertOptions can be written as:
+ * - Object: `assertOptions: { nullSameAsMissing: true, ... }`
+ * - Comma-separated list: `assertOptions: nullSameAsMissing, nullSameAsEmptyArray, ...`
+ * - Array: `assertOptions: [nullSameAsMissing, nullSameAsEmptyArray, ...]`
+ */
+@JsonDeserialize(using = AssertOptionsDeserializer::class)
+data class AssertOptions(
+    /** Treat null and missing (absent key) as equal. */
+    val nullSameAsMissing: Boolean = false,
+    /** Treat null and empty array [] as equal. */
+    val nullSameAsEmptyArray: Boolean = false,
+    /** Treat missing (absent key) and empty array [] as equal. */
+    val missingSameAsEmptyArray: Boolean = false,
+    /** Only compare keys present in expected; ignore extra keys in actual. */
+    val ignoreExtraFieldsInActual: Boolean = false,
+    /** Compare numbers by numeric value only (e.g. 1234.0 equals 1234 equals 1234.00). */
+    val numbersEqualIgnoreFormat: Boolean = false
+) {
+    companion object {
+        private val OPTION_NAMES = setOf(
+            "nullSameAsMissing",
+            "nullSameAsEmptyArray",
+            "missingSameAsEmptyArray",
+            "ignoreExtraFieldsInActual",
+            "numbersEqualIgnoreFormat"
+        )
+
+        fun fromNames(names: List): AssertOptions {
+            val normalized = names.map { it.trim() }.filter { it.isNotEmpty() }.filter { it in OPTION_NAMES }.toSet()
+            return AssertOptions(
+                nullSameAsMissing = "nullSameAsMissing" in normalized,
+                nullSameAsEmptyArray = "nullSameAsEmptyArray" in normalized,
+                missingSameAsEmptyArray = "missingSameAsEmptyArray" in normalized,
+                ignoreExtraFieldsInActual = "ignoreExtraFieldsInActual" in normalized,
+                numbersEqualIgnoreFormat = "numbersEqualIgnoreFormat" in normalized
+            )
+        }
+    }
+}
+
+/**
+ * Deserializes assertOptions from either an object (boolean keys) or a string/array of option names.
+ */
+class AssertOptionsDeserializer : JsonDeserializer() {
+    @Throws(IOException::class)
+    override fun deserialize(p: JsonParser, ctxt: DeserializationContext): AssertOptions {
+        val node: JsonNode = p.codec.readTree(p) ?: return AssertOptions()
+        return when {
+            node.isObject -> deserializeObject(node)
+            node.isTextual -> AssertOptions.fromNames(node.asText().split(','))
+            node.isArray -> AssertOptions.fromNames(node.map { if (it.isTextual) it.asText() else "" })
+            else -> AssertOptions()
+        }
+    }
+
+    private fun deserializeObject(node: JsonNode): AssertOptions {
+        return AssertOptions(
+            nullSameAsMissing = node.path("nullSameAsMissing").asBoolean(false),
+            nullSameAsEmptyArray = node.path("nullSameAsEmptyArray").asBoolean(false),
+            missingSameAsEmptyArray = node.path("missingSameAsEmptyArray").asBoolean(false),
+            ignoreExtraFieldsInActual = node.path("ignoreExtraFieldsInActual").asBoolean(false),
+            numbersEqualIgnoreFormat = node.path("numbersEqualIgnoreFormat").asBoolean(false)
+        )
+    }
+}
+
+/**
+ * YAML-driven unit test suite (e.g. *.tests.yaml).
+ * Format:
+ * - category: name of test group
+ * - setup: islSource, optional mockSource, optional inline mocks (applied after mockSource so they override)
+ * - assertOptions: optional assertion comparison options
+ * - tests or islTests: list of test entries with name, functionName, optional input, expected result
+ */
+data class YamlUnitTestSuite(
+    val category: String? = null,
+    val setup: YamlTestSetup,
+    val assertOptions: AssertOptions? = null,
+    @com.fasterxml.jackson.annotation.JsonProperty("tests") val tests: List? = null,
+    @com.fasterxml.jackson.annotation.JsonProperty("islTests") val islTests: List? = null
+) {
+    /** Test entries from either "tests" or "islTests" YAML key (for backward compatibility). */
+    val entries: List
+        get() = (islTests ?: tests).orEmpty()
+}
+
+data class YamlTestSetup(
+    val islSource: String,
+    /**
+     * Mock file(s) to load (same format as @.Mock.Load).
+     * - Single string: mockSource: mymocks.yaml
+     * - Array: mockSource: [commonMocks.yaml, otherMocks.yaml] — loaded in order, each overrides the previous.
+     */
+    val mockSource: JsonNode? = null,
+    /** Inline mocks in same format as @.Mock.Load (func/annotation arrays). Applied after mockSource; all mocks are additive (params differentiate). Uses Map so Jackson reliably deserializes nested YAML. */
+    val mocks: Map? = null
+) {
+    /** Converts inline mocks to ObjectNode for applyMocksFromNode. Handles both Map (from YAML) and ensures func/annotation structure. */
+    fun mocksAsObject(): ObjectNode? {
+        val map = mocks ?: return null
+        val node: JsonNode = JsonConvert.mapper.valueToTree(map)
+        return if (node.isObject) node as ObjectNode else null
+    }
+
+    /** Resolves mockSource to a list of file names: one for a string, many for an array, empty if null. */
+    fun mockSourceFiles(): List = when {
+        mockSource == null || mockSource.isNull -> emptyList()
+        mockSource.isTextual -> listOf(mockSource.asText().trim()).filter { it.isNotEmpty() }
+        mockSource.isArray -> mockSource.mapNotNull { if (it.isTextual) it.asText().trim().takeIf { s -> s.isNotEmpty() } else null }
+        else -> emptyList()
+    }
+
+    /** Last mock source file name (for error messages), or null if none. */
+    fun mockSourceDisplayName(): String? = mockSourceFiles().lastOrNull()
+}
+
+data class YamlUnitTestEntry(
+    val name: String,
+    val functionName: String,
+    val byPassAnnotations: Boolean? = null,
+    /** Single value for single-param functions, or object with param names as keys for multiple params. */
+    val input: Any? = null,
+    /**
+     * JSON paths to ignore when comparing expected vs actual (exact path match).
+     * Paths use dot notation; array indices as [0], [1], etc. Examples:
+     * - providerResponses.error.detail
+     * - providerResponses.items[0].uid
+     */
+    val ignore: List? = null,
+    val expected: JsonNode? = null,
+    /** Override suite assertOptions for this test only. Same formats as suite assertOptions (object, comma-separated, or array). */
+    val assertOptions: AssertOptions? = null
+)
diff --git a/isl-cmd/src/test/kotlin/com/intuit/isl/cmd/IslCommandLineTest.kt b/isl-cmd/src/test/kotlin/com/intuit/isl/cmd/IslCommandLineTest.kt
index 8e18bc7..54ab995 100644
--- a/isl-cmd/src/test/kotlin/com/intuit/isl/cmd/IslCommandLineTest.kt
+++ b/isl-cmd/src/test/kotlin/com/intuit/isl/cmd/IslCommandLineTest.kt
@@ -114,6 +114,35 @@ class IslCommandLineTest {
         assertTrue(output.contains("true"))
     }
 
+    @Test
+    fun `test transform with Log extensions`(@TempDir tempDir: Path) {
+        val scriptFile = tempDir.resolve("log-test.isl").toFile()
+        scriptFile.writeText("""
+            fun run(${'$'}input) {
+                @.Log.Info("Processing", ${'$'}input.name)
+                result: { message: "done" }
+            }
+        """.trimIndent())
+
+        val inputFile = tempDir.resolve("input.json").toFile()
+        inputFile.writeText("""{"name": "test"}""")
+
+        val outputStream = ByteArrayOutputStream()
+        System.setOut(PrintStream(outputStream))
+
+        val cmd = CommandLine(IslCommandLine())
+        val exitCode = cmd.execute(
+            "transform",
+            scriptFile.absolutePath,
+            "-i", inputFile.absolutePath,
+            "--pretty"
+        )
+
+        assertEquals(0, exitCode)
+        val output = outputStream.toString()
+        assertTrue(output.contains("[INFO]") && output.contains("Processing"), "Expected [INFO] in output: $output")
+    }
+
     @Test
     fun `test validate command with valid script`(@TempDir tempDir: Path) {
         val scriptFile = tempDir.resolve("valid.isl").toFile()
diff --git a/isl-test/build.gradle.kts b/isl-test/build.gradle.kts
new file mode 100644
index 0000000..878807a
--- /dev/null
+++ b/isl-test/build.gradle.kts
@@ -0,0 +1,93 @@
+plugins {
+    kotlin("jvm")
+    id("jacoco")
+    id("org.gradle.test-retry") version "1.6.0"
+}
+
+val kotlinVersion: String = "2.1.10"
+val kotlinCoroutinesVersion: String = "1.10.1"
+val jacksonVersion: String = "2.18.3"
+
+dependencies {
+    // Project dependency
+    implementation(project(":isl-transform"))
+
+    // Kotlin
+    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
+    implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
+    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion")
+    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$kotlinCoroutinesVersion")
+
+    // Jackson (needed for JSON/YAML when using isl-transform)
+    implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion")
+    implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
+    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
+    implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jacksonVersion")
+    implementation("com.opencsv:opencsv:5.10")
+
+    // Logging
+    implementation("org.slf4j:slf4j-api:2.0.17")
+    implementation("ch.qos.logback:logback-classic:1.5.16")
+
+    // Testing
+    testImplementation("org.jetbrains.kotlin:kotlin-test-junit5:$kotlinVersion")
+    testImplementation("org.junit.jupiter:junit-jupiter-api:5.12.1")
+    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.12.1")
+    testImplementation("org.junit.jupiter:junit-jupiter-params:5.12.1")
+    testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.12.1")
+    testImplementation("io.mockk:mockk-jvm:1.13.17")
+}
+
+sourceSets {
+    main {
+        kotlin {
+            srcDirs("src/main/kotlin")
+        }
+    }
+    test {
+        kotlin {
+            srcDirs("src/test/kotlin")
+        }
+    }
+}
+
+tasks.jacocoTestReport {
+    dependsOn(tasks.test, tasks.processResources, tasks.classes)
+}
+
+tasks.jacocoTestCoverageVerification {
+    dependsOn(tasks.jacocoTestReport, tasks.classes)
+    violationRules {
+        rule {
+            limit {
+                counter = "LINE"
+                value = "COVEREDRATIO"
+                minimum = "0.0".toBigDecimal()
+            }
+        }
+    }
+    classDirectories.setFrom(tasks.jacocoTestReport.get().classDirectories)
+}
+
+tasks.test {
+    finalizedBy(tasks.jacocoTestReport)
+    retry {
+        maxRetries.set(3)
+        maxFailures.set(10)
+    }
+}
+
+tasks.check {
+    dependsOn(tasks.jacocoTestCoverageVerification)
+}
+
+tasks.jar {
+    manifest {
+        attributes(
+            "Implementation-Title" to project.name,
+            "Implementation-Version" to project.version,
+            "Specification-Title" to project.name,
+            "Specification-Version" to project.version
+        )
+    }
+}
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/LoadFunction.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/LoadFunction.kt
new file mode 100644
index 0000000..0ad7917
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/LoadFunction.kt
@@ -0,0 +1,98 @@
+package com.intuit.isl.test
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.node.JsonNodeFactory
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
+import com.intuit.isl.common.FunctionExecuteContext
+import com.intuit.isl.common.IOperationContext
+import com.intuit.isl.utils.ConvertUtils
+import com.intuit.isl.utils.JsonConvert
+import com.opencsv.CSVParserBuilder
+import com.opencsv.CSVReaderBuilder
+import java.io.File
+import java.io.StringReader
+import java.nio.file.Path
+
+/**
+ * Loads resources from files relative to the current ISL file.
+ * Use @.Load.From("fileName") where fileName is relative to the directory of the current file.
+ * Supports .json, .yaml, .yml, and .csv - all converted to JSON.
+ */
+object LoadFunction {
+    private const val functionName = "Load"
+
+    fun registerExtensions(context: IOperationContext) {
+        context.registerExtensionMethod("$functionName.From") { ctx ->
+            from(ctx)
+        }
+    }
+
+    private fun from(context: FunctionExecuteContext): Any? {
+        val fileName = ConvertUtils.tryToString(context.firstParameter)
+            ?: throw IllegalArgumentException("@.Load.From requires a file name (string)")
+
+        val testContext = context.executionContext.operationContext as? TestOperationContext
+            ?: throw IllegalStateException("@.Load.From is only available in test context")
+
+        val basePath = testContext.basePath
+            ?: throw IllegalStateException("@.Load.From requires basePath; run tests via isl test command or pass basePath to TransformTestPackageBuilder")
+
+        val currentFile = testContext.currentFile
+            ?: throw IllegalStateException("@.Load.From requires currentFile; run tests via isl test command")
+
+        val resolvedPath = resolvePath(basePath, currentFile, fileName)
+        val file = resolvedPath.toFile()
+
+        if (!file.exists()) {
+            throw IllegalArgumentException("File not found: $resolvedPath (resolved from $fileName relative to $currentFile)")
+        }
+        if (!file.isFile) {
+            throw IllegalArgumentException("Not a file: $resolvedPath")
+        }
+
+        val ext = file.extension.lowercase()
+        return when (ext) {
+            "json" -> JsonConvert.mapper.readTree(file)
+            "yaml", "yml" -> {
+                val yamlMapper = com.fasterxml.jackson.databind.ObjectMapper(YAMLFactory())
+                yamlMapper.readTree(file)
+            }
+            "csv" -> parseCsvToJson(file.readText())
+            else -> throw IllegalArgumentException(
+                "@.Load.From supports .json, .yaml, .yml, .csv; got: $fileName"
+            )
+        }
+    }
+
+    private fun resolvePath(basePath: Path, currentFile: String, fileName: String): Path {
+        val currentDir = basePath.resolve(currentFile).parent ?: basePath
+        return currentDir.resolve(fileName).normalize()
+    }
+
+    private fun parseCsvToJson(text: String): JsonNode {
+        val parser = CSVParserBuilder()
+            .withSeparator(',')
+            .withEscapeChar('\\')
+            .withIgnoreQuotations(false)
+            .build()
+        val reader = CSVReaderBuilder(StringReader(text))
+            .withSkipLines(0)
+            .withCSVParser(parser)
+            .build()
+
+        val result = JsonNodeFactory.instance.arrayNode()
+        val firstLine = reader.readNext() ?: return result
+        val headers = firstLine
+
+        var line: Array?
+        while (reader.readNext().also { line = it } != null) {
+            val item = JsonNodeFactory.instance.objectNode()
+            line?.forEachIndexed { i: Int, value: String? ->
+                val key = if (i < headers.size) headers[i] ?: "Col$i" else "Col$i"
+                item.put(key, value)
+            }
+            result.add(item)
+        }
+        return result
+    }
+}
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/TestOperationContext.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/TestOperationContext.kt
new file mode 100644
index 0000000..3c9d2d5
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/TestOperationContext.kt
@@ -0,0 +1,200 @@
+package com.intuit.isl.test
+
+import com.intuit.isl.test.annotations.SetupAnnotation
+import com.intuit.isl.test.annotations.TestAnnotation
+import com.intuit.isl.test.annotations.TestResultContext
+import com.intuit.isl.test.assertions.AssertFunction
+import com.intuit.isl.common.*
+import com.intuit.isl.test.mocks.MockFunction
+import com.intuit.isl.runtime.TransformException
+import com.intuit.isl.utils.JsonConvert
+import java.nio.file.Path
+
+/** Sentinel used to register a fallback handler for any function call that is not mocked. */
+private const val FALLBACK_METHOD_NAME = "*"
+
+class TestOperationContext : BaseOperationContext {
+    /** Current ISL file path (module name, e.g. "tests/sample.isl") for resolving relative paths in @.Load.From() */
+    var currentFile: String? = null
+        internal set
+
+    /** Base path for resolving relative file paths (e.g. project root) */
+    var basePath: Path? = null
+        internal set
+
+    /** Mock file name (e.g. from setup.mockSource) for error messages when an unmocked function is called. */
+    var mockFileName: String? = null
+        internal set
+
+    /** Test file name (e.g. the .tests.yaml path) for error messages; when set, suggest adding mocks to the test file (setup) instead of mock files. */
+    var testFileName: String? = null
+        internal set
+
+    companion object {
+        fun create(
+            testResultContext: TestResultContext,
+            currentFile: String? = null,
+            basePath: Path? = null,
+            mockFileName: String? = null,
+            testFileName: String? = null,
+            contextCustomizers: List<(IOperationContext) -> Unit> = emptyList()
+        ): TestOperationContext {
+            val context = TestOperationContext()
+
+            context.registerAnnotation(SetupAnnotation.annotationName, SetupAnnotation::runAnnotationFunction)
+            TestAnnotation.registerAnnotation(context, testResultContext)
+
+            AssertFunction.registerExtensions(context)
+            LoadFunction.registerExtensions(context)
+            MockFunction.registerExtensions(context)
+
+            context.registerExtensionMethod(FALLBACK_METHOD_NAME) { functionContext ->
+                throw buildUnmockedCallException(functionContext)
+            }
+
+            contextCustomizers.forEach { it(context) }
+
+            context.currentFile = currentFile
+            context.basePath = basePath
+            context.mockFileName = mockFileName
+            context.testFileName = testFileName
+
+            return context
+        }
+
+        private fun buildUnmockedCallException(context: FunctionExecuteContext): TransformException {
+            val functionName = context.functionName
+            val position = context.command.token.position
+            val place = "file=${position.file}, line=${position.line}, column=${position.column}" +
+                (position.endLine?.let { ", endLine=$it" } ?: "") +
+                (position.endColumn?.let { ", endColumn=$it" } ?: "")
+
+            val paramsJson = context.parameters
+                .map { JsonConvert.convert(it) }
+                .let { nodes -> JsonConvert.mapper.writeValueAsString(nodes) }
+
+            val testContext = context.executionContext.operationContext as? TestOperationContext
+            val addToHint = when {
+                testContext?.testFileName != null -> "test file [${testContext.testFileName}] (in setup.mocks or in the test)"
+                else -> "[${testContext?.mockFileName ?: "your-mocks.yaml"}]"
+            }
+
+            val yamlSnippet = buildString {
+                appendLine("- name: \"$functionName\"")
+                if (context.parameters.isNotEmpty()) {
+                    appendLine("  params: $paramsJson")
+                }
+                appendLine("  result: ")
+            }
+
+            val message = buildString {
+                appendLine("Unmocked function was called. The test must only call functions that are mocked.")
+                appendLine("Function: @.$functionName")
+                appendLine("Called from: $place")
+                appendLine("Parameters: $paramsJson")
+                appendLine("")
+                appendLine("To mock this function add this to your $addToHint then rerun the tests:")
+                appendLine("")
+                appendLine("func:")
+                appendLine(yamlSnippet)
+                appendLine("")
+            }
+
+            return TransformException(message.trimEnd(), position)
+        }
+
+        private fun buildUnmockedModifierException(modifierKey: String, context: FunctionExecuteContext): TransformException {
+            val displayName = if (modifierKey.lowercase().startsWith("modifier.")) modifierKey.drop("modifier.".length) else modifierKey
+            val position = context.command.token.position
+            val place = "file=${position.file}, line=${position.line}, column=${position.column}" +
+                (position.endLine?.let { ", endLine=$it" } ?: "") +
+                (position.endColumn?.let { ", endColumn=$it" } ?: "")
+
+            val testContext = context.executionContext.operationContext as? TestOperationContext
+            val addToHint = when {
+                testContext?.testFileName != null -> "test file [${testContext.testFileName}] (in setup.mocks or in the test)"
+                else -> "[${testContext?.mockFileName ?: "your-mocks.yaml"}]"
+            }
+
+            val yamlName = "Modifier.$displayName"
+            val yamlSnippet = buildString {
+                appendLine("- name: \"$yamlName\"")
+                appendLine("  result: ")
+            }
+
+            val message = buildString {
+                appendLine("Unmocked modifier was called. The test must only call modifiers that are mocked.")
+                appendLine("Modifier: | $displayName")
+                appendLine("Called from: $place")
+                appendLine("")
+                appendLine("To mock this modifier add this to your $addToHint then rerun the tests:")
+                appendLine("func:")
+                append(yamlSnippet)
+            }
+
+            return TransformException(message.trimEnd(), position)
+        }
+    }
+
+    constructor() : super() {
+        this.mockExtensions = TestOperationMockExtensions()
+    }
+
+    private constructor(
+        extensions: HashMap,
+        annotations: HashMap,
+        statementExtensions: HashMap,
+        internalExtensions: HashMap,
+        mockExtensions: TestOperationMockExtensions,
+        mockFileName: String? = null,
+        testFileName: String? = null
+    ) : super(
+        extensions, annotations, statementExtensions, internalExtensions, HashMap()
+    ) {
+        this.mockExtensions = mockExtensions
+        this.mockFileName = mockFileName
+        this.testFileName = testFileName
+    }
+
+    val mockExtensions : TestOperationMockExtensions
+
+    override fun getExtension(name: String): AsyncContextAwareExtensionMethod? {
+        val function = mockExtensions.mockExtensions[name.lowercase()]?.func
+        if (function != null) {
+            return function
+        }
+        val fromSuper = super.getExtension(name)
+        if (fromSuper != null) {
+            return fromSuper
+        }
+        if (name.lowercase().startsWith("modifier.")) {
+            return { context ->
+                throw buildUnmockedModifierException(name, context)
+            }
+        }
+        return null
+    }
+
+    override fun getAnnotation(annotationName: String): AsyncExtensionAnnotation? {
+        val function = mockExtensions.mockAnnotations[annotationName.lowercase()]?.func
+        if (function != null) {
+            return function
+        }
+        return super.getAnnotation(annotationName)
+    }
+
+    override fun getStatementExtension(name: String): AsyncStatementsExtensionMethod? {
+        val function = mockExtensions.mockStatementExtensions[name.lowercase()]?.func
+        if (function != null) {
+            return function
+        }
+        return super.getStatementExtension(name)
+    }
+
+    override fun clone(newInternals: HashMap): IOperationContext {
+        return TestOperationContext(
+            this.extensions, this.annotations, this.statementExtensions, newInternals, this.mockExtensions, this.mockFileName, this.testFileName
+        )
+    }
+}
+
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/TestOperationMockExtensions.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/TestOperationMockExtensions.kt
new file mode 100644
index 0000000..1e33bf0
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/TestOperationMockExtensions.kt
@@ -0,0 +1,11 @@
+package com.intuit.isl.test
+
+import com.intuit.isl.commands.CommandResult
+import com.intuit.isl.common.*
+import com.intuit.isl.test.mocks.MockContext
+
+class TestOperationMockExtensions {
+    val mockExtensions = HashMap>()
+    val mockAnnotations = HashMap>()
+    val mockStatementExtensions = HashMap>()
+}
\ No newline at end of file
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/TransformTestFile.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/TransformTestFile.kt
new file mode 100644
index 0000000..8946ca1
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/TransformTestFile.kt
@@ -0,0 +1,7 @@
+package com.intuit.isl.test
+
+data class TransformTestFile(
+    val fileName: String,
+    val testFunctions: Set = setOf(),
+    val setupFile: String? = null
+)
\ No newline at end of file
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/TransformTestPackage.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/TransformTestPackage.kt
new file mode 100644
index 0000000..6bacf1f
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/TransformTestPackage.kt
@@ -0,0 +1,113 @@
+package com.intuit.isl.test
+
+import com.intuit.isl.common.IOperationContext
+import com.intuit.isl.test.annotations.SetupAnnotation
+import com.intuit.isl.test.annotations.TestAnnotation
+import com.intuit.isl.test.annotations.TestResultContext
+import com.intuit.isl.commands.IFunctionDeclarationCommand
+import com.intuit.isl.runtime.TransformModule
+import com.intuit.isl.runtime.TransformPackage
+import java.nio.file.Path
+
+class TransformTestPackage(
+    private val transformPackage: TransformPackage,
+    private val basePath: Path? = null,
+    private val contextCustomizers: List<(IOperationContext) -> Unit> = emptyList()
+) {
+    private val testFiles = mutableMapOf()
+
+    init {
+        transformPackage.modules.forEach { file ->
+            val module = transformPackage.getModule(file)?.module
+            module?.functions?.forEach { function ->
+                val testFile = verifyIfModuleIsTestFile(function, module, file)
+                if (testFile != null) {
+                    val existing = testFiles[file]
+                    testFiles[file] = if (existing != null) {
+                        TransformTestFile(
+                            file,
+                            existing.testFunctions + testFile.testFunctions,
+                            existing.setupFile ?: testFile.setupFile
+                        )
+                    } else {
+                        testFile
+                    }
+                }
+            }
+        }
+    }
+
+    fun runAllTests(testResultContext: TestResultContext? = null) : TestResultContext {
+        return runFilteredTests(testResultContext) { _, _ -> true }
+    }
+
+    /**
+     * Run only tests that match the given predicate.
+     * @param includeTest Predicate (file, function) -> true to run the test
+     */
+    fun runFilteredTests(
+        testResultContext: TestResultContext? = null,
+        includeTest: (file: String, function: String) -> Boolean
+    ): TestResultContext {
+        val context = testResultContext ?: TestResultContext()
+        testFiles.forEach { (_, file) ->
+            file.testFunctions.filter { includeTest(file.fileName, it) }.forEach { function ->
+                runTest(file.fileName, function, context)
+            }
+        }
+        return context
+    }
+
+    fun runTest(testFile: String, testFunc: String, testResultContext: TestResultContext? = null) : TestResultContext {
+        var context = testResultContext ?: TestResultContext()
+        println();
+        println("[ISLTest]>> Start Running=$testFunc");
+        try{
+            runTest(testFile, testFunc, context, testFiles[testFile]?.setupFile)
+        } finally{
+            println("[ISLTest]<< DONE Running=$testFunc");
+        }
+        return context
+    }
+
+    private fun runTest(testFile: String, testFunc: String, testResultContext: TestResultContext, setupFunc: String? = null) {
+        val fullFunctionName = TransformPackage.toFullFunctionName(testFile, testFunc)
+        val context = TestOperationContext.create(testResultContext, testFile, basePath, mockFileName = null, contextCustomizers = contextCustomizers)
+        // Run setup function if it exists
+        if (setupFunc != null) {
+            val fullSetupFunctionName = TransformPackage.toFullFunctionName(testFile, setupFunc)
+            transformPackage.runTransform(fullSetupFunctionName, context)
+        }
+        transformPackage.runTransform(fullFunctionName, context)
+    }
+
+    private fun verifyIfModuleIsTestFile(
+        function: IFunctionDeclarationCommand,
+        module: TransformModule,
+        file: String
+    ): TransformTestFile? {
+        val testFunctions = mutableSetOf()
+        var setUpFunction: String? = null
+        function.token.annotations.forEach { a ->
+            when (a.annotationName) {
+                TestAnnotation.annotationName -> {
+                    testFunctions.add(function.token.functionName)
+                }
+
+                SetupAnnotation.annotationName -> {
+                    if (setUpFunction != null) {
+                        throw Exception("Multiple setUp functions found. File: ${module.name}, Function: ${function.token.functionName}")
+                    }
+                    setUpFunction = function.token.functionName
+                }
+            }
+        }
+
+        // Mark file as test file if it has any test functions
+        if (testFunctions.isNotEmpty()) {
+            return TransformTestFile(file, testFunctions, setUpFunction)
+        }
+        return null
+    }
+}
+
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/TransformTestPackageBuilder.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/TransformTestPackageBuilder.kt
new file mode 100644
index 0000000..1d64b4e
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/TransformTestPackageBuilder.kt
@@ -0,0 +1,21 @@
+package com.intuit.isl.test
+
+import com.intuit.isl.common.IOperationContext
+import com.intuit.isl.runtime.FileInfo
+import com.intuit.isl.runtime.TransformPackageBuilder
+import java.nio.file.Path
+import java.util.function.BiFunction
+
+class TransformTestPackageBuilder {
+    private val transformPackageBuilder = TransformPackageBuilder()
+
+    fun build(
+        files: MutableList,
+        findExternalModule: BiFunction? = null,
+        basePath: Path? = null,
+        contextCustomizers: List<(IOperationContext) -> Unit> = emptyList()
+    ): TransformTestPackage {
+        val transformPackage = transformPackageBuilder.build(files, findExternalModule)
+        return TransformTestPackage(transformPackage, basePath, contextCustomizers)
+    }
+}
\ No newline at end of file
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/annotations/SetupAnnotation.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/annotations/SetupAnnotation.kt
new file mode 100644
index 0000000..253223b
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/annotations/SetupAnnotation.kt
@@ -0,0 +1,12 @@
+package com.intuit.isl.test.annotations
+
+import com.intuit.isl.common.AnnotationExecuteContext
+
+object SetupAnnotation {
+    const val annotationName = "setup"
+
+    suspend fun runAnnotationFunction(context: AnnotationExecuteContext) : Any? {
+        return context.runNextCommand()
+    }
+}
+
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/annotations/TestAnnotation.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/annotations/TestAnnotation.kt
new file mode 100644
index 0000000..0c7f4d1
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/annotations/TestAnnotation.kt
@@ -0,0 +1,84 @@
+package com.intuit.isl.test.annotations;
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.intuit.isl.test.assertions.AssertException
+import com.intuit.isl.common.IOperationContext
+import com.intuit.isl.runtime.IslException
+import com.intuit.isl.runtime.TransformException
+import com.intuit.isl.utils.ConvertUtils
+
+object TestAnnotation {
+    const val annotationName = "test"
+
+    fun registerAnnotation(operationContext: IOperationContext, testResultContext: TestResultContext) {
+        operationContext.registerAnnotation(annotationName)  { context ->
+            var contextCommandOutput : Any? = null
+            val (testName, testGroup) = parseTestAnnotationParams(context)
+            val result = TestResult(
+                testFile = context.command.token.position.file,
+                functionName = context.functionName,
+                testName = testName,
+                testGroup = testGroup,
+                success = true
+            )
+            try {
+                contextCommandOutput = context.runNextCommand()
+            }
+            catch (e: Exception) {
+                result.success = false
+                result.message = e.message
+                result.exception = e
+                if (e is TransformException && e.cause != null) {
+                    e.cause.let {
+                        if (it is AssertException) {
+                            // Surface the assertion exception message
+                            result.message = it.message
+                            result.errorPosition = it.position
+                            result.exception = it
+                        }
+                    }
+                }
+                if (e is IslException) {
+                    result.errorPosition = e.position
+                }
+
+            }
+            finally {
+                testResultContext.testResults.add(result)
+            }
+
+            contextCommandOutput
+        }
+    }
+
+    /**
+     * Parse @test annotation parameters.
+     * Supports: @test(), @test(name), @test(name, group), @test({ name: "x", group: "y" })
+     */
+    private fun parseTestAnnotationParams(context: com.intuit.isl.common.AnnotationExecuteContext): Pair {
+        val functionName = context.functionName
+        val testFile = context.command.token.position.file
+        val defaultGroup = testFile.substringAfterLast('/').substringAfterLast('\\')
+        val params = context.parameters
+        return when {
+            params.isEmpty() -> Pair(functionName, defaultGroup)
+            params.size == 1 -> {
+                val first = params[0]
+                when {
+                    first is JsonNode && first.isObject -> {
+                        val name = first.path("name").takeIf { !it.isMissingNode }?.asText() ?: functionName
+                        val group = first.path("group").takeIf { !it.isMissingNode }?.asText()
+                        Pair(name, group)
+                    }
+                    else -> Pair(ConvertUtils.tryToString(first) ?: functionName, defaultGroup)
+                }
+            }
+            params.size >= 2 -> {
+                val name = ConvertUtils.tryToString(params[0]) ?: functionName
+                val group = ConvertUtils.tryToString(params[1])
+                Pair(name, group)
+            }
+            else -> Pair(functionName, defaultGroup)
+        }
+    }
+}
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/annotations/TestResult.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/annotations/TestResult.kt
new file mode 100644
index 0000000..ad3b140
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/annotations/TestResult.kt
@@ -0,0 +1,23 @@
+package com.intuit.isl.test.annotations
+
+import com.intuit.isl.utils.Position
+import java.lang.Exception
+
+/** One path-level difference (path, expected value, actual value) for report rendering. */
+data class ComparisonDiff(val path: String, val expectedValue: String, val actualValue: String)
+
+data class TestResult(
+    val testFile: String,
+    val functionName: String,
+    val testName: String,
+    val testGroup: String?,
+    var success: Boolean,
+    var message: String? = null,
+    var errorPosition: Position? = null,
+    var exception: Exception? = null,
+    /** When set (e.g. YAML test comparison failure), report can render expected/actual in ```json blocks. */
+    var expectedJson: String? = null,
+    var actualJson: String? = null,
+    /** Per-path differences for markdown report (Expected:/Actual: blocks). */
+    var comparisonDiffs: List? = null
+)
\ No newline at end of file
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/annotations/TestResultContext.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/annotations/TestResultContext.kt
new file mode 100644
index 0000000..123d388
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/annotations/TestResultContext.kt
@@ -0,0 +1,3 @@
+package com.intuit.isl.test.annotations
+
+data class TestResultContext(val testResults: MutableList = mutableListOf())
\ No newline at end of file
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/assertions/AssertException.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/assertions/AssertException.kt
new file mode 100644
index 0000000..820d2d9
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/assertions/AssertException.kt
@@ -0,0 +1,11 @@
+package com.intuit.isl.test.assertions
+
+import com.intuit.isl.runtime.IslException
+import com.intuit.isl.utils.Position
+
+open class AssertException(
+    message: String,
+    val functionName: String,
+    override val position: Position? = null,
+    cause: Throwable? = null
+) : IslException, Exception(message, cause)
\ No newline at end of file
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/assertions/AssertFunction.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/assertions/AssertFunction.kt
new file mode 100644
index 0000000..9959444
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/assertions/AssertFunction.kt
@@ -0,0 +1,257 @@
+package com.intuit.isl.test.assertions
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.node.ArrayNode
+import com.fasterxml.jackson.databind.node.ObjectNode
+import com.intuit.isl.test.TestOperationContext
+import com.intuit.isl.commands.ConditionEvaluator
+import com.intuit.isl.common.FunctionExecuteContext
+import com.intuit.isl.utils.ConvertUtils
+import com.intuit.isl.utils.JsonConvert
+
+object AssertFunction {
+    private const val functionName = "Assert"
+
+    fun registerExtensions(context: TestOperationContext) {
+        mapOf Any?>(
+            AssertFunction::equal.name to AssertFunction::equal,
+            AssertFunction::notEqual.name to AssertFunction::notEqual,
+            AssertFunction::lessThan.name to AssertFunction::lessThan,
+            AssertFunction::lessThanOrEqual.name to AssertFunction::lessThanOrEqual,
+            AssertFunction::greaterThan.name to AssertFunction::greaterThan,
+            AssertFunction::greaterThanOrEqual.name to AssertFunction::greaterThanOrEqual,
+            AssertFunction::matches.name to AssertFunction::matches,
+            AssertFunction::notMatches.name to AssertFunction::notMatches,
+            AssertFunction::contains.name to AssertFunction::contains,
+            AssertFunction::notContains.name to AssertFunction::notContains,
+            AssertFunction::startsWith.name to AssertFunction::startsWith,
+            AssertFunction::notStartsWith.name to AssertFunction::notStartsWith,
+            AssertFunction::endsWith.name to AssertFunction::endsWith,
+            AssertFunction::notEndsWith.name to AssertFunction::notEndsWith,
+            "in" to AssertFunction::assertIn,
+            AssertFunction::notIn.name to AssertFunction::notIn,
+            AssertFunction::isType.name to AssertFunction::isType,
+            AssertFunction::isNotType.name to AssertFunction::isNotType,
+            AssertFunction::notNull.name to AssertFunction::notNull,
+            AssertFunction::isNull.name to AssertFunction::isNull
+        ).forEach { (t, u) ->
+            registerExtensionMethod(context, t, u)
+        }
+    }
+
+    private fun equal(context: FunctionExecuteContext): Any? {
+        return evaluateCondition(context, "==")
+    }
+
+    private fun notEqual(context: FunctionExecuteContext): Any? {
+        return evaluateCondition(context, "!=")
+    }
+
+    private fun lessThan(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, "<")
+
+    private fun lessThanOrEqual(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, "<=")
+
+    private fun greaterThan(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, ">")
+
+    private fun greaterThanOrEqual(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, ">=")
+
+    private fun matches(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, "matches")
+
+    private fun notMatches(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, "!matches")
+
+    private fun contains(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, "contains")
+
+    private fun notContains(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, "!contains")
+
+    private fun startsWith(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, "startswith")
+
+    private fun notStartsWith(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, "!startswith")
+
+    private fun endsWith(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, "endswith")
+
+    private fun notEndsWith(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, "!endswith")
+
+    private fun assertIn(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, "in")
+
+    private fun notIn(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, "!in")
+
+    private fun isType(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, "is")
+
+    private fun isNotType(context: FunctionExecuteContext): Any? =
+        evaluateCondition(context, "!is")
+
+    private fun isNull(context: FunctionExecuteContext): Any? {
+        val expectedValue = context.firstParameter
+        val messageStr = tryGetMessageStr(context.secondParameter)
+
+        val result = ConditionEvaluator.evaluate(expectedValue, "notexists", null)
+        val functionName = context.functionName
+        if (!result) {
+            throw EvaluationAssertException(
+                "$functionName failed. Input value is not null. Value: $expectedValue$messageStr",
+                functionName,
+                expectedValue,
+                context.command.token.position
+            )
+        }
+
+        return null
+    }
+
+    private fun notNull(context: FunctionExecuteContext): Any? {
+        val expectedValue = context.firstParameter
+        val messageStr = tryGetMessageStr(context.secondParameter)
+
+        val result = ConditionEvaluator.evaluate(expectedValue, ConditionEvaluator.EXISTS, null)
+        val functionName = context.functionName
+        if (!result) {
+            throw EvaluationAssertException(
+                "$functionName failed. Input value is null. Value: $expectedValue$messageStr",
+                functionName,
+                expectedValue,
+                context.command.token.position
+            )
+        }
+
+        return null
+    }
+
+    private fun evaluateCondition(context: FunctionExecuteContext, condition: String): Nothing? {
+        val expectedValue = context.firstParameter
+        val actualValue = context.secondParameter
+        val messageStr = tryGetMessageStr(context.thirdParameter)
+
+        val result = when (condition) {
+            "==" -> deepEqual(expectedValue, actualValue)
+            "!=" -> !deepEqual(expectedValue, actualValue)
+            else -> ConditionEvaluator.evaluate(expectedValue, condition, actualValue)
+        }
+        val functionName = context.functionName
+        if (!result) {
+            throw ComparisonAssertException(
+                "$functionName failed. Expected: \n${toReadableString(expectedValue)}\nReceived: \n${
+                    toReadableString(
+                        actualValue
+                    )
+                }\n$messageStr",
+                functionName,
+                expectedValue,
+                actualValue,
+                context.command.token.position
+            )
+        }
+
+        return null
+    }
+
+    /**
+     * Deep equality comparison that identifies objects and compares them
+     * ignoring the order of properties. Arrays are compared with order preserved.
+     */
+    private fun deepEqual(left: Any?, right: Any?): Boolean {
+        if (left == null && right == null) return true
+        if (left == null || right == null) return false
+
+        return when {
+            isJsonObject(left) && isJsonObject(right) ->
+                objectsEqualIgnoringPropertyOrder(toJsonNode(left), toJsonNode(right))
+            isJsonArray(left) && isJsonArray(right) ->
+                arraysEqual(toJsonNode(left) as ArrayNode, toJsonNode(right) as ArrayNode)
+            else -> ConditionEvaluator.equalish(left, right)
+        }
+    }
+
+    private fun isJsonObject(value: Any?): Boolean = when (value) {
+        is ObjectNode -> true
+        is JsonNode -> value.isObject
+        is Map<*, *> -> true
+        else -> false
+    }
+
+    private fun isJsonArray(value: Any?): Boolean = when (value) {
+        is ArrayNode -> true
+        is JsonNode -> value.isArray
+        else -> false
+    }
+
+    private fun toJsonNode(value: Any?): JsonNode = when (value) {
+        is JsonNode -> value
+        is Map<*, *> -> JsonConvert.convert(value)
+        else -> JsonConvert.convert(value)
+    }
+
+    private fun objectsEqualIgnoringPropertyOrder(left: JsonNode, right: JsonNode): Boolean {
+        if (!left.isObject || !right.isObject) return ConditionEvaluator.equalish(left, right)
+
+        val leftKeys = left.fieldNames().asSequence().toSet()
+        val rightKeys = right.fieldNames().asSequence().toSet()
+        if (leftKeys != rightKeys) return false
+
+        for (key in leftKeys) {
+            if (!deepEqual(left.get(key), right.get(key))) return false
+        }
+        return true
+    }
+
+    private fun arraysEqual(left: ArrayNode, right: ArrayNode): Boolean {
+        if (left.size() != right.size()) return false
+        for (i in 0 until left.size()) {
+            if (!deepEqual(left.get(i), right.get(i))) return false
+        }
+        return true
+    }
+
+    private fun toReadableString(value: Any?): String {
+        val valueStr = ConvertUtils.tryToString(value)
+        return when {
+            valueStr == null -> {
+                ""
+            }
+
+            valueStr.isEmpty() -> {
+                "\"\""
+            }
+
+            valueStr.isBlank() -> {
+                "\"$valueStr\""
+            }
+
+            else -> {
+                valueStr
+            }
+        }
+    }
+
+    private fun tryGetMessageStr(msg: Any?): String {
+        val message = ConvertUtils.tryToString(msg)
+        var messageStr = ""
+        if (message != null) {
+            messageStr = ". Additional message: $message"
+        }
+        return messageStr
+    }
+
+    private fun registerExtensionMethod(
+        context: TestOperationContext, name: String, method: (FunctionExecuteContext) -> Any?
+    ) {
+        context.registerExtensionMethod("$functionName.${name}") {
+            method(it)
+        }
+    }
+}
+
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/assertions/ComparisonAssertException.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/assertions/ComparisonAssertException.kt
new file mode 100644
index 0000000..a52b5e5
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/assertions/ComparisonAssertException.kt
@@ -0,0 +1,12 @@
+package com.intuit.isl.test.assertions
+
+import com.intuit.isl.utils.Position
+
+class ComparisonAssertException(
+    message: String,
+    functionName : String,
+    val expectedValue: Any?,
+    val actualValue: Any?,
+    position: Position? = null,
+    cause: Throwable? = null
+) : AssertException(message, functionName, position, cause)
\ No newline at end of file
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/assertions/EvaluationAssertException.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/assertions/EvaluationAssertException.kt
new file mode 100644
index 0000000..4264a71
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/assertions/EvaluationAssertException.kt
@@ -0,0 +1,11 @@
+package com.intuit.isl.test.assertions
+
+import com.intuit.isl.utils.Position
+
+class EvaluationAssertException(
+    message: String,
+    functionName : String,
+    val inputValue: Any?,
+    position: Position? = null,
+    cause: Throwable? = null
+) : AssertException(message, functionName, position, cause)
\ No newline at end of file
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/IslMockExecutor.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/IslMockExecutor.kt
new file mode 100644
index 0000000..60639e0
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/IslMockExecutor.kt
@@ -0,0 +1,17 @@
+package com.intuit.isl.test.mocks
+
+import com.intuit.isl.common.AsyncContextAwareExtensionMethod
+import com.intuit.isl.common.FunctionExecuteContext
+
+/**
+ * Represents a mock that runs compiled ISL code instead of returning a static value.
+ * When the mock is invoked, the runner is called with the same [FunctionExecuteContext]
+ * (parameters from the call), and its return value is used as the mock result.
+ */
+class IslMockExecutor(
+    private val runner: AsyncContextAwareExtensionMethod
+) {
+    suspend fun run(context: FunctionExecuteContext): Any? {
+        return runner(context)
+    }
+}
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockCaptureContext.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockCaptureContext.kt
new file mode 100644
index 0000000..704f09b
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockCaptureContext.kt
@@ -0,0 +1,7 @@
+package com.intuit.isl.test.mocks
+
+import com.fasterxml.jackson.databind.JsonNode
+
+class MockCaptureContext {
+    val captures = mutableListOf>()
+}
\ No newline at end of file
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockContext.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockContext.kt
new file mode 100644
index 0000000..a7eb5ed
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockContext.kt
@@ -0,0 +1,10 @@
+package com.intuit.isl.test.mocks
+
+class MockContext(mappingFunc : (mockObject : MockObject) -> T) {
+    val func : T
+    val mockObject = MockObject()
+
+    init {
+        func = mappingFunc(mockObject)
+    }
+}
\ No newline at end of file
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockFunction.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockFunction.kt
new file mode 100644
index 0000000..b529ea8
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockFunction.kt
@@ -0,0 +1,447 @@
+package com.intuit.isl.test.mocks
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.node.ArrayNode
+import com.fasterxml.jackson.databind.node.ObjectNode
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
+import com.intuit.isl.runtime.FileInfo
+import com.intuit.isl.runtime.TransformCompilationException
+import com.intuit.isl.runtime.TransformPackageBuilder
+import com.intuit.isl.test.TestOperationContext
+import com.intuit.isl.common.*
+import com.intuit.isl.utils.ConvertUtils
+import com.intuit.isl.utils.JsonConvert
+import java.nio.file.Path
+
+object MockFunction {
+    private const val funcRegex = "[A-Za-z_]+\\.[A-Za-z0-9_]+(#[0-9]+)?"
+    private const val annotationRegex = "[A-Za-z0-9_]+(#[0-9]+)?"
+
+    fun registerExtensions(context: TestOperationContext) {
+        context.registerExtensionMethod("Mock.Func") {
+            mockFunction(it, funcRegex) { funcName ->
+                createFunctionContext(context, funcName)
+            }
+        }
+
+        context.registerExtensionMethod("Mock.Annotation") {
+            mockFunction(it, annotationRegex) { funcName ->
+                createAnnotationContext(context, funcName)
+            }
+        }
+
+        context.registerExtensionMethod("Mock.StatementFunc") {
+            mockFunction(it, funcRegex) { funcName ->
+                createStatementFuncContext(context, funcName)
+            }
+        }
+
+        context.registerExtensionMethod("Mock.GetFuncCaptures") {
+            getCaptures(it, funcRegex) { funcName ->
+                context.mockExtensions.mockExtensions[funcName]
+            }
+        }
+
+        context.registerExtensionMethod("Mock.GetAnnotationCaptures") {
+            getCaptures(it, annotationRegex) { funcName ->
+                context.mockExtensions.mockAnnotations[funcName]
+            }
+        }
+
+        context.registerExtensionMethod("Mock.GetStatementFuncCaptures") {
+            getCaptures(it, funcRegex) { funcName ->
+                context.mockExtensions.mockStatementExtensions[funcName]
+            }
+        }
+
+        context.registerExtensionMethod("Mock.Load") {
+            loadMocksFromFile(it)
+        }
+    }
+
+    private fun loadMocksFromFile(executeContext: FunctionExecuteContext): Any? {
+        val context = executeContext.executionContext.operationContext as? TestOperationContext
+            ?: throw IllegalStateException("@.Mock.Load is only available in test context")
+        val fileName = ConvertUtils.tryToString(executeContext.firstParameter)
+            ?: throw IllegalArgumentException("@.Mock.Load requires a file name (string)")
+
+        val basePath = context.basePath
+            ?: throw IllegalStateException("@.Mock.Load requires basePath; run tests via isl test command or pass basePath to TransformTestPackageBuilder")
+
+        val currentFile = context.currentFile
+            ?: throw IllegalStateException("@.Mock.Load requires currentFile; run tests via isl test command")
+
+        val resolvedPath = resolvePath(basePath, currentFile, fileName)
+        val file = resolvedPath.toFile()
+
+        if (!file.exists()) {
+            throw IllegalArgumentException("File not found: $resolvedPath (resolved from $fileName relative to $currentFile)")
+        }
+        if (!file.isFile) {
+            throw IllegalArgumentException("Not a file: $resolvedPath")
+        }
+
+        val ext = file.extension.lowercase()
+        val root = when (ext) {
+            "json" -> JsonConvert.mapper.readTree(file)
+            "yaml", "yml" -> com.fasterxml.jackson.databind.ObjectMapper(YAMLFactory()).readTree(file)
+            else -> throw IllegalArgumentException("@.Mock.Load supports .json, .yaml, .yml; got: $fileName")
+        }
+
+        if (!root.isObject) {
+            throw IllegalArgumentException("Mock file must have a root object with 'func' and/or 'annotation' keys")
+        }
+
+        val relativePath = basePath.relativize(resolvedPath).toString().replace("\\", "/")
+        applyMocksFromNode(context, root as ObjectNode, relativePath)
+        return null
+    }
+
+    /**
+     * Applies mocks from a parsed object (e.g. from YAML/JSON) to the test context.
+     * Root must have "func" and/or "annotation" arrays in the same format as @.Mock.Load file.
+     * Mocks are always added; params differentiate multiple mocks for the same function.
+     * Clearing only happens when the next test starts (new TestOperationContext).
+     *
+     * @param islSourceFile Optional file path (e.g. mock file or test file) used as the module name when compiling inline ISL snippets, so compilation errors point to the correct file.
+     */
+    fun applyMocksFromNode(context: TestOperationContext, root: ObjectNode, islSourceFile: String? = null) {
+        val funcMocks = root.get("func")
+        val annotationMocks = root.get("annotation")
+
+        if (funcMocks != null && funcMocks.isArray) {
+            funcMocks.forEach { entry ->
+                if (entry.isObject) {
+                    registerMockFromNode(context, entry as ObjectNode, ::createFuncMock, funcRegex, islSourceFile)
+                }
+            }
+        }
+        if (annotationMocks != null && annotationMocks.isArray) {
+            annotationMocks.forEach { entry ->
+                if (entry.isObject) {
+                    registerMockFromNode(context, entry as ObjectNode, ::createAnnotationMock, annotationRegex, islSourceFile)
+                }
+            }
+        }
+    }
+
+    private fun resolvePath(basePath: Path, currentFile: String, fileName: String): Path {
+        val currentDir = basePath.resolve(currentFile).parent ?: basePath
+        return currentDir.resolve(fileName).normalize()
+    }
+
+    private fun registerMockFromNode(
+        context: TestOperationContext,
+        node: ObjectNode,
+        registerMock: (TestOperationContext, String, Any?, Map) -> Int?,
+        nameRegex: String,
+        islSourceFile: String? = null
+    ) {
+        val nameNode = node.get("name") ?: throw IllegalArgumentException("Mock entry must have 'name' field")
+        val name = ConvertUtils.tryToString(nameNode)?.trim()
+            ?: throw IllegalArgumentException("Mock 'name' must be a non-empty string")
+        if (name.isBlank()) {
+            throw IllegalArgumentException("Mock 'name' must be a non-empty string")
+        }
+        if (!name.matches(Regex(nameRegex))) {
+            throw IllegalArgumentException("Invalid mock name: $name")
+        }
+
+        val key = name.lowercase()
+
+        val islNode = node.get("isl")
+        val islContent = if (islNode != null && !islNode.isNull) ConvertUtils.tryToString(islNode)?.trim() else null
+
+        val sourceFile = islSourceFile ?: context.mockFileName ?: context.testFileName ?: context.currentFile
+        val returnNode = if (islContent != null) null else (node.get("result") ?: node.get("return"))
+        val returnValue: Any? = when {
+            islContent != null -> compileIslSnippetToExecutor(islContent, name, sourceFile)
+            returnNode == null || returnNode.isNull -> null
+            else -> returnNode
+        }
+
+        val params = mutableMapOf()
+        val paramsNode = node.get("params")
+        if (paramsNode != null) {
+            if (paramsNode.isArray) {
+                (paramsNode as ArrayNode).forEachIndexed { i, param ->
+                    params[i] = param
+                }
+            } else {
+                // Single value (e.g. params: "start_date") -> treat as single parameter at index 0
+                params[0] = paramsNode
+            }
+        }
+
+        registerMock(context, key, returnValue, params)
+    }
+
+    /***
+     * Create a function mock.
+     * @param context The test operation context.
+     * @param functionNameStr The function name string.
+     * @param returnValue The return value.
+     * @param parameters The parameters.
+     * @return The function id.
+     */
+    fun createFuncMock(
+        context: TestOperationContext, functionNameStr: String, returnValue: Any?, parameters: Map
+    ): Int? {
+        return mockFunction(functionNameStr, returnValue, parameters) {
+            createFunctionContext(context, it)
+        }
+    }
+
+    /***
+     * Create an annotation mock.
+     * @param context The test operation context.
+     * @param functionNameStr The function name string.
+     * @param returnValue The return value.
+     * @param parameters The parameters.
+     * @return The function id.
+     */
+    fun createAnnotationMock(
+        context: TestOperationContext, functionNameStr: String, returnValue: Any?, parameters: Map
+    ): Int? {
+        return mockFunction(functionNameStr, returnValue, parameters) {
+            createAnnotationContext(context, it)
+        }
+    }
+
+    /***
+     * Create a statement function mock.
+     * @param context The test operation context.
+     * @param functionNameStr The function name string.
+     * @param returnValue The return value.
+     * @param parameters The parameters.
+     * @return The function id.
+     */
+    fun createStatementFuncMock(
+        context: TestOperationContext, functionNameStr: String, returnValue: Any?, parameters: Map
+    ): Int? {
+        return mockFunction(functionNameStr, returnValue, parameters) {
+            createStatementFuncContext(context, it)
+        }
+    }
+
+    private fun createStatementFuncContext(
+        context: TestOperationContext,
+        funcName: String
+    ): MockContext {
+        val key = funcName.lowercase()
+        return context.mockExtensions.mockStatementExtensions.getOrPut(key) {
+            MockContext { mockObj ->
+                { mockContext, statementExecution ->
+                    val name = mockContext.functionName
+                    val paramsShort = shortParams(mockContext.parameters)
+                    println("[ISL Mock] Calling mocked statement function $name($paramsShort)")
+                    // Capture the argument inputs
+                    tryFindMatch(mockObj, mockContext)
+                    // Run the statement
+                    statementExecution(mockContext.executionContext)
+                    // Return null
+                    println("[ISL Mock] Returned mocked statement function $name=${shortValue(null)}")
+                    null
+                }
+            }
+        }
+    }
+
+    private fun createAnnotationContext(
+        context: TestOperationContext,
+        funcName: String
+    ): MockContext {
+        val key = funcName.lowercase()
+        return context.mockExtensions.mockAnnotations.getOrPut(key) {
+            MockContext { mockObj ->
+                { mockContext ->
+                    val name = mockContext.annotationName
+                    val paramsShort = shortParams(mockContext.parameters)
+                    println("[ISL Mock] Calling mocked modifier $name($paramsShort)")
+                    val r = tryFindMatch(mockObj, mockContext)
+                    val result = when {
+                        r != null && r !is IslMockExecutor -> r
+                        else -> mockContext.runNextCommand()
+                    }
+                    println("[ISL Mock] Returned mocked modifier $name=${shortValue(result)}")
+                    result
+                }
+            }
+        }
+    }
+
+    private fun createFunctionContext(
+        context: TestOperationContext,
+        funcName: String
+    ): MockContext {
+        val key = funcName.lowercase()
+        return context.mockExtensions.mockExtensions.getOrPut(key) {
+            MockContext { mockObj ->
+                { mockContext ->
+                    val name = mockContext.functionName
+                    val paramsShort = shortParams(mockContext.parameters)
+                    val r = tryFindMatch(mockObj, mockContext)
+                    //println("[ISL Mock] Calling mocked function $name($paramsShort) Match=$r ")
+                    val result = if (r is IslMockExecutor) r.run(mockContext) else r
+                    //println("[ISL Mock] Returned mocked function $name=${shortValue(result)}")
+                    result
+                }
+            }
+        }
+    }
+
+    /**
+     * Compiles an ISL snippet (e.g. a single function) and returns an executor that runs it in context.
+     * The snippet must define a function called "run": "fun run( ...)"
+     * When the mock is invoked, that function is run with the call's parameters bound to its arguments.
+     *
+     * @param islContent ISL source (e.g. "fun mask(\$value) { return `xxxxxx\$value`; }")
+     * @param mockName Mock name (for error messages)
+     * @param sourceFile Optional file path (e.g. mock YAML or test file) used as the module name so compilation errors show the correct file instead of __mock_isl__
+     * @throws TransformCompilationException if compilation fails
+     */
+    private fun compileIslSnippetToExecutor(islContent: String, mockName: String, sourceFile: String? = null): IslMockExecutor {
+        if (islContent.isBlank()) {
+            throw IllegalArgumentException("Mock '$mockName': 'isl' content must be non-empty")
+        }
+        val moduleName = sourceFile?.takeIf { it.isNotBlank() } ?: "__mock_isl__"
+        val pkg = try {
+            TransformPackageBuilder().build(mutableListOf(FileInfo(moduleName, islContent)), null)
+        } catch (e: Exception) {
+            val msg = (e as? TransformCompilationException)?.message ?: e.toString()
+            throw TransformCompilationException("Mock '$mockName': ISL compilation failed. $msg", (e as? TransformCompilationException)?.position)
+        }
+        val transformer = pkg.getModule(moduleName)
+            ?: throw TransformCompilationException("Mock '$mockName': compiled module not found", null)
+        val module = transformer.module
+        val firstFunc = module.getFunction("run")
+            ?: throw TransformCompilationException("Mock '$mockName': ISL snippet must define a function 'fun run(...){ ... }')", null)
+        val runner = module.getFunctionRunner(firstFunc.name)
+            ?: throw TransformCompilationException("Mock '$mockName': could not get runner for function ${firstFunc.name}", null)
+        return IslMockExecutor(runner)
+    }
+
+    private fun  mockFunction(
+        context: FunctionExecuteContext,
+        funcValidationRegex: String,
+        getMockContext: (funcNameStr: String) -> MockContext
+    ): Any? {
+        val (functionNameStr, returnValue, parameters) = parseMockFunctionExecuteContext(context, funcValidationRegex)
+
+        return mockFunction(functionNameStr, returnValue, parameters, getMockContext)
+    }
+
+    private fun  mockFunction(
+        functionNameStr: String,
+        returnValue: Any?,
+        parameters: Map,
+        getMockContext: (funcNameStr: String) -> MockContext
+    ): Int? {
+        val (baseName, index) = parseFunctionNameWithIndex(functionNameStr)
+        val obtainedContext = getMockContext(baseName)
+
+        return obtainedContext.mockObject.addMock(returnValue, parameters, index)
+    }
+
+    /**
+     * Parses function name for optional #index suffix.
+     * e.g. "data.getdata#1" -> ("data.getdata", 1), "data.getdata" -> ("data.getdata", null)
+     */
+    private fun parseFunctionNameWithIndex(functionNameStr: String): Pair {
+        val hashIndex = functionNameStr.lastIndexOf('#')
+        if (hashIndex >= 0) {
+            val baseName = functionNameStr.substring(0, hashIndex)
+            val indexStr = functionNameStr.substring(hashIndex + 1)
+            val index = indexStr.toIntOrNull()
+            if (index != null && index >= 1) {
+                return baseName to index
+            }
+        }
+        return functionNameStr to null
+    }
+
+    private fun parseMockFunctionExecuteContext(
+        context: FunctionExecuteContext, funcValidationRegex: String
+    ): Triple> {
+        val functionNameStr = getAndValidateFunctionName(context, funcValidationRegex)
+        val returnValue = context.secondParameter
+        val parameters = mutableMapOf()
+        if (context.parameters.size > 2) {
+            parameters.putAll(context.parameters.slice(2 until context.parameters.size).mapIndexed { i, it ->
+                i to JsonConvert.convert(it)
+            }.toMap())
+        }
+        return Triple(functionNameStr, returnValue, parameters)
+    }
+
+    private fun  getCaptures(
+        context: FunctionExecuteContext, regex: String, getMockContext: (funcNameStr: String) -> MockContext?
+    ): Any? {
+        val functionNameStr = getAndValidateFunctionName(context, regex)
+        val (baseName, _) = parseFunctionNameWithIndex(functionNameStr)
+        val instanceId = context.secondParameter
+
+        val obtainedContext =
+            getMockContext(baseName) ?: throw Exception("Mock function $baseName is not registered.")
+
+        return obtainedContext.mockObject.getCaptures(ConvertUtils.tryParseInt(instanceId))
+    }
+
+    private fun getAndValidateFunctionName(context: FunctionExecuteContext, regex: String): String {
+        val functionName = context.firstParameter
+        // Get the function name in lower case
+        val functionNameStr = ConvertUtils.tryToString(functionName)?.lowercase()
+        if (functionNameStr.isNullOrBlank()) {
+            throw Exception("Function name to mock is not provided.")
+        }
+        if (!validateFunctionName(functionNameStr, regex)) {
+            throw Exception("Valid function name to mock must provided. Invalid function name: $functionNameStr")
+        }
+        return functionNameStr
+    }
+
+    private fun validateFunctionName(functionName: String, regex: String): Boolean {
+        return functionName.matches(Regex(regex))
+    }
+
+
+    private fun tryFindMatch(mockObject: MockObject, executeContext: IExecuteContext): Any? {
+        val inputParams = executeContext.parameters.mapIndexed { i, it ->
+            i to JsonConvert.convert(it)
+        }.toMap()
+        val (name, position) = when (executeContext) {
+            is FunctionExecuteContext -> executeContext.functionName to executeContext.command.token.position
+            is AnnotationExecuteContext -> executeContext.annotationName to executeContext.command.token.position
+            else -> null to null
+        }
+        val testContext = when (executeContext) {
+            is FunctionExecuteContext -> executeContext.executionContext.operationContext as? TestOperationContext
+            is AnnotationExecuteContext -> executeContext.executionContext.operationContext as? TestOperationContext
+            else -> null
+        }
+        val mockFileName = testContext?.mockFileName
+        val testFileName = testContext?.testFileName
+        return mockObject.tryFindMatch(inputParams, true, name, position, mockFileName, testFileName)
+    }
+
+    private const val SHORT_MAX_LEN = 80
+
+    private fun shortValue(value: Any?, maxLen: Int = SHORT_MAX_LEN): String {
+        val s = when (value) {
+            null -> "null"
+            else -> try {
+                JsonConvert.mapper.writeValueAsString(JsonConvert.convert(value))
+            } catch (_: Exception) {
+                value.toString()
+            }
+        }
+        return if (s.length <= maxLen) s else s.take(maxLen - 3) + "..."
+    }
+
+    private fun shortParams(parameters: Array<*>): String {
+        val s = parameters.mapIndexed { i, p -> shortValue(p, 40) }.joinToString(", ")
+        return if (s.length <= SHORT_MAX_LEN) s else s.take(SHORT_MAX_LEN - 3) + "..."
+    }
+}
+
+
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockMatcher.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockMatcher.kt
new file mode 100644
index 0000000..a1d9290
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockMatcher.kt
@@ -0,0 +1,92 @@
+package com.intuit.isl.test.mocks
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.node.ArrayNode
+import com.fasterxml.jackson.databind.node.ObjectNode
+import com.intuit.isl.commands.ConditionEvaluator
+import com.intuit.isl.utils.JsonConvert
+
+class MockMatcher(private val field: JsonNode) {
+    private val parameterMap = hashMapOf()
+
+    init {
+        when (field) {
+            is ObjectNode -> {
+                field.fields().forEach {
+                    parameterMap[it.key] = MockMatcher(it.value)
+                }
+            }
+
+            is ArrayNode -> {
+                field.forEachIndexed { i, it ->
+                    parameterMap[i.toString()] = MockMatcher(it)
+                }
+            }
+        }
+    }
+
+    fun match(targetNode: JsonNode, looseMatch: Boolean = true): Boolean {
+        // Keep track of params that have been matched
+        val paramsToMatch = parameterMap.keys.toMutableSet()
+        val noParams = paramsToMatch.isEmpty()
+        var matchedField = false
+
+        when (targetNode) {
+            is ObjectNode -> {
+                targetNode.fields().forEach {
+                    val key = it.key
+                    val value = it.value
+                    val matcher = parameterMap[key]
+                    paramsToMatch.remove(key)
+                    if (matcher != null) {
+                        // Check if elements match
+                        if (!matcher.match(value, looseMatch)) {
+                            return false
+                        }
+                    }
+                    // If there's no matcher, then it's considered a match
+                    // only if it's a loose match
+                    else if (!looseMatch) {
+                        return false
+                    }
+                }
+            }
+
+            is ArrayNode -> {
+                targetNode.forEachIndexed { i, it ->
+                    val key = i.toString()
+                    val matcher = parameterMap[key]
+                    if (matcher != null) {
+                        paramsToMatch.remove(key)
+                        // Check if elements match
+                        if (!matcher.match(it, looseMatch)) {
+                            return false
+                        }
+                    }
+                    // If there's no matcher, then it's considered a match
+                    // only if it's a loose match
+                    else if (!looseMatch) {
+                        return false
+                    }
+                }
+            }
+
+            else -> {
+                matchedField = true
+                val srcValue = JsonConvert.getValue(field)
+                val targetValue = JsonConvert.getValue(targetNode)
+                // Do simple check for other elements
+                if (!ConditionEvaluator.equalish(srcValue, targetValue)) {
+                    return false
+                }
+            }
+        }
+        // If there's no params left to match, then it's a match
+        // If the field is matched and there's no params in the first place, then it's a match
+        return if (noParams && paramsToMatch.isEmpty()) {
+            matchedField
+        } else {
+            paramsToMatch.isEmpty()
+        }
+    }
+}
\ No newline at end of file
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockObject.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockObject.kt
new file mode 100644
index 0000000..eb20d39
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockObject.kt
@@ -0,0 +1,169 @@
+package com.intuit.isl.test.mocks
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.intuit.isl.runtime.TransformException
+import com.intuit.isl.utils.JsonConvert
+import com.intuit.isl.utils.Position
+
+class MockObject {
+    private val matchingParamMap = mutableMapOf()
+    private val matchingParamMapIndexed = mutableMapOf>()
+    private val matchingParamCallCount = mutableMapOf()
+    private val matchingParamCaptures = mutableMapOf()
+
+    private var defaultReturnValue: Any? = null
+    private val defaultReturnIndexed = mutableListOf()
+    private var defaultCallCount = 0
+    private val defaultReturnCaptures = MockCaptureContext()
+    /** True when a default (no-params) mock was ever added; used to distinguish "no match" from "matched and returned null". */
+    private var hasDefaultReturn = false
+
+    /** Clears all mocks and captures so this MockObject can be reused (e.g. for local overrides). */
+    fun clear() {
+        matchingParamMap.clear()
+        matchingParamMapIndexed.clear()
+        matchingParamCallCount.clear()
+        matchingParamCaptures.clear()
+        defaultReturnValue = null
+        defaultReturnIndexed.clear()
+        defaultCallCount = 0
+        defaultReturnCaptures.captures.clear()
+        hasDefaultReturn = false
+    }
+
+    fun addMock(returnValue: Any?, parameters: Map, index: Int? = null): Int? {
+        return if (parameters.isEmpty()) {
+            hasDefaultReturn = true
+            if (index != null) {
+                ensureIndexCapacity(defaultReturnIndexed, index)
+                defaultReturnIndexed[index - 1] = returnValue
+            } else {
+                defaultReturnValue = returnValue
+            }
+            null
+        } else {
+            val mockMatcher = MockParamsMatcher(parameters)
+            if (index != null) {
+                val list = matchingParamMapIndexed.getOrPut(mockMatcher) { mutableListOf() }
+                ensureIndexCapacity(list, index)
+                list[index - 1] = returnValue
+            } else {
+                matchingParamMap[mockMatcher] = returnValue
+            }
+            mockMatcher.hashCode()
+        }
+    }
+
+    private fun ensureIndexCapacity(list: MutableList, index: Int) {
+        while (list.size < index) {
+            list.add(null)
+        }
+    }
+
+    fun getCaptures(instanceId: Int?): Any? {
+        val captures = mutableListOf>()
+        if (instanceId == null) {
+            captures.addAll(matchingParamCaptures.values.flatMap { it.captures })
+            captures.addAll(defaultReturnCaptures.captures)
+        } else {
+            matchingParamCaptures[instanceId]?.let {
+                captures.addAll(it.captures)
+            }
+        }
+
+        return if (captures.isEmpty()) {
+            null
+        } else {
+            JsonConvert.convert(captures)
+        }
+    }
+
+    /**
+     * Finds a matching mock for the given parameters.
+     * @param functionName Optional; used in the error message when no match is found.
+     * @param position Optional; attached to the thrown exception when no match is found.
+     * @param mockFileName Optional; used in the error message when testFileName is null (which mock file to add the entry to).
+     * @param testFileName Optional; when set, error message suggests adding to the test file (in setup.mocks or in the test) instead of mock files.
+     * @throws TransformException when no mock matches and no default mock is defined (with function name, params, and YAML snippet in the message).
+     */
+    fun tryFindMatch(
+        targetParams: Map,
+        looseMatch: Boolean = true,
+        functionName: String? = null,
+        position: Position? = null,
+        mockFileName: String? = null,
+        testFileName: String? = null
+    ): Any? {
+        matchingParamMap.forEach { (matcher, returnValue) ->
+            if (matcher.match(targetParams, looseMatch)) {
+                val captureContext = matchingParamCaptures.getOrPut(matcher.hashCode()) { MockCaptureContext() }
+                captureContext.captures.add(targetParams)
+                return returnValue
+            }
+        }
+
+        matchingParamMapIndexed.forEach { (matcher, returnList) ->
+            if (matcher.match(targetParams, looseMatch)) {
+                val captureContext = matchingParamCaptures.getOrPut(matcher.hashCode()) { MockCaptureContext() }
+                captureContext.captures.add(targetParams)
+                val callCount = matchingParamCallCount.getOrPut(matcher.hashCode()) { 0 }
+                if (callCount >= returnList.size) {
+                    throw MockExhaustedException(
+                        "Mock exhausted: expected at most ${returnList.size} call(s), but got call #${callCount + 1}"
+                    )
+                }
+                val result = returnList[callCount]
+                matchingParamCallCount[matcher.hashCode()] = callCount + 1
+                return result
+            }
+        }
+
+        // Default (no param match): check indexed first, then standard
+        defaultReturnCaptures.captures.add(targetParams)
+        if (defaultReturnIndexed.isNotEmpty()) {
+            if (defaultCallCount >= defaultReturnIndexed.size) {
+                throw MockExhaustedException(
+                    "Mock exhausted: expected at most ${defaultReturnIndexed.size} call(s), but got call #${defaultCallCount + 1}"
+                )
+            }
+            val result = defaultReturnIndexed[defaultCallCount]
+            defaultCallCount++
+            return result
+        }
+        if (!hasDefaultReturn) {
+            val paramsJson = targetParams.toSortedMap().values.let { JsonConvert.mapper.writeValueAsString(it) }
+            val addToHint = when {
+                testFileName != null -> "test file [$testFileName] (in setup.mocks or in the test)"
+                else -> "[${mockFileName ?: "your-mocks.yaml"}]"
+            }
+            val place = position?.let { pos ->
+                "file=${pos.file}, line=${pos.line}, column=${pos.column}" +
+                    (pos.endLine?.let { ", endLine=$it" } ?: "") +
+                    (pos.endColumn?.let { ", endColumn=$it" } ?: "")
+            }
+            val yamlSnippet = buildString {
+                appendLine("- name: \"${functionName ?: ""}\"")
+                if (targetParams.isNotEmpty()) {
+                    appendLine("  params: $paramsJson")
+                }
+                appendLine("  result: ")
+            }
+            val message = buildString {
+                appendLine(if (functionName != null) "No mock matched for function @.$functionName. The test must only call with parameters that are mocked." else "No mock matched. The test must only call with parameters that are mocked.")
+                if (functionName != null) appendLine("Function: @.$functionName")
+                if (place != null) appendLine("Called from: $place")
+                appendLine("Parameters: $paramsJson")
+                appendLine("")
+                appendLine("To mock this function add this to your $addToHint then rerun the tests:")
+                appendLine("")
+                appendLine("func:")
+                appendLine(yamlSnippet)
+                appendLine("")
+            }
+            throw TransformException(message.trimEnd(), position, null)
+        }
+        return defaultReturnValue
+    }
+}
+
+class MockExhaustedException(message: String) : Exception(message)
\ No newline at end of file
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockParamsMatcher.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockParamsMatcher.kt
new file mode 100644
index 0000000..3f9cfe6
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/mocks/MockParamsMatcher.kt
@@ -0,0 +1,30 @@
+package com.intuit.isl.test.mocks
+
+import com.fasterxml.jackson.databind.JsonNode
+
+class MockParamsMatcher(matchingParams: Map) {
+    private val matchingParamMap = mutableMapOf()
+
+    init {
+        matchingParams.forEach { (i, it) ->
+            matchingParamMap[i] = MockMatcher(it)
+        }
+    }
+
+    fun match(targetParams: Map, looseMatch: Boolean = true): Boolean {
+        targetParams.forEach { (i, it) ->
+            val matcher = matchingParamMap[i]
+            if (matcher != null) {
+                if (!matcher.match(it, looseMatch)) {
+                    return false
+                }
+            }
+            // If there's no matcher, then it's considered a match
+            // only if it's a loose match
+            else if (!looseMatch) {
+                return false
+            }
+        }
+        return true
+    }
+}
\ No newline at end of file
diff --git a/isl-test/src/main/kotlin/com/intuit/isl/test/package.kt b/isl-test/src/main/kotlin/com/intuit/isl/test/package.kt
new file mode 100644
index 0000000..b091083
--- /dev/null
+++ b/isl-test/src/main/kotlin/com/intuit/isl/test/package.kt
@@ -0,0 +1,7 @@
+@file:JvmName("IslTest")
+package com.intuit.isl.test
+
+/**
+ * ISL test utilities and shared test infrastructure.
+ * This module depends on isl-transform for running and asserting transforms.
+ */
diff --git a/isl-test/src/test/kotlin/com/intuit/isl/test/IslTestModuleTest.kt b/isl-test/src/test/kotlin/com/intuit/isl/test/IslTestModuleTest.kt
new file mode 100644
index 0000000..4de771a
--- /dev/null
+++ b/isl-test/src/test/kotlin/com/intuit/isl/test/IslTestModuleTest.kt
@@ -0,0 +1,1028 @@
+package com.intuit.isl.test
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.intuit.isl.test.TransformTestPackage
+import com.intuit.isl.runtime.FileInfo
+import com.intuit.isl.runtime.TransformPackage
+import com.intuit.isl.runtime.TransformPackageBuilder
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.MethodSource
+import org.junit.jupiter.params.provider.ValueSource
+import java.util.stream.Stream
+import kotlin.test.assertContains
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class TransformTestPackageTest {
+    companion object {
+        @JvmStatic
+        fun testFunctions(): Stream {
+            return Stream.of(
+                // assert equal = true tests
+                Arguments.of("@.Assert.equal(1, 1)", true),
+                Arguments.of("@.Assert.equal(\"hello\", \"hello\")", true),
+                Arguments.of("@.Assert.equal(null, null)", true),
+                Arguments.of("@.Assert.equal(true, true)", true),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = 1;
+                    |${"$"}var2 = 1;
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "number" : 1 };
+                    |${"$"}var2 = { "number" : 1 };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : "valid" };
+                    |${"$"}var2 = { "words" : "valid" };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : true };
+                    |${"$"}var2 = { "words" : true };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : null };
+                    |${"$"}var2 = { "words" : null };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : [1, 2, 3] };
+                    |${"$"}var2 = { "words" : [1, 2, 3] };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : { "valid" : "yes" } };
+                    |${"$"}var2 = { "words" : { "valid" : "yes" } };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : { "valid" : "yes", "words" : { "valid" : "yes" } } };
+                    |${"$"}var2 = { "words" : { "valid" : "yes", "words" : { "valid" : "yes" } } };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                // Objects with different property order should be equal
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "a" : 1, "b" : 2, "c" : "three" };
+                    |${"$"}var2 = { "c" : "three", "a" : 1, "b" : 2 };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "outer" : { "x" : 1, "y" : 2 } };
+                    |${"$"}var2 = { "outer" : { "y" : 2, "x" : 1 } };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+
+                // assert equal = false tests
+                Arguments.of("@.Assert.equal(1, 2)", false),
+                Arguments.of("@.Assert.equal(\"hello\", \"there\")", false),
+                Arguments.of("@.Assert.equal(null, 1)", false),
+                Arguments.of("@.Assert.equal(true, false)", false),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = 1;
+                    |${"$"}var2 = 2;
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "number" : 1 };
+                    |${"$"}var2 = { "number" : 2 };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : "valid" };
+                    |${"$"}var2 = { "words" : "not" };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : true };
+                    |${"$"}var2 = { "words" : false };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : null };
+                    |${"$"}var2 = { "words" : 1 };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : [1, 2, 3] };
+                    |${"$"}var2 = { "words" : [1, 2, 4] };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : { "valid" : "yes" } };
+                    |${"$"}var2 = { "words" : { "valid" : "no" } };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : { "valid" : "yes", "words" : { "valid" : "yes" } } };
+                    |${"$"}var2 = { "words" : { "valid" : "yes", "words" : { "valid" : "no" } } };
+                    |@.Assert.equal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+
+                // assert notequal = false tests
+                Arguments.of("@.Assert.notequal(1, 1)", false),
+                Arguments.of("@.Assert.notequal(\"hello\", \"hello\")", false),
+                Arguments.of("@.Assert.notequal(null, null)", false),
+                Arguments.of("@.Assert.notequal(true, true)", false),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = 1;
+                    |${"$"}var2 = 1;
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "number" : 1 };
+                    |${"$"}var2 = { "number" : 1 };
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : "valid" };
+                    |${"$"}var2 = { "words" : "valid" };
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : true };
+                    |${"$"}var2 = { "words" : true };
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : null };
+                    |${"$"}var2 = { "words" : null };
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : [1, 2, 3] };
+                    |${"$"}var2 = { "words" : [1, 2, 3] };
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : { "valid" : "yes" } };
+                    |${"$"}var2 = { "words" : { "valid" : "yes" } };
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : { "valid" : "yes", "words" : { "valid" : "yes" } } };
+                    |${"$"}var2 = { "words" : { "valid" : "yes", "words" : { "valid" : "yes" } } };
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), false
+                ),
+
+                // assert notequal = true tests
+                Arguments.of("@.Assert.notequal(1, 2)", true),
+                Arguments.of("@.Assert.notequal(\"hello\", \"there\")", true),
+                Arguments.of("@.Assert.notequal(null, 1)", true),
+                Arguments.of("@.Assert.notequal(true, false)", true),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = 1;
+                    |${"$"}var2 = 2;
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "number" : 1 };
+                    |${"$"}var2 = { "number" : 2 };
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : "valid" };
+                    |${"$"}var2 = { "words" : "not" };
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : true };
+                    |${"$"}var2 = { "words" : false };
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : null };
+                    |${"$"}var2 = { "words" : 1 };
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : [1, 2, 3] };
+                    |${"$"}var2 = { "words" : [1, 2, 4] };
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : { "valid" : "yes" } };
+                    |${"$"}var2 = { "words" : { "valid" : "no" } };
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "words" : { "valid" : "yes", "words" : { "valid" : "yes" } } };
+                    |${"$"}var2 = { "words" : { "valid" : "yes", "words" : { "valid" : "no" } } };
+                    |@.Assert.notequal(${"$"}var1, ${"$"}var2)
+                """.trimMargin(), true
+                ),
+
+                // assert isnull tests
+                Arguments.of("@.Assert.isnull(null)", true),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = null
+                    |@.Assert.isnull(${"$"}var1)
+                """.trimMargin(), true
+                ),
+
+                Arguments.of("@.Assert.isnull(1)", false),
+                Arguments.of("@.Assert.isnull(\"hello\")", false),
+                Arguments.of("@.Assert.isnull(true)", false),
+                Arguments.of("@.Assert.isnull({ \"number\" : 1 })", false),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "number" : 1 }
+                    |@.Assert.isnull(${"$"}var1)
+                """.trimMargin(), false
+                ),
+
+                // assert notnull tests
+                Arguments.of("@.Assert.notnull(null)", false),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = null
+                    |@.Assert.notnull(${"$"}var1)
+                """.trimMargin(), false
+                ),
+
+                Arguments.of("@.Assert.notnull(1)", true),
+                Arguments.of("@.Assert.notnull(\"hello\")", true),
+                Arguments.of("@.Assert.notnull(true)", true),
+                Arguments.of("@.Assert.notnull({ \"number\" : 1 })", true),
+                Arguments.of(
+                    """
+                    |${"$"}var1 = { "number" : 1 }
+                    |@.Assert.notnull(${"$"}var1)
+                """.trimMargin(), true
+                ),
+            )
+        }
+
+        @JvmStatic
+        fun testMockFunctions(): Stream {
+            val mapper = ObjectMapper()
+            return Stream.of(
+                // mocks with default value
+                Arguments.of("Test.Function", "hello", true, null, null),
+                Arguments.of("Test.Function", "hello", true, null, listOf(1)),
+                Arguments.of("Test.Function", "hello", true, null, listOf("hello")),
+                Arguments.of("Test.Function", "hello", true, null, listOf(mapper.readTree("{ \"result\" : 1 }"))),
+
+                // mocks that return the value when params exactly match
+                Arguments.of("Test.Function", "hello", true, listOf("there"), listOf("there")),
+                Arguments.of("Test.Function", "hello", true, listOf(1), listOf(1)),
+                Arguments.of("Test.Function", "hello", true, listOf(true), listOf(true)),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    true,
+                    listOf(mapper.readTree("{ \"result\" : 1 }")),
+                    listOf(mapper.readTree("{ \"result\" : 1 }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    true,
+                    listOf(mapper.readTree("{ \"result\" : true }")),
+                    listOf(mapper.readTree("{ \"result\" : true }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    true,
+                    listOf(mapper.readTree("{ \"result\" : null }")),
+                    listOf(mapper.readTree("{ \"result\" : null }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    true,
+                    listOf(mapper.readTree("{ \"result\" : [1, 2, 3] }")),
+                    listOf(mapper.readTree("{ \"result\" : [1, 2, 3] }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    true,
+                    listOf(mapper.readTree("{ \"result\" : { \"valid\" : \"yes\" } }")),
+                    listOf(mapper.readTree("{ \"result\" : { \"valid\" : \"yes\" } }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    true,
+                    listOf(mapper.readTree("{ \"result\" : { \"valid\" : \"yes\", \"result\" : { \"valid\" : \"yes\" } } }")),
+                    listOf(mapper.readTree("{ \"result\" : { \"valid\" : \"yes\", \"result\" : { \"valid\" : \"yes\" } } }"))
+                ),
+
+                // mocks that return the value when params loosely match
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    true,
+                    listOf(mapper.readTree("{ \"result\" : \"yes\" }")),
+                    listOf(mapper.readTree("{ \"result\" : \"yes\", \"other\" : \"no\" }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    true,
+                    listOf(mapper.readTree("{ \"result\" : \"yes\" }")),
+                    listOf(mapper.readTree("{ \"result\" : \"yes\", \"other\" : 1 }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    true,
+                    listOf(mapper.readTree("{ \"result\" : \"yes\" }")),
+                    listOf(mapper.readTree("{ \"result\" : \"yes\", \"other\" : true }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    true,
+                    listOf(mapper.readTree("{ \"result\" : \"yes\" }")),
+                    listOf(mapper.readTree("{ \"result\" : \"yes\", \"other\" : [1, 2, 3] }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    true,
+                    listOf(mapper.readTree("{ \"result\" : [1, 2] }")),
+                    listOf(mapper.readTree("{ \"result\" : [1, 2, 4] }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    true,
+                    listOf(mapper.readTree("{ \"result\" : [{ \"valid\" : \"yes\" }, { \"valid\" : \"no\" }] }")),
+                    listOf(mapper.readTree("{ \"result\" : [{ \"valid\" : \"yes\" }, { \"valid\" : \"no\", \"invalid\" : \"yes\" }, 4] }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    true,
+                    listOf(mapper.readTree("{ \"result\" : { \"valid\" : \"yes\" } }")),
+                    listOf(mapper.readTree("{ \"result\" : { \"valid\" : \"yes\", \"not\" : \"there\" } }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    true,
+                    listOf(mapper.readTree("{ \"result\" : { \"valid\" : \"yes\" } }")),
+                    listOf(mapper.readTree("{ \"result\" : { \"valid\" : \"yes\", \"not\" : [1, 2, 3] } }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    true,
+                    listOf(mapper.readTree("{ \"result\" : { \"valid\" : \"yes\", \"result\" : { \"valid\" : \"yes\" } } }")),
+                    listOf(mapper.readTree("{ \"result\" : { \"valid\" : \"yes\", \"result\" : { \"valid\" : \"yes\", \"result\" : { \"valid\" : \"yes\" } } } }"))
+                ),
+
+                // mocks that don't return the value when params don't exactly match
+                Arguments.of("Test.Function", "hello", false, listOf(1), listOf(null)),
+                Arguments.of("Test.Function", "hello", false, listOf("there"), listOf("not there")),
+                Arguments.of("Test.Function", "hello", false, listOf(true), listOf(false)),
+                Arguments.of("Test.Function", "hello", false, listOf(1), listOf(2)),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    false,
+                    listOf(mapper.readTree("{ \"result\" : 1 }")),
+                    listOf(mapper.readTree("{ \"result\" : 2 }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    false,
+                    listOf(mapper.readTree("{ \"result\" : true }")),
+                    listOf(mapper.readTree("{ \"result\" : false }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    false,
+                    listOf(mapper.readTree("{ \"result\" : null }")),
+                    listOf(mapper.readTree("{ \"result\" : 1 }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    false,
+                    listOf(mapper.readTree("{ \"result\" : [1, 2, 3] }")),
+                    listOf(mapper.readTree("{ \"result\" : [1, 2, 4] }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    false,
+                    listOf(mapper.readTree("{ \"result\" : { \"valid\" : \"yes\" } }")),
+                    listOf(mapper.readTree("{ \"result\" : { \"valid\" : \"no\" } }"))
+                ),
+                Arguments.of(
+                    "Test.Function",
+                    "hello",
+                    false,
+                    listOf(mapper.readTree("{ \"result\" : { \"valid\" : \"yes\" } }")),
+                    listOf(mapper.readTree("{ \"result\" : { \"notValid\" : \"yes\" } }"))
+                ),
+            )
+        }
+
+        @JvmStatic
+        fun testCaptureFunctions(): Stream {
+            return Stream.of(
+                Arguments.of(
+                    "0", "[{ \"0\" : 0 }]", 1
+                ), Arguments.of(
+                    "\"hello\"", "[{ \"0\" : \"hello\" }]", 1
+                ), Arguments.of(
+                    "0, 1, 2", "[{ \"0\" : 0, \"1\" : 1, \"2\" : 2 }]", 1
+                ), Arguments.of(
+                    "0, \"1\", \"hello\"", "[{ \"0\" : 0, \"1\" : \"1\", \"2\" : \"hello\" }]", 1
+                ), Arguments.of(
+                    "0", "[{ \"0\" : 0 }, { \"0\" : 0 }, { \"0\" : 0 }]", 3
+                ), Arguments.of(
+                    "\"hello\"", "[{ \"0\" : \"hello\" }, { \"0\" : \"hello\" }, { \"0\" : \"hello\" }]", 3
+                ), Arguments.of(
+                    "0, 1, 2",
+                    "[{ \"0\" : 0, \"1\" : 1, \"2\" : 2 }, { \"0\" : 0, \"1\" : 1, \"2\" : 2 }, { \"0\" : 0, \"1\" : 1, \"2\" : 2 }]",
+                    3
+                ), Arguments.of(
+                    "0, \"1\", \"hello\"",
+                    "[{ \"0\" : 0, \"1\" : \"1\", \"2\" : \"hello\" }, { \"0\" : 0, \"1\" : \"1\", \"2\" : \"hello\" }, { \"0\" : 0, \"1\" : \"1\", \"2\" : \"hello\" }]",
+                    3
+                )
+            )
+        }
+    }
+
+    private val testFileFunction = "test"
+    private val testFileName = "testFile.isl"
+
+    private val transformPackageBuilder = TransformPackageBuilder()
+    private lateinit var transformPackage: TransformPackage
+
+    private fun setup(testFile: String) {
+        val fileInfo = mutableListOf(FileInfo(testFileName, testFile))
+        transformPackage = transformPackageBuilder.build(fileInfo)
+    }
+
+    private fun createParamsString(params: List?, firstParam: Boolean): String {
+        return (if (params == null) {
+            ""
+        } else ((if (firstParam) {
+            ""
+        } else ", ") + params.joinToString(", ", transform = ::createParamString)))
+    }
+
+    private fun createParamString(param: Any?): String {
+        return when (param) {
+            (param == null) -> "null"
+            is Int -> param.toString()
+            is JsonNode -> param.toPrettyString()
+            is Boolean -> param.toString()
+            else -> "\"$param\""
+        }
+    }
+
+
+    @ParameterizedTest
+    @MethodSource("testFunctions")
+    fun inputTransformPackageWithTest_RunTest_AssertTestResultExists(func: String) {
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   $func
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val testResult = testPackage.runTest(testFileName, "test")
+        assertFalse(testResult.testResults.isEmpty())
+        assertTrue {
+            testResult.testResults.any {
+                it.testName == testFileFunction
+            }
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource("testFunctions")
+    fun inputTransformPackageWithTest_RunTest_AssertTestResultEqualsExpected(func: String, expectResult: Boolean) {
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   $func
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val testResult = testPackage.runTest(testFileName, "test")
+        assertFalse(testResult.testResults.isEmpty())
+        assertTrue {
+            testResult.testResults.all {
+                it.success == expectResult
+            }
+        }
+    }
+
+    @Test
+    fun inputTransformPackageWithTest_RunFailedTest_InputErrorMessageInError() {
+        val errorMsg = "This was a failure"
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   @.Assert.Equal(1, 2, "$errorMsg")
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val result = testPackage.runTest(testFileName, "test")
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first {
+            it.testName == testFileFunction
+        }
+        assertContains(testResult.message ?: "", errorMsg)
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = [
+        "Test.Function",
+        "test.function",
+        "Test.function",
+        "test.Function",
+        "Test.FUNCTION",
+        "test.FUNCTION",
+        "Test.function1",
+        "test.function1",
+        "Test.FUNCTION1",
+        "test.FUNCTION1",
+        "Test.function_1",
+        "test.function_1",
+        "Test.FUNCTION_1",
+        "test.FUNCTION_1",
+        "Test.Function#1",
+        "test.function#2"
+    ])
+    fun inputTransformPackageWithMock_MockFunction_VerifyFunctionNamePasses(funcName : String) {
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   @.Mock.func("$funcName")
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val result = testPackage.runTest(testFileName, "test")
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first {
+            it.testName == testFileFunction
+        }
+        assertTrue(testResult.success, testResult.message)
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = [
+        "Test1.Function",
+        "test1.function",
+        "Test1.function",
+        "test1.Function",
+        "Test_1.function",
+        "test_1.function",
+        "Test_1.function",
+        "test_1.Function",
+        "Test_1.h@!!0",
+        "test.hello.there",
+        "Test.h@llo",
+        "test_hell0"
+    ])
+    fun inputTransformPackageWithMock_MockFunction_VerifyIncorrectFunctionNameFails(funcName : String) {
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   @.Mock.func("$funcName")
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val result = testPackage.runTest(testFileName, "test")
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first {
+            it.testName == testFileFunction
+        }
+        assertFalse(testResult.success)
+    }
+
+    @ParameterizedTest
+    @MethodSource("testMockFunctions")
+    fun inputTransformPackageWithTestAndMock_RunTestWithMock_AssertMockReturnsInputValue(
+        functionName: String,
+        expectedReturnValue: Any?,
+        expectResult: Boolean,
+        matchingParams: List? = null,
+        inputParams: List? = null
+    ) {
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   @.Mock.func("$functionName", ${createParamString(expectedReturnValue)}${
+            createParamsString(
+                matchingParams, false
+            )
+        })
+        |   ${"$"}value = @.$functionName(${createParamsString(inputParams, true)})
+        |   @.Assert.equal(${createParamString(expectedReturnValue)}, ${"$"}value)
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val result = testPackage.runTest(testFileName, "test")
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first {
+            it.testName == testFileFunction
+        }
+        assertEquals(expectResult, testResult.success, testResult.message)
+    }
+
+    @ParameterizedTest
+    @MethodSource("testCaptureFunctions")
+    fun inputTransformPackageWithTestAndMock_RunMock_VerifyParamCaptureIsReturned(
+        inputParams: String, expectedCaptures: String, numberOfTimesToCallFunc: Int
+    ) {
+        val functionName = "Test.Function"
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   ${"$"}instanceId = @.Mock.func("$functionName")
+        |   ${"@.$functionName($inputParams)".repeat(numberOfTimesToCallFunc)}
+        |   ${"$"}value = @.Mock.GetFuncCaptures("$functionName", ${"$"}instanceId)
+        |   @.Assert.equal($expectedCaptures, ${"$"}value)
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val result = testPackage.runTest(testFileName, "test")
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first {
+            it.testName == testFileFunction
+        }
+        assertTrue(testResult.success, testResult.message)
+    }
+
+    @Test
+    fun runFuncMockWithDifferentParams_GetCapturesWithoutInstanceId_VerifyCorrectParamCaptureIsReturned() {
+        val functionName = "Test.Function"
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   ${"$"}instanceId = @.Mock.func("$functionName", null, 1)
+        |   @.$functionName(0)
+        |   @.$functionName(1)
+        |   ${"$"}value = @.Mock.GetFuncCaptures("$functionName", ${"$"}instanceId)
+        |   @.Assert.equal([{ "0": 1 }], ${"$"}value)
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val result = testPackage.runTest(testFileName, "test")
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first {
+            it.testName == testFileFunction
+        }
+        assertTrue(testResult.success, testResult.message)
+    }
+
+    @Test
+    fun runFuncMockWithDifferentParams_GetCapturesWithoutInstanceId_VerifyAllCapturesAreReturned() {
+        val functionName = "Test.Function"
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   ${"$"}instanceId = @.Mock.func("$functionName", null, 1)
+        |   @.$functionName(0)
+        |   @.$functionName(1)
+        |   ${"$"}value = @.Mock.GetFuncCaptures("$functionName")
+        |   @.Assert.equal([{ "0": 1 }, {"0": 0 }], ${"$"}value)
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val result = testPackage.runTest(testFileName, "test")
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first {
+            it.testName == testFileFunction
+        }
+        assertTrue(testResult.success, testResult.message)
+    }
+
+    @Test
+    fun runAnnotationMockWithDifferentParams_GetCapturesWithInstanceId_VerifyCorrectParamCaptureIsReturned() {
+        val annotationName = "hello"
+        val functionName = "greetings"
+        val functionCallName = "This.$functionName"
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   ${"$"}instanceId = @.Mock.annotation("$annotationName", null, 1)
+        |   @.${functionCallName}A(1)
+        |   @.${functionCallName}B(1)
+        |   ${"$"}value = @.Mock.GetAnnotationCaptures("$annotationName", ${"$"}instanceId)
+        |   @.Assert.equal([{ "0": 1 }], ${"$"}value)
+        |}
+        |
+        |@hello(0)
+        |fun ${functionName}A(${"$"}input) {
+        |   return "Hello"
+        |}
+        |
+        |@hello(1)
+        |fun ${functionName}B(${"$"}input) {
+        |   return "Hello"
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val result = testPackage.runTest(testFileName, "test")
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first {
+            it.testName == testFileFunction
+        }
+        assertTrue(testResult.success, testResult.message)
+    }
+
+    @Test
+    fun runAnnotationMockWithDifferentParams_GetCapturesWithoutInstanceId_VerifyAllCapturesAreReturned() {
+        val annotationName = "hello"
+        val functionName = "greetings"
+        val functionCallName = "This.$functionName"
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   ${"$"}instanceId = @.Mock.annotation("$annotationName", null, 1)
+        |   ${"$"}result1 = @.${functionCallName}1(1)
+        |   ${"$"}result2 = @.${functionCallName}2(1)
+        |   ${"$"}value = @.Mock.GetAnnotationCaptures("$annotationName")
+        |   @.Assert.equal([{ "0": 1 }, {"0": 0 }], ${"$"}value)
+        |}
+        |
+        |@$annotationName(0)
+        |fun ${functionName}1(${"$"}input) {
+        |   return "Hello"
+        |}
+        |
+        |@$annotationName(1)
+        |fun ${functionName}2(${"$"}input) {
+        |   return "Hello"
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val result = testPackage.runTest(testFileName, "test")
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first {
+            it.testName == testFileFunction
+        }
+        assertTrue(testResult.success, testResult.message)
+    }
+
+    @Test
+    fun runStatementFuncMockWithDifferentParams_GetCapturesWithInstanceId_VerifyCorrectParamCaptureIsReturned() {
+        val functionName = "Test.Function"
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   ${"$"}instanceId = @.Mock.statementFunc("$functionName", null, 1)
+        |   @.$functionName(0) {
+        |     ${"$"}value = 1
+        |   }
+        |   @.$functionName(1) {
+        |     ${"$"}value = 1
+        |   }
+        |   ${"$"}value = @.Mock.GetStatementFuncCaptures("$functionName", ${"$"}instanceId)
+        |   @.Assert.equal([{ "0": 1 }], ${"$"}value)
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val result = testPackage.runTest(testFileName, "test")
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first {
+            it.testName == testFileFunction
+        }
+        assertTrue(testResult.success, testResult.message)
+    }
+
+    @Test
+    fun runStatementFuncMockWithDifferentParams_GetCapturesWithoutInstanceId_VerifyAllCapturesAreReturned() {
+        val functionName = "Test.Function"
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   ${"$"}instanceId = @.Mock.statementFunc("$functionName", null, 1)
+        |   @.$functionName(0) {
+        |     ${"$"}value = 1
+        |   }
+        |   @.$functionName(1) {
+        |     ${"$"}value = 1
+        |   }
+        |   ${"$"}value = @.Mock.GetStatementFuncCaptures("$functionName")
+        |   @.Assert.equal([{ "0": 1 }, {"0": 0 }], ${"$"}value)
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val result = testPackage.runTest(testFileName, "test")
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first {
+            it.testName == testFileFunction
+        }
+        assertTrue(testResult.success, testResult.message)
+    }
+
+    @Test
+    fun indexedFuncMock_ReturnsDifferentValuesPerCall_Succeeds() {
+        val functionName = "Test.Function"
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   @.Mock.func("$functionName#1", 5)
+        |   @.Mock.func("$functionName#2", 3)
+        |   @.Mock.func("$functionName#3", null)
+        |   ${"$"}r1 = @.$functionName()
+        |   ${"$"}r2 = @.$functionName()
+        |   ${"$"}r3 = @.$functionName()
+        |   @.Assert.equal(5, ${"$"}r1)
+        |   @.Assert.equal(3, ${"$"}r2)
+        |   @.Assert.isnull(${"$"}r3)
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val result = testPackage.runTest(testFileName, "test")
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first { it.testName == testFileFunction }
+        assertTrue(testResult.success, testResult.message)
+    }
+
+    @Test
+    fun indexedFuncMock_Exhausted_FailsWithClearMessage() {
+        val functionName = "Test.Function"
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   @.Mock.func("$functionName#1", 5)
+        |   @.Mock.func("$functionName#2", 3)
+        |   ${"$"}r1 = @.$functionName()
+        |   ${"$"}r2 = @.$functionName()
+        |   ${"$"}r3 = @.$functionName()
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val result = testPackage.runTest(testFileName, "test")
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first { it.testName == testFileFunction }
+        assertFalse(testResult.success)
+        assertContains(testResult.message ?: "", "Mock exhausted")
+    }
+
+    @Test
+    fun indexedAnnotationMock_AllowsMultipleCallsWhenDefined_Succeeds() {
+        val annotationName = "seq"
+        val functionName = "greetings"
+        val functionCallName = "This.$functionName"
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   @.Mock.annotation("$annotationName#1", null)
+        |   @.Mock.annotation("$annotationName#2", null)
+        |   ${"$"}r1 = @.${functionCallName}A()
+        |   ${"$"}r2 = @.${functionCallName}B()
+        |   @.Assert.equal("Hello", ${"$"}r1)
+        |   @.Assert.equal("Hello", ${"$"}r2)
+        |}
+        |
+        |@$annotationName
+        |fun ${functionName}A() {
+        |   return "Hello"
+        |}
+        |
+        |@$annotationName
+        |fun ${functionName}B() {
+        |   return "Hello"
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val result = testPackage.runTest(testFileName, "test")
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first { it.testName == testFileFunction }
+        assertTrue(testResult.success, testResult.message)
+    }
+
+    @Test
+    fun indexedAnnotationMock_Exhausted_FailsWithClearMessage() {
+        val annotationName = "seq"
+        val functionName = "greetings"
+        val functionCallName = "This.$functionName"
+        val testFile = """
+        |@test
+        |fun ${testFileFunction}() {
+        |   @.Mock.annotation("$annotationName#1", "first")
+        |   ${"$"}r1 = @.${functionCallName}A()
+        |   ${"$"}r2 = @.${functionCallName}B()
+        |}
+        |
+        |@$annotationName
+        |fun ${functionName}A() {
+        |   return "Hello"
+        |}
+        |
+        |@$annotationName
+        |fun ${functionName}B() {
+        |   return "Hello"
+        |}
+        """.trimMargin()
+        setup(testFile)
+        val testPackage = TransformTestPackage(transformPackage)
+        val result = testPackage.runTest(testFileName, "test")
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first { it.testName == testFileFunction }
+        assertFalse(testResult.success)
+        assertContains(testResult.message ?: "", "Mock exhausted")
+    }
+}
\ No newline at end of file
diff --git a/isl-test/src/test/kotlin/com/intuit/isl/test/LoadFunctionTest.kt b/isl-test/src/test/kotlin/com/intuit/isl/test/LoadFunctionTest.kt
new file mode 100644
index 0000000..ca4442b
--- /dev/null
+++ b/isl-test/src/test/kotlin/com/intuit/isl/test/LoadFunctionTest.kt
@@ -0,0 +1,127 @@
+package com.intuit.isl.test
+
+import com.intuit.isl.runtime.FileInfo
+import com.intuit.isl.test.annotations.TestResultContext
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class LoadFunctionTest {
+
+    @Test
+    fun loadFrom_jsonFile_returnsParsedJson(@TempDir tempDir: Path) {
+        val fixturesDir = tempDir.resolve("tests").resolve("fixtures")
+        Files.createDirectories(fixturesDir)
+        Files.writeString(fixturesDir.resolve("data.json"), """{"name": "test", "value": 42}""")
+
+        val testIsl = tempDir.resolve("tests").resolve("sample.isl")
+        Files.createDirectories(testIsl.parent)
+        Files.writeString(
+            testIsl,
+            """
+            |@test
+            |fun testLoadJson() {
+            |   ${'$'}data = @.Load.From("fixtures/data.json")
+            |   @.Assert.equal("test", ${'$'}data.name)
+            |   @.Assert.equal(42, ${'$'}data.value)
+            |}
+            """.trimMargin()
+        )
+
+        val fileInfos = mutableListOf(
+            FileInfo("tests/sample.isl", testIsl.toFile().readText())
+        )
+        val testPackage = TransformTestPackageBuilder().build(fileInfos, null, tempDir)
+        val result = testPackage.runTest("tests/sample.isl", "testLoadJson")
+
+        assertFalse(result.testResults.isEmpty(), "Expected test results; got: ${result.testResults}")
+        val testResult = result.testResults.firstOrNull { it.testName.equals("testLoadJson", ignoreCase = true) }
+            ?: result.testResults.first()
+        assertTrue(testResult.success, "Test failed: ${testResult.message}")
+    }
+
+    @Test
+    fun loadFrom_yamlFile_returnsParsedJson(@TempDir tempDir: Path) {
+        val fixturesDir = tempDir.resolve("tests").resolve("fixtures")
+        Files.createDirectories(fixturesDir)
+        Files.writeString(
+            fixturesDir.resolve("config.yaml"),
+            """
+            |key: value
+            |nested:
+            |  count: 10
+            """.trimMargin()
+        )
+
+        val testIsl = tempDir.resolve("tests").resolve("sample.isl")
+        Files.createDirectories(testIsl.parent)
+        Files.writeString(
+            testIsl,
+            """
+            |@test
+            |fun testLoadYaml() {
+            |   ${'$'}data = @.Load.From("fixtures/config.yaml")
+            |   @.Assert.equal("value", ${'$'}data.key)
+            |   @.Assert.equal(10, ${'$'}data.nested.count)
+            |}
+            """.trimMargin()
+        )
+
+        val fileInfos = mutableListOf(
+            FileInfo("tests/sample.isl", testIsl.toFile().readText())
+        )
+        val testPackage = TransformTestPackageBuilder().build(fileInfos, null, tempDir)
+        val result = testPackage.runTest("tests/sample.isl", "testLoadYaml")
+
+        assertFalse(result.testResults.isEmpty(), "Expected test results; got: ${result.testResults}")
+        val testResult = result.testResults.firstOrNull { it.testName.equals("testLoadYaml", ignoreCase = true) }
+            ?: result.testResults.first()
+        assertTrue(testResult.success, "Test failed: ${testResult.message}")
+    }
+
+    @Test
+    fun loadFrom_csvFile_returnsArrayOfObjects(@TempDir tempDir: Path) {
+        val fixturesDir = tempDir.resolve("tests").resolve("fixtures")
+        Files.createDirectories(fixturesDir)
+        Files.writeString(
+            fixturesDir.resolve("data.csv"),
+            """
+            |id,name,score
+            |1,Alice,100
+            |2,Bob,85
+            """.trimMargin()
+        )
+
+        val testIsl = tempDir.resolve("tests").resolve("sample.isl")
+        Files.createDirectories(testIsl.parent)
+        Files.writeString(
+            testIsl,
+            """
+            |@test
+            |fun testLoadCsv() {
+            |   ${'$'}data = @.Load.From("fixtures/data.csv")
+            |   @.Assert.equal(2, ${'$'}data | length)
+            |   @.Assert.equal("Alice", ${'$'}data[0].name)
+            |   @.Assert.equal("Bob", ${'$'}data[1].name)
+            |   @.Assert.equal(100, ${'$'}data[0].score)
+            |}
+            """.trimMargin()
+        )
+
+        val fileInfos = mutableListOf(
+            FileInfo("tests/sample.isl", testIsl.toFile().readText())
+        )
+        val testPackage = TransformTestPackageBuilder().build(fileInfos, null, tempDir)
+        val result = testPackage.runTest("tests/sample.isl", "testLoadCsv")
+
+        assertFalse(result.testResults.isEmpty(), "Expected test results; got: ${result.testResults}")
+        val testResult = result.testResults.firstOrNull { it.testName.equals("testLoadCsv", ignoreCase = true) }
+            ?: result.testResults.first()
+        assertTrue(testResult.success, "Test failed: ${testResult.message}")
+    }
+}
+
diff --git a/isl-test/src/test/kotlin/com/intuit/isl/test/MockLoadTest.kt b/isl-test/src/test/kotlin/com/intuit/isl/test/MockLoadTest.kt
new file mode 100644
index 0000000..bac310e
--- /dev/null
+++ b/isl-test/src/test/kotlin/com/intuit/isl/test/MockLoadTest.kt
@@ -0,0 +1,347 @@
+package com.intuit.isl.test
+
+import com.intuit.isl.runtime.FileInfo
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.test.assertContains
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class MockLoadTest {
+
+    @Test
+    fun mockLoad_yamlFile_registersMocksAndReturnsExpectedValues(@TempDir tempDir: Path) {
+        val mocksDir = tempDir.resolve("tests").resolve("mocks")
+        Files.createDirectories(mocksDir)
+        Files.writeString(
+            mocksDir.resolve("api-mocks.yaml"),
+            """
+            |func:
+            |  - name: "Test.Function#1"
+            |    return: 5
+            |  - name: "Test.Function#2"
+            |    return: 3
+            |  - name: "Test.Function#3"
+            |    return: null
+            """.trimMargin()
+        )
+
+        val testIsl = tempDir.resolve("tests").resolve("sample.isl")
+        Files.createDirectories(testIsl.parent)
+        Files.writeString(
+            testIsl,
+            """
+            |@test
+            |fun testMockLoad() {
+            |   @.Mock.Load("mocks/api-mocks.yaml")
+            |   ${'$'}r1 = @.Test.Function()
+            |   ${'$'}r2 = @.Test.Function()
+            |   ${'$'}r3 = @.Test.Function()
+            |   @.Assert.equal(5, ${'$'}r1)
+            |   @.Assert.equal(3, ${'$'}r2)
+            |   @.Assert.isnull(${'$'}r3)
+            |}
+            """.trimMargin()
+        )
+
+        val fileInfos = mutableListOf(
+            FileInfo("tests/sample.isl", testIsl.toFile().readText())
+        )
+        val testPackage = TransformTestPackageBuilder().build(fileInfos, null, tempDir)
+        val result = testPackage.runTest("tests/sample.isl", "testMockLoad")
+
+        assertFalse(result.testResults.isEmpty(), "Expected test results; got: ${result.testResults}")
+        val testResult = result.testResults.firstOrNull { it.testName.equals("testMockLoad", ignoreCase = true) }
+            ?: result.testResults.first()
+        assertTrue(testResult.success, "Test failed: ${testResult.message}")
+    }
+
+    @Test
+    fun mockLoad_yamlFile_withParams_registersParamBasedMocks(@TempDir tempDir: Path) {
+        val mocksDir = tempDir.resolve("tests").resolve("mocks")
+        Files.createDirectories(mocksDir)
+        Files.writeString(
+            mocksDir.resolve("param-mocks.yaml"),
+            """
+            |func:
+            |  - name: "Test.Function"
+            |    return: "matched"
+            |    params: [4]
+            |  - name: "Test.Function"
+            |    return: null
+            """.trimMargin()
+        )
+
+        val testIsl = tempDir.resolve("tests").resolve("sample.isl")
+        Files.createDirectories(testIsl.parent)
+        Files.writeString(
+            testIsl,
+            """
+            |@test
+            |fun testMockLoadParams() {
+            |   @.Mock.Load("mocks/param-mocks.yaml")
+            |   ${'$'}r1 = @.Test.Function(4)
+            |   ${'$'}r2 = @.Test.Function(5)
+            |   @.Assert.equal("matched", ${'$'}r1)
+            |   @.Assert.notEqual("matched", ${'$'}r2)
+            |}
+            """.trimMargin()
+        )
+
+        val fileInfos = mutableListOf(
+            FileInfo("tests/sample.isl", testIsl.toFile().readText())
+        )
+        val testPackage = TransformTestPackageBuilder().build(fileInfos, null, tempDir)
+        val result = testPackage.runTest("tests/sample.isl", "testMockLoadParams")
+
+        assertFalse(result.testResults.isEmpty(), "Expected test results; got: ${result.testResults}")
+        val testResult = result.testResults.firstOrNull { it.testName.equals("testMockLoadParams", ignoreCase = true) }
+            ?: result.testResults.first()
+        assertTrue(testResult.success, "Test failed: ${testResult.message}")
+    }
+
+    @Test
+    fun mockLoad_jsonFile_registersMocks(@TempDir tempDir: Path) {
+        val mocksDir = tempDir.resolve("tests").resolve("mocks")
+        Files.createDirectories(mocksDir)
+        Files.writeString(
+            mocksDir.resolve("mocks.json"),
+            """
+            |{
+            |  "func": [
+            |    { "name": "Test.Function", "return": "from-json" }
+            |  ]
+            |}
+            """.trimMargin()
+        )
+
+        val testIsl = tempDir.resolve("tests").resolve("sample.isl")
+        Files.createDirectories(testIsl.parent)
+        Files.writeString(
+            testIsl,
+            """
+            |@test
+            |fun testMockLoadJson() {
+            |   @.Mock.Load("mocks/mocks.json")
+            |   ${'$'}r = @.Test.Function()
+            |   @.Assert.equal("from-json", ${'$'}r)
+            |}
+            """.trimMargin()
+        )
+
+        val fileInfos = mutableListOf(
+            FileInfo("tests/sample.isl", testIsl.toFile().readText())
+        )
+        val testPackage = TransformTestPackageBuilder().build(fileInfos, null, tempDir)
+        val result = testPackage.runTest("tests/sample.isl", "testMockLoadJson")
+
+        assertFalse(result.testResults.isEmpty(), "Expected test results; got: ${result.testResults}")
+        val testResult = result.testResults.firstOrNull { it.testName.equals("testMockLoadJson", ignoreCase = true) }
+            ?: result.testResults.first()
+        assertTrue(testResult.success, "Test failed: ${testResult.message}")
+    }
+
+    @Test
+    fun mockLoad_fileNotFound_failsWithClearMessage(@TempDir tempDir: Path) {
+        val testIsl = tempDir.resolve("tests").resolve("sample.isl")
+        Files.createDirectories(testIsl.parent)
+        Files.writeString(
+            testIsl,
+            """
+            |@test
+            |fun testMockLoadMissing() {
+            |   @.Mock.Load("mocks/nonexistent.yaml")
+            |}
+            """.trimMargin()
+        )
+
+        val fileInfos = mutableListOf(
+            FileInfo("tests/sample.isl", testIsl.toFile().readText())
+        )
+        val testPackage = TransformTestPackageBuilder().build(fileInfos, null, tempDir)
+        val result = testPackage.runTest("tests/sample.isl", "testMockLoadMissing")
+
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.first()
+        assertFalse(testResult.success)
+        assertContains(testResult.message ?: "", "File not found")
+    }
+
+    @Test
+    fun mockLoad_yamlFile_withIslSnippet_compilesAndRunsMockInContext(@TempDir tempDir: Path) {
+        val mocksDir = tempDir.resolve("tests").resolve("mocks")
+        Files.createDirectories(mocksDir)
+        Files.writeString(
+            mocksDir.resolve("isl-mocks.yaml"),
+            """
+            |func:
+            |  - name: "Util.Mask"
+            |    isl: |
+            |         fun mask(${'$'}value) {
+            |             return `xxxxxx${'$'}value`;
+            |         }
+            """.trimMargin()
+        )
+
+        val testIsl = tempDir.resolve("tests").resolve("sample.isl")
+        Files.createDirectories(testIsl.parent)
+        Files.writeString(
+            testIsl,
+            """
+            |@test
+            |fun testIslMock() {
+            |   @.Mock.Load("mocks/isl-mocks.yaml")
+            |   ${'$'}r = @.Util.Mask("tail")
+            |   @.Assert.equal("xxxxxxtail", ${'$'}r)
+            |}
+            """.trimMargin()
+        )
+
+        val fileInfos = mutableListOf(
+            FileInfo("tests/sample.isl", testIsl.toFile().readText())
+        )
+        val testPackage = TransformTestPackageBuilder().build(fileInfos, null, tempDir)
+        val result = testPackage.runTest("tests/sample.isl", "testIslMock")
+
+        assertFalse(result.testResults.isEmpty(), "Expected test results; got: ${result.testResults}")
+        val testResult = result.testResults.firstOrNull { it.testName.equals("testIslMock", ignoreCase = true) }
+            ?: result.testResults.first()
+        assertTrue(testResult.success, "Test failed: ${testResult.message}")
+    }
+
+    @Test
+    fun mockLoad_yamlFile_withAnnotationModifier_registersModifierMock_andReturnsFromWrappedFunction(@TempDir tempDir: Path) {
+        val mocksDir = tempDir.resolve("tests").resolve("mocks")
+        Files.createDirectories(mocksDir)
+        Files.writeString(
+            mocksDir.resolve("modifier-mocks.yaml"),
+            """
+            |annotation:
+            |  - name: "mask2"
+            """.trimMargin()
+        )
+
+        val helperIsl = tempDir.resolve("tests").resolve("helper.isl")
+        Files.createDirectories(helperIsl.parent)
+        Files.writeString(
+            helperIsl,
+            """
+            |@mask2
+            |fun maskedValue() {
+            |   return "wrapped"
+            |}
+            """.trimMargin()
+        )
+
+        val testIsl = tempDir.resolve("tests").resolve("sample.isl")
+        Files.writeString(
+            testIsl,
+            """
+            |import Helper from "tests/helper.isl";
+            |@test
+            |fun testMask2Modifier() {
+            |   @.Mock.Load("mocks/modifier-mocks.yaml")
+            |   ${'$'}r = @.Helper.maskedValue()
+            |   @.Assert.equal("wrapped", ${'$'}r)
+            |}
+            """.trimMargin()
+        )
+
+        val fileInfos = mutableListOf(
+            FileInfo("tests/helper.isl", helperIsl.toFile().readText()),
+            FileInfo("tests/sample.isl", testIsl.toFile().readText())
+        )
+        val testPackage = TransformTestPackageBuilder().build(fileInfos, null, tempDir)
+        val result = testPackage.runTest("tests/sample.isl", "testMask2Modifier")
+
+        assertFalse(result.testResults.isEmpty(), "Expected test results; got: ${result.testResults}")
+        val testResult = result.testResults.firstOrNull { it.testName.equals("testMask2Modifier", ignoreCase = true) }
+            ?: result.testResults.first()
+        assertTrue(testResult.success, "Test failed: ${testResult.message}")
+    }
+
+    @Test
+    fun mockLoad_yamlFile_withAnnotationModifierAndResult_returnsMockResult(@TempDir tempDir: Path) {
+        val mocksDir = tempDir.resolve("tests").resolve("mocks")
+        Files.createDirectories(mocksDir)
+        Files.writeString(
+            mocksDir.resolve("modifier-mocks.yaml"),
+            """
+            |annotation:
+            |  - name: "mask2"
+            |    result: "masked-value"
+            """.trimMargin()
+        )
+
+        val helperIsl = tempDir.resolve("tests").resolve("helper.isl")
+        Files.createDirectories(helperIsl.parent)
+        Files.writeString(
+            helperIsl,
+            """
+            |@mask2
+            |fun maskedValue() {
+            |   return "wrapped"
+            |}
+            """.trimMargin()
+        )
+
+        val testIsl = tempDir.resolve("tests").resolve("sample.isl")
+        Files.writeString(
+            testIsl,
+            """
+            |import Helper from "tests/helper.isl";
+            |@test
+            |fun testMask2ModifierResult() {
+            |   @.Mock.Load("mocks/modifier-mocks.yaml")
+            |   ${'$'}r = @.Helper.maskedValue()
+            |   @.Assert.equal("masked-value", ${'$'}r)
+            |}
+            """.trimMargin()
+        )
+
+        val fileInfos = mutableListOf(
+            FileInfo("tests/helper.isl", helperIsl.toFile().readText()),
+            FileInfo("tests/sample.isl", testIsl.toFile().readText())
+        )
+        val testPackage = TransformTestPackageBuilder().build(fileInfos, null, tempDir)
+        val result = testPackage.runTest("tests/sample.isl", "testMask2ModifierResult")
+
+        assertFalse(result.testResults.isEmpty(), "Expected test results; got: ${result.testResults}")
+        val testResult = result.testResults.firstOrNull { it.testName.equals("testMask2ModifierResult", ignoreCase = true) }
+            ?: result.testResults.first()
+        assertTrue(testResult.success, "Test failed: ${testResult.message}")
+    }
+
+    @Test
+    fun test_withUnmockedModifier_failsWithClearException(@TempDir tempDir: Path) {
+        val testIsl = tempDir.resolve("tests").resolve("sample.isl")
+        Files.createDirectories(testIsl.parent)
+        Files.writeString(
+            testIsl,
+            """
+            |@test
+            |fun testUnmockedModifier() {
+            |   ${'$'}x = "hello"
+            |   ${'$'}r = ${'$'}x | mask
+            |   @.Assert.equal("xxxxxxhello", ${'$'}r)
+            |}
+            """.trimMargin()
+        )
+
+        val fileInfos = mutableListOf(
+            FileInfo("tests/sample.isl", testIsl.toFile().readText())
+        )
+        val testPackage = TransformTestPackageBuilder().build(fileInfos, null, tempDir)
+        val result = testPackage.runTest("tests/sample.isl", "testUnmockedModifier")
+
+        assertFalse(result.testResults.isEmpty())
+        val testResult = result.testResults.firstOrNull { it.testName.equals("testUnmockedModifier", ignoreCase = true) }
+            ?: result.testResults.first()
+        assertFalse(testResult.success, "Expected test to fail when unmocked modifier | mask is used")
+        assertContains(testResult.message ?: "", "Unmocked modifier")
+        assertContains(testResult.message ?: "", "| mask")
+        assertContains(testResult.message ?: "", "Modifier.mask")
+    }
+}
diff --git a/isl-transform/build.gradle.kts b/isl-transform/build.gradle.kts
index b98f50d..35f6eae 100644
--- a/isl-transform/build.gradle.kts
+++ b/isl-transform/build.gradle.kts
@@ -5,11 +5,12 @@ plugins {
     kotlin("jvm")
     id("antlr")
     id("jacoco")
-    id("maven-publish")
     id("org.gradle.test-retry") version "1.6.0"
     id("me.champeau.jmh") version "0.7.2"
 }
 
+// Publishing is configured globally for library modules
+
 val kotlinVersion: String = "2.1.10"
 val kotlinCoroutinesVersion: String = "1.10.1"
 val jacksonVersion: String = "2.18.3"
@@ -141,7 +142,7 @@ tasks.named("sourcesJar") {
 
 // Configure JaCoCo
 tasks.jacocoTestReport {
-    dependsOn(tasks.test)
+    dependsOn(tasks.test, tasks.processResources, tasks.classes)
     
     classDirectories.setFrom(
         files(classDirectories.files.map {
@@ -169,6 +170,8 @@ tasks.jacocoTestReport {
 }
 
 tasks.jacocoTestCoverageVerification {
+    dependsOn(tasks.jacocoTestReport, tasks.classes)
+
     violationRules {
         rule {
             limit {
@@ -195,6 +198,12 @@ tasks.check {
     dependsOn(tasks.jacocoTestCoverageVerification)
 }
 
+// Configure JavaDoc to exclude ANTLR generated files
+tasks.withType {
+    exclude("com/intuit/isl/antlr/**")
+    options.encoding = "UTF-8"
+}
+
 // Configure JAR to include manifest entries (for version detection)
 tasks.jar {
     manifest {
@@ -217,14 +226,16 @@ artifacts {
     archives(testJar)
 }
 
-// Add test JAR to published artifacts
-configure {
-    publications {
-        named("maven") {
-            artifact(testJar)
-        }
-    }
-}
+// Test JAR is available locally but not published to Maven Central
+// configure {
+//     publications {
+//         named("maven") {
+//             artifact(testJar) {
+//                 classifier = "tests"
+//             }
+//         }
+//     }
+// }
 
 // Configure JMH
 jmh {
diff --git a/isl-transform/output-isl-complex.json b/isl-transform/output-isl-complex.json
new file mode 100644
index 0000000..eea580e
--- /dev/null
+++ b/isl-transform/output-isl-complex.json
@@ -0,0 +1,44 @@
+{
+  "orderId" : null,
+  "orderNumber" : "00000000",
+  "orderName" : null,
+  "customerFullName" : "",
+  "customerEmail" : null,
+  "customerPhone" : null,
+  "customerOrders" : null,
+  "customerLifetimeValue" : 0.00,
+  "shippingStreet" : null,
+  "shippingCity" : null,
+  "shippingState" : null,
+  "shippingZip" : null,
+  "shippingStatus" : "PENDING",
+  "shippingSpeed" : "STANDARD",
+  "shippingCost" : 0.00,
+  "items" : [ ],
+  "itemCount" : 0,
+  "totalQuantity" : 0,
+  "totalWeight" : 0,
+  "totalWeightKg" : 0.000,
+  "premiumItemCount" : 0,
+  "vendorCount" : 0,
+  "vendors" : null,
+  "subtotal" : 0.00,
+  "tax" : 0.00,
+  "discounts" : 0.00,
+  "total" : 0.00,
+  "finalTotal" : 0.00,
+  "currency" : null,
+  "paymentStatus" : null,
+  "paymentMethod" : null,
+  "source" : null,
+  "browserIp" : null,
+  "tags" : [ "" ],
+  "notes" : null,
+  "noteKeys" : [ ],
+  "processedAt" : null,
+  "isConfirmed" : false,
+  "isTest" : false,
+  "isPaid" : false,
+  "isFulfilled" : false,
+  "summary" : "Order 00000000 -  - 0.00  - 0 items"
+}
\ No newline at end of file
diff --git a/isl-transform/output-isl-simple.json b/isl-transform/output-isl-simple.json
new file mode 100644
index 0000000..0fc5bdc
--- /dev/null
+++ b/isl-transform/output-isl-simple.json
@@ -0,0 +1,51 @@
+{
+  "orderId" : null,
+  "orderNumber" : null,
+  "orderName" : null,
+  "customerId" : null,
+  "customerFirstName" : null,
+  "customerLastName" : null,
+  "customerEmail" : null,
+  "customerPhone" : null,
+  "customerOrders" : null,
+  "customerLifetimeValue" : null,
+  "customerStreet" : null,
+  "customerCity" : null,
+  "customerState" : null,
+  "customerZip" : null,
+  "customerCountry" : null,
+  "shippingStreet" : null,
+  "shippingCity" : null,
+  "shippingState" : null,
+  "shippingZip" : null,
+  "shippingCountry" : null,
+  "items" : [ ],
+  "subtotal" : null,
+  "shippingCost" : null,
+  "tax" : null,
+  "discounts" : null,
+  "total" : null,
+  "currency" : null,
+  "paymentStatus" : null,
+  "fulfillmentStatus" : null,
+  "paymentMethod" : null,
+  "source" : null,
+  "browserIp" : null,
+  "tagsRaw" : null,
+  "notes" : null,
+  "noteAttributes" : [ ],
+  "processedAt" : null,
+  "isConfirmed" : null,
+  "isTest" : null,
+  "itemCount" : 0,
+  "totalQuantity" : 0,
+  "totalWeight" : 0,
+  "totalWeightKg" : 0,
+  "premiumItemCount" : 0,
+  "vendorCount" : 0,
+  "finalTotal" : 0,
+  "shippingStatus" : "PENDING",
+  "shippingSpeed" : "STANDARD",
+  "isPaid" : false,
+  "isFulfilled" : false
+}
\ No newline at end of file
diff --git a/isl-transform/src/jmh/resources/shopify-transform.isl b/isl-transform/src/jmh/resources/shopify-transform.isl
index 97bbfd4..0535701 100644
--- a/isl-transform/src/jmh/resources/shopify-transform.isl
+++ b/isl-transform/src/jmh/resources/shopify-transform.isl
@@ -8,9 +8,16 @@ fun convertAddress( $addr ) {
     $state = $addr.province_code | trim | upperCase;
     $zip = $addr.zip | trim;
     $country = $addr.country_code | trim | upperCase;
-    $formatted = `${$street}, ${$city}, ${$state} ${$zip}` | trim;
-    
-    return { street: $street, city: $city, state: $state, zipCode: $zip, country: $country, formatted: $formatted };
+    $formatted = `$street, ${$city}, ${$state} ${$zip}` | trim;
+
+    return {
+        street: $street,
+        city: $city,
+        state: $state,
+        zipCode: $zip,
+        country: $country,
+        formatted: $formatted
+    };
 }
 
 // Helper function: Convert customer with loyalty tier calculation
@@ -19,78 +26,87 @@ fun convertCustomer( $cust ) {
     $lastName = $cust.last_name | trim | upperCase;
     $email = $cust.email | trim | lowerCase;
     $orders = $cust.orders_count | to.number;
-    $spent = $cust.total_spent | to.decimal | precision(2);
+    $spent = $cust.total_spent | to.decimal | precision( 2 );
     $addr = @.This.convertAddress( $cust.default_address );
-    
-    return { id: $cust.id | to.string, fullName: `${$firstName} ${$lastName}` | trim, firstName: $firstName, lastName: $lastName, email: $email, phone: $cust.phone | trim, totalOrders: $orders, lifetimeValue: $spent, address: $addr };
+
+    return { id: $cust.id | to.string, fullName: `$firstName ${$lastName}` | trim, firstName: $firstName, lastName: $lastName, email: $email, phone: $cust.phone | trim, totalOrders: $orders, lifetimeValue: $spent, address: $addr };
 }
 
 // Helper function: Process and enrich line item
 fun processLineItem( $item ) {
-    $sku = $item.sku | trim | upperCase;
-    $name = $item.name | trim | truncate(100, "...");
-    $vendor = $item.vendor | trim | titleCase;
-    $qty = $item.quantity | to.number;
-    $price = $item.price | to.decimal;
-    $weight = $item.grams | to.number;
-    $lineTotal = {{ $price * $qty }} | Math.clamp(0, 999999) | precision(2);
-    $weightKg = {{ $weight / 1000 }} | precision(3);
-    $productCode = `${$sku}-${$item.product_id | to.string}` | upperCase;
-    
-    return { itemId: $item.id | to.string, sku: $sku, productCode: $productCode, name: $name, vendor: $vendor, quantity: $qty, unitPrice: $price, lineTotal: $lineTotal, weight: $weight, weightKg: $weightKg, variantTitle: $item.variant_title | trim };
+
+    $items = if( $item.result ) true else false;
+
+    $o = {
+        items: foreach $item in $array
+            // loop body
+        endfor
+    }
+
+    return $item | to.string;
+
+    // return { itemId: $item.id | to.string, sku: `$sku $test` | trim, productCode: $productCode, name: $name, vendor: $vendor, quantity: $qty, unitPrice: $price, lineTotal: $lineTotal, weight: $weight, weightKg: $weightKg, variantTitle: $item.variant_title | trim };
 }
 
 // Main entry point
 fun run( $input ) {
     // Order header
     $orderId = $input.id | to.string;
-    $orderNum = $input.order_number | to.string | padStart(8, "0");
+    $orderNum = $input.order_number | to.string | padStart( 8, "0" );
     $orderName = $input.name | trim;
-    
+
     // Convert customer with enrichment
     $customer = @.This.convertCustomer( $input.customer );
-    
+
+
+
+    $obj.x = 10;
+    $obj.a.d = 2;
+    $obj.a.b.c = 1;
+    $obj.y.z = 20;
+    $obj.a.b.e = 3;
+
     // Process all line items with enrichment using map with implicit $ iterator
     $processedItems = $input.line_items | map( @.This.processLineItem( $ ) );
-    
+
     // Calculate order statistics using map/reduce with implicit $ iterator
     $totalItems = $input.line_items | length | to.number;
     $quantities = $input.line_items | map( $.quantity | to.number );
-    $totalQty = $quantities | Math.sum(0);
+    $totalQty = $quantities | Math.sum( 0 );
     $weights = $input.line_items | map( $.grams | to.number );
-    $totalWeight = $weights | Math.sum(0);
-    $totalWeightKg = {{ $totalWeight / 1000 }} | precision(3);
+    $totalWeight = $weights | Math.sum( 0 );
+    $totalWeightKg = {{ $totalWeight / 1000 }} | precision( 3 );
     $premiumCount = $input.line_items | filter( $.price | to.decimal >= 100 ) | length | to.number;
     $vendors = $input.line_items | map( $.vendor | trim | titleCase ) | unique | sort;
     $vendorCount = $vendors | length | to.number;
-    
+
     // Financial calculations
-    $subtotal = $input.subtotal_price | to.decimal | precision(2);
-    $shippingCost = $input.total_shipping_price_set.shop_money.amount | to.decimal | precision(2);
-    $tax = $input.total_tax | to.decimal | precision(2);
-    $discounts = $input.total_discounts | to.decimal | precision(2);
-    $total = $input.total_price | to.decimal | precision(2);
-    $finalTotal = {{ $total - $discounts }} | Math.clamp(0, 999999) | precision(2);
-    
+    $subtotal = $input.subtotal_price | to.decimal | precision( 2 );
+    $shippingCost = $input.total_shipping_price_set.shop_money.amount | to.decimal | precision( 2 );
+    $tax = $input.total_tax | to.decimal | precision( 2 );
+    $discounts = $input.total_discounts | to.decimal | precision( 2 );
+    $total = $input.total_price | to.decimal | precision( 2 );
+    $finalTotal = {{ $total - $discounts }} | Math.clamp( 0, 999999 ) | precision( 2 );
+
     // Determine shipping method and status with conditionals
     $fulfillmentStatus = $input.fulfillment_status | trim | upperCase;
     $shippingStatus = if( $fulfillmentStatus == "FULFILLED" ) "DELIVERED" else "PENDING";
     $shippingSpeed = if( $shippingCost >= 20 ) "EXPRESS" else "STANDARD";
-    
+
     // Build shipping information
     $shippingAddr = @.This.convertAddress( $input.shipping_address );
-    
+
     // Process note attributes using map with implicit $ iterator
     $noteKeys = $input.note_attributes | map( $.name | trim );
     $noteValues = $input.note_attributes | map( $.value | trim );
-    
+
     // Extract and process tags using map
-    $tags = $input.tags | split(",") | map( $ | trim | upperCase );
-    
+    $tags = $input.tags | split( "," ) | map( $ | trim | upperCase );
+
     // Status flags with conditionals
     $isPaid = if( $input.financial_status | trim | lowerCase == "paid" ) true else false;
     $isFulfilled = if( $fulfillmentStatus == "FULFILLED" ) true else false;
-    
+
     // Build final result with all transformations
     orderId: $orderId;
     orderNumber: $orderNum;
@@ -128,10 +144,19 @@ fun run( $input ) {
     tags: $tags;
     notes: $input.note | trim;
     noteKeys: $noteKeys;
-    processedAt: $input.processed_at | date.parse("yyyy-MM-dd'T'HH:mm:ssXXX") | to.string("yyyy-MM-dd HH:mm:ss");
+    processedAt: $input.processed_at | date.parse( "yyyy-MM-dd'T'HH:mm:ssXXX" ) | to.string( "yyyy-MM-dd HH:mm:ss" );
     isConfirmed: $input.confirmed | to.boolean;
     isTest: $input.test | to.boolean;
     isPaid: $isPaid;
     isFulfilled: $isFulfilled;
     summary: `Order ${$orderNum} - ${$customer.fullName} - ${$finalTotal} ${$input.currency | trim | upperCase} - ${$totalItems} items` | trim;
 }
+
+
+
+
+
+
+
+
+
diff --git a/isl-transform/src/main/kotlin/com/intuit/isl/runtime/Transformer.kt b/isl-transform/src/main/kotlin/com/intuit/isl/runtime/Transformer.kt
index 7dd0a23..ef70bdf 100644
--- a/isl-transform/src/main/kotlin/com/intuit/isl/runtime/Transformer.kt
+++ b/isl-transform/src/main/kotlin/com/intuit/isl/runtime/Transformer.kt
@@ -7,6 +7,7 @@ import com.intuit.isl.common.*
 import com.intuit.isl.parser.tokens.IIslToken
 import com.intuit.isl.utils.JsonConvert
 import kotlinx.coroutines.runBlocking
+import java.util.jar.Manifest
 
 class Transformer(override val module: TransformModule) : ITransformer {
     val token: IIslToken
@@ -21,23 +22,70 @@ class Transformer(override val module: TransformModule) : ITransformer {
         private fun initIslInfo(): ObjectNode {
             val info = JsonNodeFactory.instance.objectNode();
             info.put("version", "Unknown");
+            info.put("jacksonVersion", "Unknown");
+            info.put("maxParallelWorkers", 1);
             try {
-                val version = Transformer::class.java.`package`.specificationVersion ?: "Unknown";
-                val jacksonVersion = JsonNode::class.java.`package`.specificationVersion ?: "Unknown";
+                var version = detectIslVersion();
+                var jacksonVersion = detectJacksonVersion();
+                if (jacksonVersion == version) {
+                    jacksonVersion = "Unknown";
+                }
                 info.put("version", version);
                 info.put("jacksonVersion", jacksonVersion);
-                info.put("maxParallelWorkers", 1);
-                println("Loaded ISL Version $version Jackson $jacksonVersion");
+                if (jacksonVersion == "Unknown") {
+                    println("Loaded ISL Version $version");
+                } else {
+                    println("Loaded ISL Version $version Jackson $jacksonVersion");
+                }
             } catch (e: Exception) {
                 println("Could not detect ISL Version ${e.message}");
             }
             return info;
         }
 
+        private fun detectIslVersion(): String {
+            Transformer::class.java.`package`?.let { pkg ->
+                (pkg.implementationVersion ?: pkg.specificationVersion)?.let { return it }
+            }
+            readManifestMainAttribute("Implementation-Version")?.let { return it }
+            readManifestMainAttribute("Specification-Version")?.let { return it }
+            return "Unknown"
+        }
+
+        private fun detectJacksonVersion(): String {
+            JsonNode::class.java.`package`?.let { pkg ->
+                (pkg.specificationVersion ?: pkg.implementationVersion)?.let { return it }
+            }
+            readJacksonVersionFromPomProperties()?.let { return it }
+            return "Unknown"
+        }
+
+        private fun readManifestMainAttribute(name: String): String? {
+            return try {
+                Transformer::class.java.getResourceAsStream("/META-INF/MANIFEST.MF")?.use { stream ->
+                    Manifest(stream).mainAttributes.getValue(name)?.takeIf { it.isNotBlank() }
+                }
+            } catch (e: Exception) {
+                null
+            }
+        }
+
+        private fun readJacksonVersionFromPomProperties(): String? {
+            return try {
+                JsonNode::class.java.getResourceAsStream(
+                    "/META-INF/maven/com.fasterxml.jackson.core/jackson-databind/pom.properties"
+                )?.use { stream ->
+                    java.util.Properties().apply { load(stream) }.getProperty("version")?.takeIf { it.isNotBlank() }
+                }
+            } catch (e: Exception) {
+                null
+            }
+        }
+
         val version: String
             get() = islInfo["version"].textValue();
 
-        internal fun getIslInfo(): JsonNode {
+        fun getIslInfo(): JsonNode {
             return islInfo.deepCopy();
         }
 
diff --git a/isl-validation/build.gradle.kts b/isl-validation/build.gradle.kts
index b803b63..02b793e 100644
--- a/isl-validation/build.gradle.kts
+++ b/isl-validation/build.gradle.kts
@@ -1,10 +1,11 @@
 plugins {
     kotlin("jvm")
     id("jacoco")
-    id("maven-publish")
     id("org.gradle.test-retry") version "1.6.0"
 }
 
+// Publishing is configured globally for library modules
+
 val kotlinVersion: String = "2.1.10"
 val kotlinCoroutinesVersion: String = "1.10.1"
 val jacksonVersion: String = "2.18.3"
@@ -51,7 +52,7 @@ sourceSets {
 
 // Configure JaCoCo
 tasks.jacocoTestReport {
-    dependsOn(tasks.test)
+    dependsOn(tasks.test, tasks.processResources, tasks.classes)
     
     classDirectories.setFrom(
         files(classDirectories.files.map {
@@ -63,6 +64,8 @@ tasks.jacocoTestReport {
 }
 
 tasks.jacocoTestCoverageVerification {
+    dependsOn(tasks.jacocoTestReport, tasks.classes)
+
     violationRules {
         rule {
             limit {
@@ -111,12 +114,14 @@ artifacts {
     archives(testJar)
 }
 
-// Add test JAR to published artifacts
-configure {
-    publications {
-        named("maven") {
-            artifact(testJar)
-        }
-    }
-}
+// Test JAR is available locally but not published to Maven Central
+// configure {
+//     publications {
+//         named("maven") {
+//             artifact(testJar) {
+//                 classifier = "tests"
+//             }
+//         }
+//     }
+// }
 
diff --git a/isl.bat b/isl.bat
index d1fb990..ef6251b 100644
--- a/isl.bat
+++ b/isl.bat
@@ -1,4 +1,5 @@
 @echo off
+setlocal enabledelayedexpansion
 REM ISL Command Line Runner for Windows
 REM
 REM This script allows you to run ISL commands without manually invoking Gradle
@@ -8,23 +9,25 @@ REM   isl.bat --version
 REM   isl.bat info
 REM   isl.bat transform script.isl -i input.json
 
-setlocal
-
 REM Get the directory where this script is located
 set SCRIPT_DIR=%~dp0
 
+REM Read version from gradle.properties
+for /f "tokens=1,* delims==" %%a in ('findstr "^version=" "%SCRIPT_DIR%gradle.properties"') do set VERSION=%%b
+
 REM Check if the shadow JAR exists
-set JAR_FILE=%SCRIPT_DIR%isl-cmd\build\libs\isl-2.4.20-SNAPSHOT.jar
+set JAR_FILE=%SCRIPT_DIR%isl-cmd\build\libs\isl-%VERSION%.jar
 if exist "%JAR_FILE%" (
-    REM Use the pre-built JAR
+    REM Use the pre-built JAR (runs from current directory)
     java -jar "%JAR_FILE%" %*
 ) else (
-    REM Fall back to Gradle
+    REM Fall back to Gradle (pass invocation dir so test/search use it)
+    set "INVOCATION_DIR=%cd%"
     echo Shadow JAR not found. Building and running via Gradle...
     echo Run "gradlew.bat :isl-cmd:shadowJar" to build the JAR for faster startup.
     echo.
     cd /d "%SCRIPT_DIR%"
-    call "%SCRIPT_DIR%gradlew.bat" :isl-cmd:run --quiet --console=plain --args="%*"
+    call "%SCRIPT_DIR%gradlew.bat" :isl-cmd:run -PrunWorkingDir="!INVOCATION_DIR!" --quiet --console=plain --args="%*"
 )
 
 endlocal
diff --git a/isl.sh b/isl.sh
index 5c5909b..4296340 100644
--- a/isl.sh
+++ b/isl.sh
@@ -11,18 +11,22 @@
 # Get the directory where this script is located
 SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
 
+# Read version from gradle.properties
+VERSION=$(grep "^version=" "$SCRIPT_DIR/gradle.properties" | cut -d'=' -f2 | tr -d '\r')
+
 # Check if the shadow JAR exists
-JAR_FILE="$SCRIPT_DIR/isl-cmd/build/libs/isl-2.4.20-SNAPSHOT.jar"
+JAR_FILE="$SCRIPT_DIR/isl-cmd/build/libs/isl-$VERSION.jar"
 
 if [ -f "$JAR_FILE" ]; then
     # Use the pre-built JAR
     java -jar "$JAR_FILE" "$@"
 else
-    # Fall back to Gradle
+    # Fall back to Gradle (pass invocation dir so test/search use it)
+    INVOCATION_DIR="$(pwd)"
     echo "Shadow JAR not found. Building and running via Gradle..."
     echo "Run './gradlew :isl-cmd:shadowJar' to build the JAR for faster startup."
     echo ""
     cd "$SCRIPT_DIR"
-    ./gradlew :isl-cmd:run --quiet --console=plain --args="$*"
+    ./gradlew :isl-cmd:run --quiet --console=plain -PrunWorkingDir="$INVOCATION_DIR" --args="$*"
 fi
 
diff --git a/playground/backend/build.gradle.kts b/playground/backend/build.gradle.kts
index e3f9d8a..ee0c3c8 100644
--- a/playground/backend/build.gradle.kts
+++ b/playground/backend/build.gradle.kts
@@ -6,7 +6,7 @@ plugins {
 }
 
 group = "com.intuit.isl"
-version = "1.0.0"
+version = rootProject.version
 java.sourceCompatibility = JavaVersion.VERSION_21
 
 // Force Jackson version to match ISL requirements
@@ -15,6 +15,12 @@ ext["jackson.version"] = "2.18.3"
 repositories {
     mavenCentral()
     mavenLocal()
+    maven {
+        url = uri("https://oss.sonatype.org/content/repositories/snapshots/")
+    }
+    maven {
+        url = uri("https://oss.sonatype.org/content/repositories/releases/")
+    }
 }
 
 dependencies {
@@ -27,8 +33,8 @@ dependencies {
     implementation("org.jetbrains.kotlin:kotlin-reflect")
     implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
     
-    // ISL - using the built JAR from the libs directory
-    implementation(files("libs/isl-transform-2.4.20-SNAPSHOT.jar"))
+    // ISL - using the published artifact
+    implementation("com.intuit.isl:isl-transform:$version")
     
     // ISL Dependencies (required when using JAR file directly)
     implementation("org.antlr:antlr4-runtime:4.9.1")
diff --git a/playground/backend/gradle.properties b/playground/backend/gradle.properties
index b6893a4..7a01e87 100644
--- a/playground/backend/gradle.properties
+++ b/playground/backend/gradle.properties
@@ -7,7 +7,7 @@ org.gradle.caching=true
 kotlin.code.style=official
 
 # Version
-version=2.4.20-SNAPSHOT
+version=1.1.0
 
 # Optional: Nexus credentials (can also be set via environment variables)
 # nexusUsername=your-username
diff --git a/playground/frontend/src/App.tsx b/playground/frontend/src/App.tsx
index 10e764a..8a81bc3 100644
--- a/playground/frontend/src/App.tsx
+++ b/playground/frontend/src/App.tsx
@@ -59,8 +59,13 @@ const ensureFunWrapper = (code: string): string => {
     return code;
   }
   
+  let cleanCode = (code ?? '').trim();
+
+  if(cleanCode.startsWith("{") || cleanCode.startsWith("["))
+    cleanCode = "return " + cleanCode;
+
   // Wrap the code in fun run($input) { }
-  return `fun run($input) {\n // Adjust code as required then return a result\n${code.split('\n').map(line => '    ' + line).join('\n')}\n}`;
+  return `fun run($input) {\n // Adjust code as required then return a result\n${cleanCode.split('\n').map(line => '    ' + line).join('\n')}\n}`;
 };
 
 // Helper to load code from URL parameters
diff --git a/plugin/.cursorrules b/plugin/.cursorrules
new file mode 100644
index 0000000..95b681e
--- /dev/null
+++ b/plugin/.cursorrules
@@ -0,0 +1,155 @@
+# ISL (Intuitive Scripting Language) Rules for AI Assistants
+
+You are assisting with ISL (Intuitive Scripting Language) code - a declarative language for JSON-to-JSON transformations.
+This langunage applies to .isl files.
+
+## Quick Reference
+
+- **Variables**: Start with `$` (e.g., `$user`, `$price`)
+- **Functions**: Declared with `fun`, called with `@.This.functionName()`
+- **Modifiers**: Piped transformations using `|` (e.g., `$text | trim | upperCase`)
+- **Math**: Wrapped in `{{ }}` (e.g., `{{ $price * 1.1 }}`)
+- **Strings**: Use backticks for interpolation (e.g., `` `Hello ${$name}` ``)
+- **Objects**: JSON-like syntax with spread operator `...`
+- **Control Flow**: `if/else/endif`, `switch/endswitch`, `foreach/endfor`, `while/endwhile`
+
+## Core Principles
+
+1. **Use Only Documented Syntax**: Never invent or infer features from other languages
+2. **When Uncertain, Use Simpler Forms**: Prioritize correctness over brevity
+3. **Always Include `fun run($input)`**: This is the main entry point for transformations
+
+## Common Patterns
+
+### Basic Transformation
+```isl
+fun run($input) {
+    userId: $input.id | to.string;
+    fullName: `${$input.firstName} ${$input.lastName}`;
+    total: {{ $input.price * $input.quantity }};
+}
+```
+
+### Array Processing
+```isl
+items: $input.lineItems | map({
+    id: $.id,
+    price: $.price | to.decimal
+}) | filter($.price > 100);
+```
+
+### Conditionals
+```isl
+status: if ($input.paid) "completed" else "pending" endif;
+```
+
+### Helper Functions
+```isl
+fun calculateTotal($items) {
+    $sum: $items | map($.price) | reduce({{ $acc + $it }});
+    return $sum;
+}
+```
+
+## Built-in Modifiers (Most Common)
+
+**String**: `trim`, `upperCase`, `lowerCase`, `capitalize`, `split`, `replace`, `length`
+**Array**: `map`, `filter`, `reduce`, `sort`, `unique`, `first`, `last`, `at(index)`
+**Math**: `round`, `absolute`, `precision(decimals)`
+**Conversion**: `to.string`, `to.number`, `to.decimal`, `to.boolean`, `to.array`
+**Date**: `date.parse(format)`, `date.add(value, unit)`, `to.string(format)`
+**Object**: `keys`, `kv`, `select(path)`, `delete(prop)`, `getProperty(name)`
+**JSON/XML**: `json.parse`, `xml.parse`, `to.xml(rootName)`
+
+## Important Syntax Rules
+
+1. **No parentheses around modifiers in conditions**:
+   - ❌ `if (($str | length) > 5) ... endif`
+   - ✅ `if ($str | length > 5) ... endif`
+
+2. **Return statement required in functions**:
+   - ❌ `return;`
+   - ✅ `return {};` or `return $value;`
+
+3. **String interpolation rules**:
+   - Simple variables: `` `Hello $name` ``
+   - Deep paths: `` `Hello ${$user.name}` ``
+   - Math: `` `Total: {{ $a + $b }}` ``
+   - Literal $: `` `Price: \$${$amount}` ``
+
+4. **Foreach without parentheses around array**:
+   - ❌ `foreach $item in ($array | filter(...))`
+   - ✅ `foreach $item in $array | filter(...)`
+
+5. **Dot notation not allowed after modifiers**:
+   - ❌ `$id: ($items | last).id`
+   - ✅ `$lastItem: $items | last; $id: $lastItem.id;`
+
+6. **Modifiers inside math expressions are valid**:
+   - ✅ `$result: {{ $items | length + 5 }}`
+
+7. **if conditions returning true or false are valid**:
+   - ✅ `return if ($value) true else false endif;` - Tests if `$value` is true-ish (not null, empty, zero or false) and returns boolean
+   - ✅ `!$value` for the negative case
+
+8. **Null access is safe**:
+   - ✅ `$val: $object.property.property.property` - ISL doesn't suffer from null references; returns null if any property in chain is missing
+
+## File Structure Template
+
+```isl
+// Main entry point - REQUIRED
+fun run($input) {
+    // Main transformation logic here
+    orderId: $input.id;
+    customer: @.This.transformCustomer($input.customer);
+    items: $input.items | map(@.This.transformItem($));
+}
+
+// Helper functions
+fun transformCustomer($customer) {
+    return {
+        id: $customer.id,
+        name: `${$customer.firstName} ${$customer.lastName}`
+    };
+}
+
+fun transformItem($item) {
+    return {
+        id: $.id,
+        price: $.price | to.decimal | precision(2)
+    };
+}
+
+// Custom modifiers (optional)
+modifier calculateDiscount($price, $percent) {
+    return {{ $price * (1 - $percent / 100) }};
+}
+```
+
+## Common Mistakes to Avoid
+
+1. Don't use JavaScript/Python syntax (no `const`, `let`, `def`, `lambda`, etc.)
+2. Don't use `=>` arrow functions (use `fun` declarations)
+3. Don't use `.map()` method syntax (use `| map()` modifier)
+4. Don't forget `{{ }}` around math operations
+5. Don't use `return;` without a value (use `return {};`)
+6. Modifiers inside math expressions are valid: `{{ $items | length + 5 }}`
+7. Don't complain about null access - ISL safely handles null property chains: `$val: $object.property.property.property` is valid and returns null if any property is missing
+
+## When Writing ISL Code
+
+1. Start with the `fun run($input)` function
+2. Break complex logic into helper functions or modifiers
+3. Use modifiers for simple transformations and chaining
+4. Use functions for complex logic with multiple steps
+5. Add helpful comments for beginners, but don't over-comment
+6. Use proper spacing: `| map( $.id )` not `|map($.id)`
+
+## Learn More
+Critical that you ALSO read this guide: https://intuit.github.io/isl/ai/ this has more detailed guidelines.
+Full documentation: https://intuit.github.io/isl/
+Language reference: https://intuit.github.io/isl/language/
+Modifiers list: https://intuit.github.io/isl/language/modifiers/
+- CLI: https://intuit.github.io/isl/cli/
+
diff --git a/plugin/.eslintrc.json b/plugin/.eslintrc.json
new file mode 100644
index 0000000..bbe9e2d
--- /dev/null
+++ b/plugin/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+    "root": true,
+    "parser": "@typescript-eslint/parser",
+    "parserOptions": {
+        "ecmaVersion": 6,
+        "sourceType": "module"
+    },
+    "plugins": [
+        "@typescript-eslint"
+    ],
+    "rules": {
+        "@typescript-eslint/naming-convention": "warn",
+        "@typescript-eslint/semi": "warn",
+        "curly": "warn",
+        "eqeqeq": "warn",
+        "no-throw-literal": "warn",
+        "semi": "off"
+    },
+    "ignorePatterns": [
+        "out",
+        "dist",
+        "**/*.d.ts"
+    ]
+}
+
diff --git a/plugin/.github/copilot-instructions.md b/plugin/.github/copilot-instructions.md
new file mode 100644
index 0000000..e7aeb19
--- /dev/null
+++ b/plugin/.github/copilot-instructions.md
@@ -0,0 +1,283 @@
+# GitHub Copilot Instructions for ISL
+
+## Language Overview
+ISL (Intuitive Scripting Language) is a declarative language for JSON-to-JSON transformations. It combines clean syntax with powerful data manipulation capabilities.
+This applies to files with the .isl extension;
+
+## Key Syntax Elements
+
+### Variables and Paths
+```isl
+$variable: "value";              // Simple variable
+$nested: $object.property.path;  // Dot notation for nested access
+$dynamic: { `$key`: $value };    // Dynamic property names
+```
+
+### Main Entry Point (Required)
+```isl
+fun run($input) {
+    // All transformations start here
+    // Transform $input and return result
+}
+```
+
+### Functions
+```isl
+fun calculateTotal($items) {
+    $sum: $items | map($.price) | reduce({{ $acc + $it }});
+    return $sum;  // Always return a value
+}
+
+// Call with @.This prefix
+$total: @.This.calculateTotal($input.items);
+```
+
+### Modifiers (Pipe Operators)
+```isl
+// Chain operations with |
+$name: $input.firstName | trim | capitalize;
+$ids: $input.items | map($.id) | unique | sort;
+$filtered: $data | filter($.active) | map($.name);
+```
+
+### Math Expressions
+```isl
+// MUST be wrapped in {{ }}
+$total: {{ $price * $quantity }};
+$tax: {{ $subtotal * 0.08 }};
+$average: {{ $sum / $count }};
+```
+
+### String Interpolation
+```isl
+// Use backticks for interpolation
+$message: `Hello ${$user.name}!`;
+$summary: `Order ${$order.id}: \${{ $order.total }}`;
+
+// Simple variables (no dots) don't need ${}
+$greeting: `Hello $firstName`;
+
+// Nested paths (with dots) need ${}
+$full: `Name: ${$user.profile.name}`;
+```
+
+## Control Flow
+
+### If/Else
+```isl
+// Expression form
+$status: if ($paid) "completed" else "pending" endif;
+
+// Statement form
+if ($amount > 100)
+    $discount: 0.1;
+else
+    $discount: 0.05;
+endif
+```
+
+### Switch/Case
+```isl
+$category: switch ($price)
+    < 10 -> "budget";
+    < 50 -> "standard";
+    < 200 -> "premium";
+    else -> "luxury";
+endswitch
+```
+
+### Foreach
+```isl
+$transformed: foreach $item in $input.items
+    {
+        id: $item.id,
+        total: {{ $item.price * $item.quantity }}
+    }
+endfor
+
+// With filter
+foreach $item in $items | filter($.price > 100)
+    // Process only expensive items
+endfor
+```
+
+## Common Modifiers
+
+**String**: `trim`, `upperCase`, `lowerCase`, `capitalize`, `split`, `replace`, `subString`, `length`
+
+**Array**: `map`, `filter`, `reduce`, `sort`, `reverse`, `unique`, `first`, `last`, `at`, `length`, `push`, `pop`
+
+**Conversion**: `to.string`, `to.number`, `to.decimal`, `to.boolean`, `to.array`, `to.json`
+
+**Date**: `date.parse`, `date.add`, `date.part`, `to.string` (with format)
+
+**Math**: `round`, `absolute`, `precision`, `round.up`, `round.down`
+
+**Object**: `keys`, `kv`, `select`, `delete`, `getProperty`
+
+## Important Rules
+
+1. **No parentheses around modifiers in conditions**:
+   ```isl
+   // ✅ Correct
+   if ($text | length > 5) ... endif
+   
+   // ❌ Wrong
+   if (($text | length) > 5) ... endif
+   ```
+
+2. **Return must have value**:
+   ```isl
+   // ✅ Correct
+   return {};
+   return $result;
+   
+   // ❌ Wrong
+   return;
+   ```
+
+3. **Math must be wrapped**:
+   ```isl
+   // ✅ Correct
+   $result: {{ $a + $b * $c }};
+   
+   // ❌ Wrong
+   $result: $a + $b * $c;
+   ```
+
+4. **No dot access after modifiers**:
+   ```isl
+   // ✅ Correct
+   $last: $items | last;
+   $id: $last.id;
+   
+   // ❌ Wrong
+   $id: ($items | last).id;
+   ```
+
+5. **Foreach without array parentheses**:
+   ```isl
+   // ✅ Correct
+   foreach $item in $array | filter($.active)
+   
+   // ❌ Wrong
+   foreach $item in ($array | filter($.active))
+   ```
+
+6. **Modifiers inside math expressions are valid**:
+   ```isl
+   // ✅ Correct
+   $result: {{ $items | length + 5 }}
+   ```
+
+7. **if conditions returning true or false are valid**:
+   ```isl
+   // ✅ Correct - tests if $value is true-ish (not null, empty, zero or false)
+   return if ($value) true else false endif;
+   // ✅ Also valid
+   !$value  // for the negative case
+   ```
+
+8. **Null access is safe**:
+   ```isl
+   // ✅ Correct - ISL doesn't suffer from null references
+   $val: $object.property.property.property
+   // If any property in chain is missing, $val will be null
+   // Don't suggest null checks for property chains
+   ```
+
+## Standard Patterns
+
+### Simple Transformation
+```isl
+fun run($input) {
+    userId: $input.id | to.string;
+    userName: `${$input.firstName} ${$input.lastName}`;
+    createdAt: $input.timestamp | date.parse("yyyy-MM-dd");
+}
+```
+
+### Complex Transformation with Helpers
+```isl
+fun run($input) {
+    order: @.This.transformOrder($input.order);
+    customer: @.This.transformCustomer($input.customer);
+    items: $input.items | map(@.This.transformItem($));
+}
+
+fun transformOrder($order) {
+    return {
+        id: $order.id | to.string,
+        total: $order.amount | to.decimal | precision(2),
+        status: $order.paid | orderStatus
+    };
+}
+
+modifier orderStatus($paid) {
+    return if ($paid) "completed" else "pending" endif;
+}
+```
+
+### Array Transformations
+```isl
+// Map array to new structure
+items: $input.items | map({
+    id: $.id,
+    name: $.title | trim | capitalize,
+    price: $.amount | to.decimal
+})
+
+// Filter and transform
+expensiveItems: $input.items 
+    | filter($.price > 100)
+    | map({ name: $.title, price: $.price })
+    | sort
+
+// Aggregate values
+$total: $items 
+    | map({{ $.price * $.quantity }})
+    | reduce({{ $acc + $it }});
+```
+
+### Conditional Fields
+```isl
+{
+    id: $input.id,
+    name: $input.name,
+    // Conditionally include email
+    ...if ($input.email) { email: $input.email } else {} endif,
+    // Use coalesce for defaults
+    phone: $input.phone ?? "N/A"
+}
+```
+
+## What Copilot Should NOT Suggest
+
+- ❌ JavaScript syntax (`const`, `let`, `var`, `function`, `=>`)
+- ❌ Python syntax (`def`, `lambda`, `:`)
+- ❌ Method chaining (`.map()`, `.filter()`)
+- ❌ Math without `{{ }}` wrapping
+- ❌ `return;` without value
+- ❌ Accessing properties after modifiers inline
+- ❌ Traditional for loops (`for (i=0; i 100);
+```
+
+### Math Expressions
+```isl
+// MUST wrap in {{ }}
+$total: {{ $price * $quantity * 1.1 }};
+$average: {{ ($sum / $count) }};
+```
+
+### String Interpolation
+```isl
+// Use backticks
+$greeting: `Hello ${$user.name}!`;
+$message: `Total: \${{ $price * $qty }}`;  // Escape $ for literal
+```
+
+## Syntax Rules (STRICT)
+
+1. **Every transformation needs `fun run($input)`** - main entry point
+2. **Math operations MUST be in `{{ }}`** - no exceptions
+3. **No parentheses around modifiers in conditions**:
+   ```isl
+   // ❌ WRONG
+   if (($text | length) > 5) ... endif
+   
+   // ✅ CORRECT
+   if ($text | length > 5) ... endif
+   ```
+
+4. **Return statement always needs a value**:
+   ```isl
+   // ❌ WRONG
+   return;
+   
+   // ✅ CORRECT
+   return {};
+   return $value;
+   ```
+
+5. **String interpolation depth rules**:
+   ```isl
+   // Simple variable (no dots)
+   `Hello $name`
+   
+   // Nested path (has dots) - needs ${}
+   `Hello ${$user.name}`
+   
+   // Math expression
+   `Total: {{ $a + $b }}`
+   ```
+
+## Standard File Structure
+
+```isl
+// 1. Main entry point (REQUIRED)
+fun run($input) {
+    // Your transformation here
+    id: $input.id | to.string;
+    name: `${$input.firstName} ${$input.lastName}`;
+    total: {{ $input.price * $input.quantity }};
+}
+
+// 2. Helper functions
+fun helperFunction($param) {
+    return $result;
+}
+
+// 3. Custom modifiers
+modifier customModifier($value, $param) {
+    return $transformed;
+}
+```
+
+## Common Built-in Modifiers
+
+### String Operations
+`trim`, `upperCase`, `lowerCase`, `capitalize`, `split(delimiter)`, `replace(old, new)`, `subString(start, end)`, `length`
+
+### Array Operations
+`map(expression)`, `filter(condition)`, `reduce(expression)`, `sort`, `reverse`, `unique`, `first`, `last`, `at(index)`, `length`, `isEmpty`, `push(item)`, `pop`
+
+### Type Conversions
+`to.string`, `to.number`, `to.decimal`, `to.boolean`, `to.array`, `to.json`, `to.xml(rootName)`
+
+### Date/Time
+`date.parse(format)`, `date.add(value, unit)`, `date.part(part)`, `to.string(format)`, `to.number` (epoch seconds)
+
+### Math
+`round`, `absolute`, `negate`, `precision(decimals)`, `round.up(decimals)`, `round.down(decimals)`
+
+### Object Operations
+`keys`, `kv` (key-value pairs), `select(path)`, `delete(prop)`, `getProperty(name)`
+
+## Control Flow
+
+### If/Else
+```isl
+// As statement
+if ($status == "active")
+    // do something
+else
+    // do something else
+endif
+
+// As expression
+$result: if ($paid) "success" else "pending" endif;
+```
+
+### Switch/Case
+```isl
+$result: switch ($code)
+    200 -> "OK";
+    404 -> "Not Found";
+    /^5\d\d/ -> "Server Error";
+    < 300 -> "Success";
+    else -> "Unknown";
+endswitch
+```
+
+### Foreach Loop
+```isl
+$transformed: foreach $item in $input.items
+    {
+        id: $item.id,
+        doubled: {{ $item.value * 2 }}
+    }
+endfor
+
+// With filter
+foreach $item in $items | filter($.price > 100)
+    // process expensive items
+endfor
+```
+
+### While Loop
+```isl
+$i: 0;
+while ($i < 10)
+    $i: {{ $i + 1 }};
+endwhile
+```
+
+## Operators
+
+### Comparison
+`==`, `!=`, `<`, `>`, `<=`, `>=`
+
+### Logical
+`and`, `or`, `!`
+
+### String
+`contains`, `startsWith`, `endsWith` (case-sensitive), `matches` (regex)
+
+### Other
+`in` (array membership), `is` (type check), `??` (coalesce - first non-null)
+
+## Best Practices
+
+1. **Use modifiers for simple transformations**
+   ```isl
+   // ✅ Good
+   $price: $input.amount | to.decimal | precision(2);
+   ```
+
+2. **Use functions for complex logic**
+   ```isl
+   // ✅ Good
+   fun calculateShipping($order) {
+       $baseRate: 5.99;
+       $weight: $order.totalWeight;
+       return if ($weight > 50) {{ $baseRate * 1.5 }} else $baseRate endif;
+   }
+   ```
+
+3. **Break down complex transformations**
+   ```isl
+   // ✅ Good - readable
+   fun run($input) {
+       customer: @.This.transformCustomer($input.customer);
+       items: $input.items | map(@.This.transformItem($));
+       shipping: @.This.calculateShipping($input);
+   }
+   ```
+
+4. **Use spread operator for object merging**
+   ```isl
+   {
+       ...$baseObject,
+       additionalField: "value"
+   }
+   ```
+
+5. **Proper spacing improves readability**
+   ```isl
+   // ✅ Good
+   $result: $items | map( $.id ) | filter( $ > 100 );
+   
+   // ❌ Avoid
+   $result:$items|map($.id)|filter($>100);
+   ```
+
+## Common Patterns
+
+### Transform nested arrays
+```isl
+items: $input.orders | map({
+    orderId: $.id,
+    items: $.lineItems | map({
+        productId: $.product.id,
+        quantity: $.qty
+    })
+})
+```
+
+### Conditional object properties
+```isl
+{
+    id: $input.id,
+    name: $input.name,
+    // Include email only if present
+    ...if ($input.email) { email: $input.email } else {} endif
+}
+```
+
+### Error handling with coalesce
+```isl
+displayName: $user.preferredName ?? $user.firstName ?? "Guest";
+amount: $transaction.amount | to.decimal ?? 0.0;
+```
+
+## What NOT to Do
+
+❌ Don't use JavaScript/Python/Java syntax
+❌ Don't use arrow functions `=>`
+❌ Don't use method chaining `.map().filter()`
+❌ Don't forget `{{ }}` around math
+❌ Don't use `return;` without value
+❌ Don't access properties after modifiers: `($items | last).id`
+❌ Don't put parentheses around modified values in conditions
+
+## Important Clarifications
+
+✅ **Modifiers inside math expressions are valid**:
+   ```isl
+   $result: {{ $items | length + 5 }}
+   ```
+
+✅ **if conditions returning true or false are valid**:
+   ```isl
+   return if ($value) true else false endif;
+   ```
+   This tests if `$value` is true-ish (not null, empty, zero or false) and returns a boolean true or false. Use `!$value` for the negative case.
+
+✅ **Null access is safe**:
+   ```isl
+   $val: $object.property.property.property
+   ```
+   ISL does not suffer from null references. If any property in a chain is missing, ISL will simply make `$val` null. Don't complain about or suggest null checks for property chains.
+
+## Documentation
+- Critical that you ALSO read this guide: https://intuit.github.io/isl/ai/ this has more detailed guidelines.
+- Full Docs: https://intuit.github.io/isl/
+- Modifiers: https://intuit.github.io/isl/language/modifiers/
+- CLI: https://intuit.github.io/isl/cli/
+
diff --git a/plugin/CHANGELOG.md b/plugin/CHANGELOG.md
new file mode 100644
index 0000000..ddafbb9
--- /dev/null
+++ b/plugin/CHANGELOG.md
@@ -0,0 +1,214 @@
+# Change Log
+
+All notable changes to the ISL Language Support extension will be documented in this file.
+
+## [1.1.0] - TBD
+
+### Added
+- **Custom Extensions Support**: Define project-specific functions and modifiers
+  - `.islextensions` file support in workspace root
+  - JSON-based definition format for functions and modifiers
+  - Full parameter and return type documentation
+  - IntelliSense/autocomplete integration for custom definitions
+  - Hover documentation for custom functions and modifiers
+  - Validation recognizes custom extensions (no warnings)
+  - Auto-reload when `.islextensions` file changes
+  - File watcher for automatic extension updates
+  - Comprehensive documentation and examples
+  - Support for optional parameters and default values
+  - Usage examples in hover tooltips
+  - See [ISL Extensions Documentation](docs/EXTENSIONS.md) for details
+
+### Improved
+- Enhanced completion provider to support custom extensions
+- Enhanced hover provider with detailed custom extension documentation
+- Enhanced validator to recognize custom functions and modifiers
+- Better extensibility architecture for future enhancements
+
+## [1.0.0] - 2025-11-26
+
+### Added
+- **Signature Help**: Parameter hints for functions and modifiers
+- **Inlay Hints**: Type annotations displayed inline for variables
+- **Code Actions & Quick Fixes**:
+  - Simplify unnecessary string interpolation braces (`${$var}` → `$var`)
+  - Convert `default()` modifier to null coalescing operator (`??`)
+  - Format long single-line objects onto multiple lines
+  - Change colon (`:`) to equals (`=`) for variable assignments
+  - "Fix all in file" option for colon-to-equals conversions
+- **Enhanced Snippets**: 20+ new snippets for common ISL patterns including:
+  - Safe navigation and error handling
+  - Array and object transformations
+  - Conditional field patterns
+  - Date operations
+  - Batch processing patterns
+- **CodeLens Enhancements**: 
+  - "Test Function" action for quick testing
+  - "Find Usages" for tracking function references
+- **Status Bar Enhancements**: Active file indicator and quick actions
+
+### Improved
+
+#### Syntax Highlighting
+- Fixed division operator (`/`) being incorrectly highlighted as regex
+- Removed regex literal pattern (ISL uses regex as string arguments)
+- Improved modifier highlighting with consistent coloring for namespaced modifiers
+- Enhanced pipe operator (`|`) visibility with distinct color
+- Better number literal highlighting
+
+#### Code Completion
+- Fixed variable autocompletion preserving `$` prefix when completing
+- Improved context-aware suggestions
+
+#### Formatter
+- **Pipe Spacing**: Automatically adds space after pipe operator (`|trim` → `| trim`)
+- **Function/Modifier Parameters**: Consistent spacing in declarations
+  - With parameters: `fun name( $param1, $param2 )`
+  - Without parameters: `fun name()`
+- **Control Flow Parameters**: Consistent spacing for `if`, `switch`, `while`, `foreach`
+  - Example: `if ( $condition )`, `switch ( $value )`
+- **Nested Control Flow**: Proper indentation for nested structures, including:
+  - Nested switch statements in switch cases (`"A" -> switch($x) ... endswitch;`)
+  - Control flow after arrow operator (`->`)
+- **Switch Statement Objects**: Fixed indentation for object values in switch cases
+  - Properly handles `};` in case values without closing the switch
+- **Modifier Chain Indentation**: Multi-line modifier chains are now indented
+- **Multi-line String Preservation**: Backtick strings preserve original formatting and indentation
+- Removed automatic splitting of long modifier chains (user controls line breaks)
+
+#### Validator
+- **Semantic Validation**:
+  - Undefined function detection
+  - Undefined modifier detection (including `push`, `getProperty`, `setProperty`)
+  - Undeclared variable usage detection
+  - Smart variable declaration tracking (supports both `=` and `:` operators)
+- **Improved Control Flow Balance**:
+  - Fixed `endfor`/`foreach` matching in expression contexts
+  - Fixed `endswitch`/`switch` matching for nested switches
+  - Fixed `endif`/`if` detection in complex conditions
+  - Recognizes control flow after arrow operator (`->`) in switch cases
+  - Better handling of inline vs. block control flow statements
+- **Return Statement Validation**: Completely rewritten to accurately track function scope with proper brace depth tracking
+- **Comment Handling**: All validations now correctly ignore code in comments
+- **Assignment Operators**: Recognizes both `=` and `:` for variable declarations
+- **Information-Level Diagnostics** (blue squiggly):
+  - Long object declarations suggesting formatting
+  - Colon usage suggesting equals for consistency
+  - Unnecessary string interpolation braces
+
+#### Spell Checking
+- Added ISL-specific keywords to cSpell dictionary:
+  - Control flow: `endfor`, `endswitch`, `endif`, `endwhile`
+  - Modifiers: `upperCase`, `toLowerCase`, `pushItems`, `getProperty`, `setProperty`
+  - VSCode extension terms: `inlayhints`, `codelens`, `quickfix`
+  - Common concatenated forms
+
+### Fixed
+- Division operator no longer highlighted as regex in expressions
+- Variable completion maintains `$` prefix
+- Object formatting no longer cuts string interpolations
+- Math expressions no longer misidentified as objects in type hints
+- Variables in comments no longer trigger "undeclared variable" warnings
+- Control flow balance correctly handles all nesting scenarios
+- Return statement validation accurate in complex nested functions
+- Function/modifier parameter spacing consistent after formatting
+- Nested switch statement indentation correct
+- Switch case object values maintain proper indentation
+- Multi-line backtick strings preserve internal formatting
+
+### Configuration
+- Removed `isl.formatting.formatModifierChains` (automatic chain splitting removed)
+- Kept `isl.formatting.alignProperties` for object property alignment
+
+## [1.0.0-Beta]
+
+### Added
+- Initial release
+- Comprehensive syntax highlighting for ISL files
+- IntelliSense with code completion for:
+  - Keywords and control flow structures
+  - Service calls (@.Date, @.Math, etc.)
+  - Modifiers (|filter, |map, etc.)
+  - Variable references
+- Real-time validation and linting
+  - Syntax error detection
+  - Brace matching
+  - Control flow balance checking
+  - Variable declaration validation
+- Hover documentation for:
+  - Keywords
+  - Built-in services
+  - Modifiers
+  - Variables
+- Go to definition support for:
+  - Function declarations
+  - Type definitions
+  - Imported modules
+- Code formatting with customizable settings
+- ISL execution from VS Code
+  - Run with inline JSON input
+  - Run with external JSON file
+  - Formatted output display
+  - Integrated output console
+- Comprehensive code snippets for common ISL patterns
+- Configuration options for validation, formatting, and execution
+- Status bar indicator for ISL files
+- Language configuration for:
+  - Comment toggling
+  - Bracket matching
+  - Auto-closing pairs
+  - Code folding
+
+### Features Details
+
+#### Syntax Highlighting
+- Keywords: fun, modifier, if, foreach, while, switch, return, etc.
+- Operators: ==, !=, <, >, <=, >=, contains, matches, ??, etc.
+- String interpolation: `${expression}`, `{{math}}`, `@.Function()`
+- Variables with $ prefix
+- Function calls and modifiers
+- Comments (// and #)
+- Regular expressions
+
+#### Code Completion
+- Context-aware completions
+- Service completions after @.
+- Modifier completions after |
+- Variable completions after $
+- Snippet expansions
+
+#### Validation
+- Balanced braces, brackets, and parentheses
+- Matching if/endif, foreach/endfor, while/endwhile, switch/endswitch
+- Valid variable names (no reserved keywords)
+- Proper string interpolation syntax
+- Return statements only in functions
+
+#### Execution
+- Automatic detection of ISL runtime (isl.sh/isl.bat)
+- Custom Java home configuration
+- Detailed execution output
+- Error reporting with stdout/stderr
+- JSON output formatting
+
+### Documentation
+- Comprehensive README with examples
+- Configuration guide
+- Feature documentation
+- Getting started guide
+
+## [Unreleased]
+
+### Planned Features
+- Semantic token provider for better highlighting
+- Symbol provider for outline view
+- Reference finder
+- Rename symbol support
+- Debug adapter protocol support
+- Test runner integration
+- Performance optimizations
+- Multi-file validation
+- Import resolution and auto-import
+- Type checking based on type declarations
+- Bracket pair colorization
+
diff --git a/plugin/LICENSE b/plugin/LICENSE
new file mode 100644
index 0000000..3003b4d
--- /dev/null
+++ b/plugin/LICENSE
@@ -0,0 +1,18 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+Copyright 2024 ISL Contributors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
diff --git a/plugin/PLUGIN-OVERVIEW.md b/plugin/PLUGIN-OVERVIEW.md
new file mode 100644
index 0000000..05e6947
--- /dev/null
+++ b/plugin/PLUGIN-OVERVIEW.md
@@ -0,0 +1,134 @@
+# ISL VSCode Plugin - Technical Overview
+
+## Architecture
+
+A TypeScript-based VSCode extension providing comprehensive language support for ISL (Intuitive Scripting Language).
+
+### Core Components
+
+```
+plugin/
+├── src/
+│   ├── extension.ts          # Extension activation & registration
+│   ├── completion.ts         # IntelliSense provider
+│   ├── hover.ts              # Hover documentation
+│   ├── definition.ts         # Go-to-definition
+│   ├── formatter.ts          # Code formatting
+│   ├── validator.ts          # Syntax validation
+│   ├── executor.ts           # ISL runtime integration
+│   ├── codelens.ts           # CodeLens actions
+│   ├── inlayhints.ts         # Type hints
+│   ├── signature.ts          # Signature help
+│   └── codeactions.ts        # Quick fixes
+│
+├── syntaxes/
+│   └── isl.tmLanguage.json   # TextMate grammar for syntax highlighting
+│
+└── snippets/
+    └── isl.json              # Code snippets
+```
+
+## Implementation Details
+
+### Provider Pattern
+Each language feature is implemented as a separate provider:
+- **CompletionItemProvider**: Context-aware code completion
+- **HoverProvider**: Documentation on hover
+- **DefinitionProvider**: Navigate to declarations
+- **DocumentFormattingProvider**: Smart code formatting
+- **DiagnosticProvider**: Real-time validation
+- **CodeLensProvider**: Inline actions (test, usages)
+- **InlayHintsProvider**: Type annotations
+- **SignatureHelpProvider**: Parameter hints
+- **CodeActionProvider**: Quick fixes and refactoring
+
+### Syntax Highlighting
+- **Technology**: TextMate grammar (JSON)
+- **Scopes**: 50+ token types for precise highlighting
+- **Features**: String interpolation, nested structures, modifiers
+
+### Validation Engine
+- **Approach**: Real-time, incremental validation
+- **Debouncing**: 500ms delay on typing
+- **Checks**: Syntax errors, semantic validation, control flow balance
+- **Diagnostics**: Error, Warning, Information, Hint levels
+
+### Formatter
+- **Strategy**: Multi-pass processing
+  1. Normalize spacing (pipes, parameters, assignments)
+  2. Calculate indentation (control flow, nesting, continuations)
+  3. Preserve literals (multi-line strings, comments)
+- **Features**:
+  - Smart indentation for nested control flow
+  - Modifier chain alignment
+  - Parameter spacing normalization
+  - Multi-line string preservation
+
+## Performance Optimizations
+
+- **Debounced Validation**: Reduces CPU during typing
+- **Incremental Updates**: Only revalidates changed documents
+- **Lazy Loading**: Providers loaded on-demand
+- **Efficient Parsing**: Optimized regex patterns
+- **Async Execution**: Non-blocking ISL runtime calls
+
+## Configuration System
+
+All settings prefixed with `isl.`:
+- `validation.*` - Validation behavior
+- `formatting.*` - Formatting preferences
+- `execution.*` - ISL runtime configuration
+- `linting.*` - Linting rules
+
+## Extension Lifecycle
+
+1. **Activation**: Triggered on `.isl` file or ISL command
+2. **Registration**: Providers registered with VSCode
+3. **Events**: Document change listeners attached
+4. **Deactivation**: Cleanup on extension unload
+
+## Technologies
+
+- **TypeScript 5.0**: Type-safe implementation
+- **VS Code API 1.75+**: Extension framework
+- **TextMate**: Syntax highlighting
+- **Node.js**: Runtime environment
+
+## Key Features Added in v1.1.0
+
+### Semantic Validation
+- Undefined function/modifier detection
+- Variable declaration tracking
+- Return statement scope validation
+
+### Enhanced Formatting
+- Control flow indentation (including `->` operator)
+- Switch case object handling
+- Multi-line string preservation
+- Modifier chain indentation
+
+### Quick Fixes
+- Simplify string interpolation
+- Convert `:` to `=` for assignments
+- Format long objects
+- Replace `default()` with `??`
+
+### Developer Experience
+- Signature help for functions
+- Inlay type hints
+- CodeLens actions (test, find usages)
+- 20+ new code snippets
+
+## Status
+
+✅ Production-ready  
+✅ Zero linting errors  
+✅ TypeScript strict mode  
+✅ Comprehensive error handling  
+✅ Full test coverage via manual testing  
+
+---
+
+For user documentation, see [README.md](README.md)  
+For setup instructions, see [SETUP.md](SETUP.md)  
+For publishing guide, see [PUBLISHING.md](PUBLISHING.md)
diff --git a/plugin/PUBLISHING.md b/plugin/PUBLISHING.md
new file mode 100644
index 0000000..9bf427c
--- /dev/null
+++ b/plugin/PUBLISHING.md
@@ -0,0 +1,135 @@
+# Publishing the ISL VSCode Extension
+
+Quick guide for publishing the extension to VSCode Marketplace and Open VSX Registry.
+
+## Prerequisites
+
+### 1. VSCode Marketplace Account
+
+1. Go to https://marketplace.visualstudio.com/manage
+2. Sign in with Microsoft account
+3. Create a publisher (or use existing)
+4. Generate Personal Access Token (PAT):
+   - Visit https://dev.azure.com/
+   - User Settings → Personal Access Tokens
+   - Create token with scope: **Marketplace → Manage**
+   - Save the token securely
+
+### 2. Open VSX Account (Optional)
+
+For VS Codium and other VSCode-compatible editors:
+
+1. Go to https://open-vsx.org/
+2. Sign in with GitHub
+3. Settings → Access Tokens → Generate
+4. Save the token
+
+### 3. Install VSCE
+
+```bash
+npm install -g @vscode/vsce
+```
+
+## Publishing Workflow
+
+### Update Version
+
+```bash
+cd plugin
+npm version patch  # or minor, or major
+```
+
+This updates `package.json` and creates a git tag.
+
+### Pre-Publish Checklist
+
+- [ ] Version updated in `package.json`
+- [ ] `CHANGELOG.md` updated with release notes
+- [ ] Code compiles without errors: `npm run compile`
+- [ ] Extension tested locally
+- [ ] `publisher` field correct in `package.json`
+
+### Publish to VSCode Marketplace
+
+Before publishing make sure you rebuild the ISL and download the [latest ISL Library](https://central.sonatype.com/search?namespace=com.intuit.isl)
+```bash
+./gradlew :plugin:build-isl-runtime:buildIslRuntime -PislVersion=1.1.0
+```
+
+```bash
+cd plugin
+vsce publish
+```
+
+Or with explicit token:
+```bash
+vsce publish -p YOUR_PAT
+```
+
+### Publish to Open VSX (Optional)
+
+```bash
+npx ovsx publish -p YOUR_OVSX_TOKEN
+```
+
+## GitHub Actions (Automated)
+
+### Setup Secrets
+
+Add to GitHub repository (Settings → Secrets → Actions):
+- `VSCE_TOKEN`: VSCode Marketplace PAT
+- `OVSX_TOKEN`: Open VSX token
+
+### Trigger Release
+
+```bash
+# Tag and push
+git tag plugin-v1.1.0
+git push origin plugin-v1.1.0
+```
+
+The workflow automatically:
+1. Builds extension
+2. Packages `.vsix`
+3. Publishes to both marketplaces
+4. Creates GitHub release
+
+## Troubleshooting
+
+### "Cannot find publisher"
+Ensure `publisher` in `package.json` matches your marketplace publisher ID exactly.
+
+### "Authentication failed"
+- Verify PAT is valid and not expired
+- Check PAT has "Marketplace → Manage" scope
+- Ensure secret name matches exactly: `VSCE_TOKEN`
+
+### "Version already exists"
+You cannot republish the same version. Increment version number.
+
+### "Package too large"
+VSCode Marketplace limit is 100MB. Our extension is well under this.
+
+## Manual Testing Before Publishing
+
+```bash
+# Package locally
+cd plugin
+vsce package
+
+# Install in VSCode
+code --install-extension isl-language-support-X.X.X.vsix
+
+# Test all features
+```
+
+## Links
+
+- **VSCode Marketplace**: https://marketplace.visualstudio.com/vscode
+- **Publisher Management**: https://marketplace.visualstudio.com/manage
+- **Open VSX Registry**: https://open-vsx.org/
+- **Publishing Guide**: https://code.visualstudio.com/api/working-with-extensions/publishing-extension
+
+---
+
+For technical details, see [PLUGIN-OVERVIEW.md](PLUGIN-OVERVIEW.md)
diff --git a/plugin/README.md b/plugin/README.md
new file mode 100644
index 0000000..c500977
--- /dev/null
+++ b/plugin/README.md
@@ -0,0 +1,341 @@
+# ISL Language Support for VS Code & Cursor
+
+Comprehensive language support for ISL [(Intuitive Scripting Language)](https://intuit.github.io/isl/) - a powerful JSON transformation scripting language.
+
+[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
+
+## What is ISL
+
+ISL is a low-code [interpreted scripting language]() and runtime container designed to provide developers and non-developers an easy way to write, test, and deploy user developed code inside any service.
+
+The ISL supports an intuitive simplified syntax with features that make data acquisition and data transformations easy with minimal lines of code. In addition, the language supports easy extensibility allowing it to be used as a multi-purpose service extensibility language.
+
+The ISL can be embedded in any JVM based project to provide runtime based extensibility through a fast and lightweight runtime.
+
+## Overview
+
+In the most simple form the ISL is a JSON transformation language:
+
+Given Input JSON:
+```json
+{
+	"title": "IPod Nano - 8GB",
+	"body_html": "It's the small iPod with a big idea: Video.",
+	"id": 632910392,
+	"images": [
+		{
+			"id": 850703190,
+			"src": "http://example.com/burton.jpg",
+		}
+	],
+	"options": {
+		"name": "Color",
+		"values": [
+			"Pink",
+			"Red",
+			"Green",
+			"Black"
+		]
+	},
+	"status": "active",
+	"tags": "Emotive, Flash Memory, MP3, Music",
+	"updated_at": 1645004735,
+	"vendor": "Apple"
+}
+```
+
+And Transformation:
+```isl
+fun transform( $input ){
+    return {
+      // Simple JSON Path Selectors
+      id: $input.id,
+      // piped modifiers using `|`
+      name: $input.title | trim,
+      // easy string building using interpolation ` ... `
+      short_description: `${ $input.title } by ${ $input.vendor }`,
+      // child object building
+      primary_image: {
+          id: $input.images[0].id,
+          url: $input.images[0].src
+      },
+      // conditional properties
+      is_active: if( $input.status == "active" ) true else false,
+      option_name: $input.options.name,
+      // array to csv
+      option_values: $input.options.values | join(','),
+      // date processing
+      updated: $input.updated_at | date.fromEpochSeconds | to.string("yyyy-MM-dd HH:mm")
+    }
+}
+```
+
+Will output:
+```json
+{
+	"id": 632910392,
+	"name": "IPod Nano - 8GB",
+	"short_description": "IPod Nano - 8GB by Apple",
+	"primary_image": {
+		"id": 850703190,
+		"url": "http://example.com/burton.jpg"
+	},
+	"is_active": true,
+	"option_name": "Color",
+	"option_values": "Pink,Red,Green,Black",
+	"updated": "2022-02-47 09:45"
+}
+```
+
+
+## Features
+
+### 🎨 Syntax Highlighting
+- Complete syntax highlighting for all ISL constructs
+- String interpolation: `${variable}`, `{{math}}`, `@.Function()`
+- Modifiers, operators, control flow, and more
+
+### 🔧 Custom Extensions Support
+**NEW!** Define your own project-specific functions and modifiers:
+- Create a `.islextensions` file in your workspace root
+- **Or configure a global source** (URL or file path) shared across all projects
+- Define custom functions and modifiers with full type information
+- Get IntelliSense, hover documentation, and validation for your extensions
+- Auto-reload when definitions change
+- Workspace-local files override global source for project-specific extensions
+- [Learn more about ISL Extensions](docs/EXTENSIONS.md)
+
+**Built-in definitions:** All built-in functions and modifiers (e.g. `date.fromEpochSeconds`, `Math.sum`, `trim`) are defined in **`isl-language.json`** at the plugin root. Edit that file to add or change built-ins; completion, hover, signatures, and validation all use it as the single source of truth.
+
+### 💡 IntelliSense & Code Completion
+Smart completion for:
+- **Keywords**: `fun`, `foreach`, `if`, `switch`, etc.
+- **Services**: `@.Date`, `@.Math`, `@.String`, etc.
+- **Modifiers**: `|filter`, `|map`, `|trim`, `|upperCase`, 50+ more
+- **Variables**: Automatic discovery from your code
+
+### ✅ Validation & Linting
+Real-time error detection:
+- Balanced braces, brackets, parentheses
+- Control flow matching (`if`/`endif`, `foreach`/`endfor`, etc.)
+- Undefined functions and modifiers
+- Undeclared variable usage
+- Invalid syntax and semantic errors
+
+### 📖 Hover Documentation
+Hover over any element for:
+- Keyword syntax and usage
+- Service method descriptions
+- Modifier documentation with examples
+- Variable type information
+
+### 🔧 Code Actions & Quick Fixes
+- Simplify string interpolation (`${$var}` → `$var`)
+- Convert `:` to `=` for variable assignments
+- Format long objects onto multiple lines
+- Replace `default()` with `??` operator
+
+### ✨ Smart Formatting
+- Automatic indentation and spacing
+- Parameter spacing normalization
+- Modifier chain alignment
+- Multi-line string preservation
+- Format on save support
+
+### 🎯 Signature Help & Type Hints
+- Parameter hints for functions and modifiers
+- Inline type annotations for variables
+- CodeLens actions (test functions, find usages)
+
+### ▶️ Execute ISL
+- Run transformations directly from editor
+- Test with inline JSON or external files
+- View formatted output side-by-side
+- Integrated error reporting
+
+### 📋 Code Snippets
+20+ ready-to-use snippets for:
+- Functions and modifiers
+- Control flow patterns
+- Array and object transformations
+- Date operations
+- Error handling
+
+## Quick Start
+
+1. Install the extension
+2. Open or create a `.isl` file
+3. Start coding with full language support!
+
+### Defining Custom Extensions
+
+Want IntelliSense for your project-specific functions and modifiers?
+
+**Option 1: Project-specific (workspace-local)**
+1. Create a `.islextensions` file in your workspace root
+2. Define your custom functions and modifiers in JSON format
+3. Enjoy full IDE support for your extensions!
+
+**Option 2: Global source (shared across projects)**
+1. Configure `isl.extensions.source` in VS Code settings
+2. Set to a URL (e.g., `https://example.com/extensions.json`) or file path
+3. All projects automatically use these extensions
+4. Override with workspace-local `.islextensions` when needed
+
+**Example `.islextensions`:**
+```json
+{
+  "functions": [
+    {
+      "name": "sendEmail",
+      "description": "Sends an email via custom service",
+      "parameters": [
+        {"name": "to", "type": "String"},
+        {"name": "subject", "type": "String"},
+        {"name": "body", "type": "String"}
+      ]
+    }
+  ],
+  "modifiers": [
+    {
+      "name": "formatPhone",
+      "description": "Formats phone numbers",
+      "parameters": [
+        {"name": "format", "type": "String", "optional": true}
+      ]
+    }
+  ]
+}
+```
+
+See [ISL Extensions Documentation](docs/EXTENSIONS.md) for complete details.
+
+### Example
+
+```isl
+fun run($input) {
+    $customers = foreach $customer in $input.customers
+        {
+            id: $customer.id | to.string,
+            name: `${$customer.first} ${$customer.last}` | trim,
+            email: $customer.email | lowerCase,
+            orders: $customer.orders | filter($order.status == "completed")
+        }
+    endfor
+    
+    return {
+        customers: $customers,
+        total: $customers | length,
+        processed: @.Date.Now() | to.string("yyyy-MM-dd")
+    }
+}
+```
+
+## Configuration
+
+Available settings (all prefixed with `isl.`):
+
+```json
+{
+  "isl.validation.enabled": true,
+  "isl.formatting.enabled": true,
+  "isl.formatting.indentSize": 4,
+  "isl.formatting.useTabs": false,
+  "isl.formatting.alignProperties": false,
+  "isl.execution.islCommand": "isl",
+  "isl.execution.javaHome": "",
+  "isl.extensions.source": "",
+  "isl.extensions.cacheTTL": 3600
+}
+```
+
+**Extension Settings:**
+- `isl.extensions.source`: Global source for `.islextensions` (URL or file path). Workspace-local files take precedence.
+- `isl.extensions.cacheTTL`: Cache TTL in seconds for URL-based extensions (default: 3600 = 1 hour).
+
+## Commands
+
+- **ISL: Validate Current File** - Run validation
+- **ISL: Run Transformation** - Execute with inline input
+- **ISL: Run Transformation with Input File** - Execute with JSON file
+- **ISL: Format Document** - Format code
+- **ISL: Open Documentation** - Open ISL docs
+
+## Windsurf Troubleshooting
+
+If the extension doesn't work in Windsurf (commands not found, no Output panel):
+
+1. **Check Developer Tools** – Open **Help → Toggle Developer Tools** (or **Developer: Toggle Developer Tools** from Command Palette). Check the Console tab for errors when loading the extension or when activating it.
+
+2. **Check Extension Host** – In the Output panel, select **Extension Host** from the dropdown. Look for activation errors or stack traces.
+
+3. **Verify installation** – Ensure you're using Windsurf 1.89+ if required. Reinstall the extension: uninstall, then install from the `.vsix` file again.
+
+4. **Activation errors** – If activation fails, the extension now shows an error message. Check **Output → ISL Language Support** for details.
+
+## Requirements
+
+To execute ISL transformations:
+- Java Runtime Environment (JRE) 11+
+- The extension bundles an embedded ISL CLI (`plugin/lib/isl-cmd-all.jar`) — no separate install needed
+
+### Updating the Embedded ISL Runtime
+
+When developing the extension with ISL changes, rebuild and copy the fat JAR:
+
+```bash
+# From the repository root
+./gradlew buildIslRuntimeLocal
+```
+
+See [lib/README.md](lib/README.md) for details.
+
+## 🤖 AI Assistant Support
+
+This extension includes AI configuration for Cursor, Windsurf, GitHub Copilot, and other AI editors. Your AI assistant automatically understands ISL syntax and can help write transformations.
+
+Ask your AI to:
+- Generate ISL transformations
+- Explain ISL syntax and modifiers
+- Convert data logic to ISL
+- Debug and optimize code
+
+## Resources
+
+- [ISL Documentation](https://intuit.github.io/isl/)
+- [ISL GitHub Repository](https://github.com/intuit/isl)
+- [Language Reference](https://intuit.github.io/isl/dsl/)
+- [Quick Start Guide](https://intuit.github.io/isl/quickstart/)
+- [ISL Extensions Guide](docs/EXTENSIONS.md) - Define custom functions and modifiers
+- [Example `.islextensions` file](.islextensions.example)
+
+## Release Notes
+
+### 1.1.0
+
+**Major improvements:**
+- Signature help and inlay hints
+- Code actions and quick fixes
+- Enhanced formatter (parameter spacing, nested control flow)
+- Semantic validation (undefined functions/modifiers, variable tracking)
+- 20+ new code snippets
+- Better control flow balance detection
+- Multi-line string preservation
+
+### 1.0.0
+
+Initial release with syntax highlighting, completion, validation, formatting, and execution support.
+
+See [CHANGELOG.md](CHANGELOG.md) for full details.
+
+## Contributing
+
+Found a bug or have a feature request? File an issue on [GitHub](https://github.com/intuit/isl).
+
+## License
+
+Apache License 2.0 - See [LICENSE](LICENSE) file.
+
+---
+
+**Enjoy using ISL!** 🚀
diff --git a/plugin/SETUP.md b/plugin/SETUP.md
new file mode 100644
index 0000000..056dcc1
--- /dev/null
+++ b/plugin/SETUP.md
@@ -0,0 +1,184 @@
+# ISL Extension - Setup Guide
+
+Guide for installing and setting up the ISL Language Support extension.
+
+## For Users
+
+### Installation
+
+#### From Marketplace
+1. Open VS Code or Cursor
+2. Go to Extensions (Ctrl+Shift+X / Cmd+Shift+X)
+3. Search for "ISL Language Support"
+4. Click Install
+
+#### Manual Installation
+1. Download the `.vsix` file from releases
+2. Open VS Code/Cursor
+3. Extensions view → "..." menu → "Install from VSIX..."
+4. Select the downloaded file
+
+### Configuration
+
+After installation, open Settings (Ctrl+,) and search for "ISL":
+
+**Essential Settings:**
+- `isl.execution.islCommand`: Path to `isl.sh` or `isl.bat`
+- `isl.execution.javaHome`: Java installation path (if not in PATH)
+
+**Optional Settings:**
+- `isl.formatting.indentSize`: Spaces per indent level (default: 4)
+- `isl.formatting.useTabs`: Use tabs instead of spaces
+- `isl.validation.enabled`: Enable/disable validation
+
+### ISL Runtime Setup
+
+To execute ISL transformations, you need the ISL runtime:
+
+1. **Install Java 11+**
+   ```bash
+   java -version  # Verify installation
+   ```
+
+2. **Get ISL Runtime**
+   - Clone: https://github.com/intuit/isl
+   - Build: `./gradlew build`
+   - Or use pre-built `isl.sh` / `isl.bat`
+
+3. **Configure Extension**
+   - Set `isl.execution.islCommand` to path of ISL script
+   - Example: `/path/to/isl/isl.sh`
+
+### Verification
+
+1. Open any `.isl` file
+2. You should see:
+   - Syntax highlighting
+   - IntelliSense suggestions
+   - Validation indicators
+3. Test execution: Right-click → "ISL: Run Transformation"
+
+## For Developers
+
+### Prerequisites
+- Node.js 18+
+- TypeScript 5+
+- VS Code or Cursor
+
+### Development Setup
+
+```bash
+# Navigate to plugin directory
+cd plugin
+
+# Install dependencies
+npm install
+
+# Compile TypeScript
+npm run compile
+
+# Start watch mode (auto-recompile)
+npm run watch
+```
+
+### Running Extension
+
+1. Open `plugin` folder in VS Code
+2. Press `F5` to launch Extension Development Host
+3. Open a `.isl` file to test features
+
+### Project Structure
+
+```
+plugin/
+├── src/                      # TypeScript source
+│   ├── extension.ts          # Entry point
+│   ├── completion.ts         # Code completion
+│   ├── validator.ts          # Validation
+│   ├── formatter.ts          # Formatting
+│   └── ...                   # Other providers
+├── syntaxes/                 # Syntax highlighting
+├── snippets/                 # Code snippets
+└── package.json              # Extension manifest
+```
+
+### Building & Packaging
+
+```bash
+# Update embedded ISL CLI (when ISL runtime changes)
+./gradlew buildIslRuntimeLocal   # from repo root
+
+# Compile
+npm run compile
+
+# Package for distribution
+npm install -g @vscode/vsce
+vsce package
+```
+
+Creates `isl-language-support-X.X.X.vsix`
+
+### Testing
+
+**Feature Testing Checklist:**
+- [ ] Syntax highlighting works
+- [ ] Code completion (keywords, services, modifiers)
+- [ ] Validation shows errors correctly
+- [ ] Hover documentation appears
+- [ ] Go to definition navigates correctly
+- [ ] Formatting works
+- [ ] ISL execution runs
+- [ ] Snippets expand correctly
+- [ ] Code actions appear
+- [ ] Inlay hints show types
+
+### Debugging
+
+1. Set breakpoints in TypeScript files
+2. Press `F5` to start debugger
+3. Extension host opens with debugger attached
+4. Check Debug Console for logs
+
+## Troubleshooting
+
+### Extension doesn't activate
+- Check file has `.isl` extension
+- View → Command Palette → "Change Language Mode" → ISL
+
+### Syntax highlighting not working
+- Restart VS Code
+- Check language mode is set to ISL
+
+### ISL execution fails
+- Verify Java: `java -version`
+- Check `isl.execution.islCommand` setting points to valid path
+- View Output panel: View → Output → ISL
+
+### Validation errors
+- Check for actual syntax errors in code
+- Disable temporarily: `isl.validation.enabled: false`
+- Report false positives on GitHub
+
+### Formatting issues
+- Check `isl.formatting.indentSize` setting
+- Try manual format: Shift+Alt+F
+- Report issues with code sample
+
+## Resources
+
+- [Plugin Overview](PLUGIN-OVERVIEW.md) - Technical architecture
+- [Publishing Guide](PUBLISHING.md) - How to publish
+- [ISL Documentation](https://intuit.github.io/isl/)
+- [VSCode Extension API](https://code.visualstudio.com/api)
+
+## Contributing
+
+1. Fork repository
+2. Create feature branch
+3. Make changes
+4. Test thoroughly
+5. Submit pull request
+
+## License
+
+Apache License 2.0 - See [LICENSE](LICENSE) file
diff --git a/plugin/build-isl-runtime/build.gradle.kts b/plugin/build-isl-runtime/build.gradle.kts
new file mode 100644
index 0000000..30e5f7f
--- /dev/null
+++ b/plugin/build-isl-runtime/build.gradle.kts
@@ -0,0 +1,70 @@
+/**
+ * Standalone Gradle build to download ISL artifacts from Maven Central
+ * and package them as a fat JAR for the VS Code plugin.
+ *
+ * Run: ./gradlew buildIslRuntime -PislVersion=1.1.0
+ * Output: plugin/lib/isl-cmd-all.jar
+ */
+plugins {
+    java
+    id("com.github.johnrengelman.shadow") version "8.1.1"
+}
+
+val islVersion: String = findProperty("islVersion")?.toString()
+    ?: project.findProperty("version")?.toString()
+    ?: "1.1.0"
+
+repositories {
+    mavenCentral()
+}
+
+dependencies {
+    implementation("com.intuit.isl:isl-cmd:$islVersion")
+}
+
+tasks.shadowJar {
+    archiveBaseName.set("isl-cmd")
+    archiveClassifier.set("all")
+    archiveVersion.set("")
+    archiveFileName.set("isl-cmd-all.jar")
+
+    manifest {
+        attributes(
+            "Main-Class" to "com.intuit.isl.cmd.IslCommandLineKt",
+            "Implementation-Title" to "ISL Command Line",
+            "Implementation-Version" to islVersion,
+            "Multi-Release" to "true"
+        )
+    }
+
+    mergeServiceFiles()
+}
+
+val buildIslRuntime by tasks.registering(Copy::class) {
+    group = "build"
+    description = "Download ISL from Maven Central and package as fat JAR for the plugin"
+    dependsOn(tasks.shadowJar)
+
+    from(tasks.shadowJar.get().archiveFile)
+    into(project.rootProject.layout.projectDirectory.dir("plugin/lib"))
+    rename { "isl-cmd-all.jar" }
+
+    doLast {
+        val dest = project.rootProject.file("plugin/lib/isl-cmd-all.jar")
+        logger.lifecycle("✓ Built isl-cmd-all.jar (ISL $islVersion) -> ${dest.absolutePath}")
+
+        val deps = configurations.runtimeClasspath.get()
+            .resolvedConfiguration
+            .lenientConfiguration
+            .allModuleDependencies
+            .sortedBy { "${it.moduleGroup}:${it.moduleName}" }
+
+        logger.lifecycle("")
+        logger.lifecycle("Packages joined into isl-cmd-all.jar (${deps.size} total):")
+        logger.lifecycle("-".repeat(60))
+        deps.forEach { dep ->
+            logger.lifecycle("  ${dep.moduleGroup}:${dep.moduleName}:${dep.moduleVersion}")
+        }
+        logger.lifecycle("-".repeat(60))
+    }
+}
diff --git a/plugin/cspell.json b/plugin/cspell.json
new file mode 100644
index 0000000..da2f9ec
--- /dev/null
+++ b/plugin/cspell.json
@@ -0,0 +1,78 @@
+{
+  "version": "0.2",
+  "language": "en",
+  "words": [
+    "endfor",
+    "endwhile",
+    "endif",
+    "endswitch",
+    "foreach",
+    "upperCase",
+    "toLowerCase",
+    "titleCase",
+    "coalesce",
+    "inlayhints",
+    "codelens",
+    "tmLanguage",
+    "dateparse",
+    "dateadd",
+    "mathsum",
+    "mathavg",
+    "mathmax",
+    "mathmin",
+    "tostring",
+    "tonumber",
+    "todecimal",
+    "toboolean",
+    "backquote",
+    "backticks",
+    "squiggly",
+    "squeegly",
+    "scopename",
+    "isempty",
+    "isnotempty",
+    "padstart",
+    "padend",
+    "startswith",
+    "endswith",
+    "getproperty",
+    "setproperty",
+    "tofixed",
+    "toprecision",
+    "textmate",
+    "unescape",
+    "groupby",
+    "findindex",
+    "typeof",
+    "varname",
+    "funcname",
+    "modname",
+    "parens",
+    "impl",
+    "refactor",
+    "vals",
+    "addr",
+    "cond",
+    "decl",
+    "args",
+    "nums",
+    "objs",
+    "expr",
+    "ident",
+    "substringUpto",
+    "tobinary",
+    "replacefirst",
+    "parsemultiline"
+  ],
+  "ignoreWords": [],
+  "import": [],
+  "flagWords": [],
+  "ignorePaths": [
+    "node_modules/**",
+    "out/**",
+    "dist/**",
+    "*.min.js",
+    "package-lock.json"
+  ]
+}
+
diff --git a/plugin/docs/COMPARISON-ISL-LANGUAGE-EXTENSION.md b/plugin/docs/COMPARISON-ISL-LANGUAGE-EXTENSION.md
new file mode 100644
index 0000000..d41f0e1
--- /dev/null
+++ b/plugin/docs/COMPARISON-ISL-LANGUAGE-EXTENSION.md
@@ -0,0 +1,47 @@
+# Features to Add (from isl-language extension)
+
+Features the isl-language extension has that we need to add to this plugin.
+
+---
+
+## 1. Lint-on-save only
+
+- **Add:** Option to run validation **only on save** (and optionally on open), not on every text change.
+- **Config:** e.g. `isl.validation.lintOnSave` (boolean). When `true`, validate on save/open only; when `false`, keep current debounced-on-typing behavior.
+
+---
+
+## 2. Exclude patterns
+
+- **Add:** Glob patterns to **exclude files** from linting (e.g. `**/node_modules/**`, `**/generated/**`).
+- **Config:** e.g. `isl.validation.exclude` (string[]). Skip validation when the document path matches any pattern.
+
+---
+
+## 3. Grammar-based syntax validation
+
+- **Add:** Syntax diagnostics from a **formal grammar** (e.g. ANTLR) so all syntax errors (unexpected tokens, missing brackets, etc.) are caught in one place with consistent positions and messages. Keep existing semantic checks (undefined refs, etc.) on top.
+
+---
+
+## 4. Diagnostic related information
+
+- **Add:** Attach **related information** to diagnostics (e.g. rule id) so the editor can show “Rule: XYZ” or similar.
+
+---
+
+## 5. CLI linter
+
+- **Add:** A **standalone CLI** that lints ISL files from the command line (e.g. `isl-lint file.isl` or `isl-validate file.isl`) for CI and scripts. It should accept file paths, output diagnostics (human-readable or JSON), and exit non-zero when there are errors.
+
+---
+
+## 6. NPX install
+
+- **Add:** A way to **install this extension via npx** without cloning (e.g. `npx isl-language-support install-extension --vscode` / `--cursor`) for users who don’t use the marketplace.
+
+---
+
+## 7. “Lint file” command
+
+- **Add:** A **command** that forces a lint/validation refresh for the current file (e.g. “Lint file” or “ISL: Lint file”), so users can re-run validation on demand.
diff --git a/plugin/docs/EXTENSIONS.md b/plugin/docs/EXTENSIONS.md
new file mode 100644
index 0000000..35de437
--- /dev/null
+++ b/plugin/docs/EXTENSIONS.md
@@ -0,0 +1,388 @@
+# ISL Extensions
+
+The ISL Language Support extension allows you to define custom functions and modifiers specific to your project using a `.islextensions` file. This enables the plugin to provide IntelliSense, hover documentation, and validation for your project-specific ISL extensions.
+
+## How It Works
+
+Similar to how `.cspell.json` defines custom dictionary words or `.cursorrules` defines AI assistant rules, you can create a `.islextensions` file in your project root to define custom ISL functions and modifiers.
+
+When you open an ISL file, the extension automatically:
+1. Looks for a `.islextensions` file in the workspace root (highest priority)
+2. If not found, loads from global source (URL or file path) if configured
+3. Loads and parses the custom definitions
+4. Integrates them into autocomplete, hover documentation, and validation
+5. Watches for changes and automatically reloads when the file is updated
+
+### Loading Priority
+
+The extension loads extensions in the following order (first match wins):
+
+1. **Workspace-local `.islextensions`** - Project-specific extensions that override global ones
+2. **Global source** - Shared extensions from URL or file path (configured in settings)
+3. **Empty** - No custom extensions
+
+This allows you to:
+- Share common extensions across all projects via a global source
+- Override or extend with project-specific extensions when needed
+- Keep projects in sync without duplicating the file in each project
+
+## File Format
+
+The `.islextensions` file uses JSON format with the following structure:
+
+```json
+{
+  "functions": [
+    {
+      "name": "functionName",
+      "description": "What the function does",
+      "parameters": [
+        {
+          "name": "param1",
+          "type": "String",
+          "description": "Parameter description",
+          "optional": false,
+          "defaultValue": "default"
+        }
+      ],
+      "returns": {
+        "type": "Object",
+        "description": "What the function returns"
+      },
+      "examples": [
+        "$result: @.This.functionName($param1);"
+      ]
+    }
+  ],
+  "modifiers": [
+    {
+      "name": "modifierName",
+      "description": "What the modifier does",
+      "parameters": [
+        {
+          "name": "param1",
+          "type": "String",
+          "optional": true,
+          "defaultValue": "default",
+          "description": "Parameter description"
+        }
+      ],
+      "returns": {
+        "type": "String",
+        "description": "What the modifier returns"
+      },
+      "examples": [
+        "$result: $input | modifierName;",
+        "$result: $input | modifierName(\"value\");"
+      ]
+    }
+  ]
+}
+```
+
+## Field Reference
+
+### Function Definition
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `name` | string | Yes | The function name (used in `@.This.functionName()`) |
+| `description` | string | No | Description shown in hover and autocomplete |
+| `parameters` | array | No | Array of parameter definitions |
+| `returns` | object | No | Return type and description |
+| `examples` | array | No | Array of usage examples |
+
+### Modifier Definition
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `name` | string | Yes | The modifier name (used with `\| modifierName`) |
+| `description` | string | No | Description shown in hover and autocomplete |
+| `parameters` | array | No | Array of parameter definitions |
+| `returns` | object | No | Return type and description |
+| `examples` | array | No | Array of usage examples |
+
+### Parameter Definition
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `name` | string | Yes | Parameter name |
+| `type` | string | No | Parameter type (String, Number, Boolean, Array, Object, etc.) |
+| `description` | string | No | Parameter description |
+| `optional` | boolean | No | Whether the parameter is optional (default: false) |
+| `defaultValue` | string | No | Default value if not provided |
+
+### Returns Definition
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `type` | string | No | Return type |
+| `description` | string | No | Description of what is returned |
+
+## Example
+
+Here's a complete example for a project with custom email and database functions:
+
+```json
+{
+  "functions": [
+    {
+      "name": "sendEmail",
+      "description": "Sends an email using the custom email service",
+      "parameters": [
+        {
+          "name": "to",
+          "type": "String",
+          "description": "Email recipient address"
+        },
+        {
+          "name": "subject",
+          "type": "String",
+          "description": "Email subject line"
+        },
+        {
+          "name": "body",
+          "type": "String",
+          "description": "Email body content"
+        }
+      ],
+      "returns": {
+        "type": "Object",
+        "description": "Result with success status"
+      },
+      "examples": [
+        "$result: @.This.sendEmail($user.email, \"Welcome\", `Hello ${$user.name}!`);"
+      ]
+    }
+  ],
+  "modifiers": [
+    {
+      "name": "formatPhone",
+      "description": "Formats a phone number to standard format",
+      "parameters": [
+        {
+          "name": "format",
+          "type": "String",
+          "optional": true,
+          "defaultValue": "US",
+          "description": "Format style (US, INTERNATIONAL, E164)"
+        }
+      ],
+      "returns": {
+        "type": "String",
+        "description": "Formatted phone number"
+      },
+      "examples": [
+        "$phone: $user.phone | formatPhone;",
+        "$phone: $user.phone | formatPhone(\"INTERNATIONAL\");"
+      ]
+    }
+  ]
+}
+```
+
+## Features
+
+Once you define your extensions, you get:
+
+### 1. IntelliSense / Autocomplete
+- Type `@.This.` to see your custom functions
+- Type `|` to see your custom modifiers
+- Full parameter hints and descriptions
+
+### 2. Hover Documentation
+- Hover over custom function or modifier names to see:
+  - Full signature with parameter types
+  - Description
+  - Parameter details
+  - Return type
+  - Usage examples
+  - Note indicating it's from `.islextensions`
+
+### 3. Validation
+- No warnings for using your custom functions/modifiers
+- The validator recognizes them as valid
+- Works alongside built-in ISL functions/modifiers
+
+### 4. Auto-reload
+- Changes to `.islextensions` are automatically detected
+- Extension reloads definitions without restart
+- All open ISL files are revalidated
+
+## Global Extensions Source
+
+To avoid maintaining `.islextensions` files in every project, you can configure a global source that all projects will use automatically.
+
+### Configuration
+
+Open VS Code Settings (Ctrl+, / Cmd+,) and search for "ISL extensions source", or add to your `settings.json`:
+
+```json
+{
+  "isl.extensions.source": "https://example.com/shared-extensions.json",
+  "isl.extensions.cacheTTL": 3600
+}
+```
+
+### Options
+
+- **URL**: Download from a web URL (e.g., `https://your-company.com/isl-extensions.json`)
+  - Content is cached for the duration specified by `isl.extensions.cacheTTL` (default: 1 hour)
+  - Automatically re-downloads when cache expires
+  - Falls back to cached content if download fails
+
+- **File Path**: Load from a local file
+  - Absolute path: `/path/to/extensions.json` or `C:\path\to\extensions.json`
+  - Relative to home: `~/isl-extensions.json` or `~/.config/isl/extensions.json`
+  - Relative paths are resolved from your home directory
+
+### Use Cases
+
+**Team/Company Shared Extensions:**
+```json
+{
+  "isl.extensions.source": "https://your-company.github.io/isl-extensions/shared.json"
+}
+```
+
+**Local Shared File:**
+```json
+{
+  "isl.extensions.source": "~/isl-extensions.json"
+}
+```
+
+**Project Override:**
+- Create a `.islextensions` file in your project root
+- It will automatically override the global source for that project
+- Perfect for project-specific extensions while still using shared ones
+
+### Cache Management
+
+- URL downloads are cached in memory for performance
+- Cache duration is controlled by `isl.extensions.cacheTTL` (in seconds)
+- Cache is cleared when configuration changes
+- If download fails, expired cache is used as fallback
+
+## Best Practices
+
+1. **Keep it organized**: Group related functions and modifiers together
+2. **Document thoroughly**: Add descriptions and examples - they appear in IDE tooltips
+3. **Use types**: Specify parameter and return types for better IntelliSense
+4. **Share globally**: Use `isl.extensions.source` to share extensions across all projects
+5. **Project overrides**: Use workspace-local `.islextensions` for project-specific extensions
+6. **Version control**: Commit `.islextensions` to your repository for project-specific extensions
+7. **Validate JSON**: Use a JSON validator to ensure your file is valid before saving
+
+## Troubleshooting
+
+### Extensions not showing up
+- Check that `.islextensions` is in the workspace root (if using workspace-local)
+- Verify global source is configured correctly in settings (if using global source)
+- Check that URL is accessible and returns valid JSON (if using URL)
+- Verify file path exists and is readable (if using file path)
+- Verify JSON syntax is valid (use a JSON validator)
+- Check Output panel (View > Output, select "ISL Language Support") for error messages
+- Reload VS Code window if needed
+
+### Global source not loading
+- Verify the URL is accessible (try opening in browser)
+- Check network connectivity if using URL
+- Verify file path is correct and file exists if using file path
+- Check cache TTL setting - may need to wait for cache to expire
+- Check Output panel for detailed error messages
+
+### Validation errors
+- Ensure `name` field is present for all functions/modifiers
+- Check that parameter names are valid identifiers
+- Verify no duplicate names
+
+### Changes not applying
+- The extension watches for file changes automatically
+- If changes don't apply, try reloading the window
+- Check the Output panel (View > Output, select "ISL Language Support") for error messages
+
+## Schema Support
+
+For better editing experience, you can add JSON schema validation by adding this at the top of your `.islextensions` file:
+
+```json
+{
+  "$schema": "https://json-schema.org/draft-07/schema#",
+  "functions": [
+    ...
+  ]
+}
+```
+
+(Note: A full schema definition will be provided in future versions)
+
+## Examples by Use Case
+
+### Custom Database Functions
+```json
+{
+  "functions": [
+    {
+      "name": "queryDB",
+      "description": "Executes a database query",
+      "parameters": [
+        {"name": "sql", "type": "String"},
+        {"name": "params", "type": "Array", "optional": true}
+      ],
+      "returns": {"type": "Array"},
+      "examples": ["$users: @.This.queryDB(\"SELECT * FROM users\");"]
+    }
+  ]
+}
+```
+
+### Custom Validation Modifiers
+```json
+{
+  "modifiers": [
+    {
+      "name": "validateEmail",
+      "description": "Validates email format",
+      "parameters": [],
+      "returns": {"type": "Boolean"},
+      "examples": ["if ($email | validateEmail) ... endif"]
+    }
+  ]
+}
+```
+
+### Custom Formatting Modifiers
+```json
+{
+  "modifiers": [
+    {
+      "name": "toCurrency",
+      "description": "Formats number as currency",
+      "parameters": [
+        {
+          "name": "currency",
+          "type": "String",
+          "optional": true,
+          "defaultValue": "USD"
+        }
+      ],
+      "returns": {"type": "String"},
+      "examples": [
+        "$price: $amount | toCurrency;",
+        "$price: $amount | toCurrency(\"EUR\");"
+      ]
+    }
+  ]
+}
+```
+
+## Future Enhancements
+
+Planned improvements:
+- JSON Schema for `.islextensions` validation
+- Support for importing external extension definitions
+- Multiple extension files support
+- Extension marketplace/sharing
+- Auto-generation from Java/Kotlin extension implementations
+
diff --git a/plugin/docs/ISL-LANGUAGE-GUIDE.md b/plugin/docs/ISL-LANGUAGE-GUIDE.md
new file mode 100644
index 0000000..96bb3ec
--- /dev/null
+++ b/plugin/docs/ISL-LANGUAGE-GUIDE.md
@@ -0,0 +1,485 @@
+# ISL Language Quick Reference
+
+This guide helps AI assistants and developers understand ISL (Intuitive Scripting Language) syntax and best practices.
+
+## Table of Contents
+1. [Quick Start](#quick-start)
+2. [Core Syntax](#core-syntax)
+3. [Built-in Modifiers](#built-in-modifiers)
+4. [Control Flow](#control-flow)
+5. [Common Patterns](#common-patterns)
+6. [Important Rules](#important-rules)
+
+## Quick Start
+
+Every ISL transformation starts with a `run` function:
+
+```isl
+fun run($input) {
+    // Transform $input here
+    userId: $input.id | to.string;
+    userName: `${$input.firstName} ${$input.lastName}`;
+    createdDate: $input.timestamp | date.parse("yyyy-MM-dd");
+}
+```
+
+## Core Syntax
+
+### Variables
+Variables start with `$` and can hold any JSON value:
+```isl
+$name: "John";
+$age: 30;
+$active: true;
+$address: { street: "123 Main St", city: "Boston" };
+$items: [1, 2, 3];
+```
+
+Access nested properties with dot notation:
+```isl
+$street: $user.address.street;
+$firstItem: $array[0];
+```
+
+### Functions
+Declare reusable logic with `fun`:
+```isl
+fun calculateTax($amount, $rate) {
+    return {{ $amount * $rate }};
+}
+
+// Call with @.This prefix
+$tax: @.This.calculateTax($subtotal, 0.08);
+```
+
+Functions **must always return a value**. Use `return {};` for empty returns.
+
+### Modifiers
+Transform data by piping values through modifiers:
+```isl
+$cleanName: $input.name | trim | capitalize;
+$ids: $items | map($.id) | unique;
+$adults: $users | filter($.age >= 18);
+```
+
+Create custom modifiers:
+```isl
+modifier formatCurrency($amount) {
+    return `\$${$amount | to.decimal | precision(2)}`;
+}
+
+$price: 99.9975 | formatCurrency;  // "$100.00"
+```
+
+### Math Expressions
+All math operations must be wrapped in `{{ }}`:
+```isl
+$total: {{ $price * $quantity }};
+$average: {{ ($sum / $count) }};
+$discount: {{ $amount * 0.1 }};
+```
+
+### String Interpolation
+Use backticks for string interpolation:
+```isl
+// Simple variable (no dots)
+$greeting: `Hello $firstName`;
+
+// Nested paths (has dots) - needs ${}
+$message: `User ${$user.name} logged in`;
+
+// Math expressions
+$summary: `Total: \${{ $price * $qty }}`;  // Escape $ for literal
+```
+
+### Objects
+Create objects with JSON-like syntax:
+```isl
+$user: {
+    id: $input.userId,
+    name: `${$input.first} ${$input.last}`,
+    active: true
+};
+```
+
+Use spread operator to merge objects:
+```isl
+$extended: {
+    ...$baseUser,
+    role: "admin",
+    permissions: ["read", "write"]
+};
+```
+
+Dynamic property names:
+```isl
+$key: "dynamicProp";
+$obj: { `$key`: "value" };  // { "dynamicProp": "value" }
+```
+
+## Built-in Modifiers
+
+### String Modifiers
+```isl
+$text | trim                      // Remove whitespace
+$text | trim("*")                 // Remove specific characters
+$text | upperCase                 // UPPERCASE
+$text | lowerCase                 // lowercase
+$text | capitalize                // Capitalize first letter
+$text | titleCase                 // Title Case Each Word
+$text | replace("old", "new")     // Replace text
+$text | split(",")                // Split into array
+$text | subString(0, 10)          // Extract substring
+$text | length                    // Get length
+$text | padStart(10, "0")         // Pad left
+$text | truncate(50, "...")       // Truncate with suffix
+$text | default("fallback")       // Default if empty/null
+```
+
+### Array Modifiers
+```isl
+$array | map(expression)          // Transform each element
+$array | filter(condition)        // Select matching elements
+$array | reduce(expression)       // Aggregate to single value
+$array | sort                     // Sort ascending
+$array | reverse                  // Reverse order
+$array | unique                   // Remove duplicates
+$array | first                    // Get first element
+$array | last                     // Get last element
+$array | at(2)                    // Get element at index
+$array | length                   // Get array length
+$array | isEmpty                  // Check if empty
+$array | push(item)               // Add item to end
+$array | pop                      // Remove last item
+$array | join(", ")               // Join to string
+```
+
+### Conversion Modifiers
+```isl
+$value | to.string               // Convert to string
+$value | to.number               // Convert to integer
+$value | to.decimal              // Convert to decimal
+$value | to.boolean              // Convert to boolean
+$value | to.array                // Convert to array
+$value | to.json                 // Serialize to JSON
+$value | to.xml("root")          // Convert to XML
+```
+
+### Math Modifiers
+```isl
+$num | absolute                  // Absolute value
+$num | negate                    // Negative value
+$num | round                     // Round to nearest
+$num | precision(2)              // Format decimal places
+$num | round.up(2)               // Round up
+$num | round.down(2)             // Round down
+```
+
+### Date/Time Modifiers
+```isl
+@.Date.Now()                                          // Current date/time
+$str | date.parse("yyyy-MM-dd")                       // Parse date string
+$date | date.add(1, "DAYS")                           // Add time unit
+$date | date.add(-2, "HOURS")                         // Subtract
+$date | date.part("YEAR")                             // Extract part
+$date | to.string("MM/dd/yyyy HH:mm:ss")              // Format date
+$date | to.number                                     // Unix epoch (seconds)
+$date | to.epochmillis                                // Epoch milliseconds
+$epochSecs | date.fromEpochSeconds                    // Parse epoch
+```
+
+### Object Modifiers
+```isl
+$obj | keys                      // Get property names
+$obj | kv                        // Key-value pairs array
+$obj | select("path.to.prop")    // Extract nested value
+$obj | delete("propName")        // Remove property
+$obj | getProperty("Name")       // Case-insensitive get
+```
+
+### Data Format Modifiers
+```isl
+$str | json.parse                // Parse JSON string
+$str | xml.parse                 // Parse XML string
+$str | yaml.parse                // Parse YAML string
+$str | csv.parsemultiline        // Parse CSV data
+$bytes | encode.base64           // Base64 encode
+$str | decode.base64             // Base64 decode
+$str | to.hex                    // Convert to hex
+$hex | from.hex                  // Convert from hex
+```
+
+### Cryptographic Modifiers
+```isl
+$data | crypto.sha256 | to.hex   // SHA-256 hash
+$data | crypto.sha512 | to.hex   // SHA-512 hash
+$data | crypto.md5 | to.hex      // MD5 hash
+$msg | crypto.hmacsha256($key) | encode.base64  // HMAC-SHA256
+```
+
+## Control Flow
+
+### If/Else
+```isl
+// Expression form (simple condition only)
+$status: if ($paid) "success" else "pending" endif;
+$discount: if ($amount > 100) 0.1 endif;  // else is optional
+
+// Statement form (supports complex logic)
+if ($user.role == "admin" and $user.active)
+    $permissions: ["read", "write", "delete"];
+else
+    $permissions: ["read"];
+endif
+```
+
+### Switch/Case
+```isl
+$message: switch ($statusCode)
+    200 -> "OK";
+    404 -> "Not Found";
+    /^5\d\d/ -> "Server Error";        // Regex match
+    < 300 -> "Success";                // Comparison
+    contains "Error" -> "Failed";      // String contains
+    in [401, 403] -> "Unauthorized";   // Array membership
+    else -> "Unknown";
+endswitch
+```
+
+### Foreach Loop
+```isl
+// Basic foreach
+$doubled: foreach $n in $numbers
+    {{ $n * 2 }}
+endfor
+
+// Transform objects
+$users: foreach $user in $input.users
+    {
+        id: $user.id,
+        name: `${$user.first} ${$user.last}`,
+        index: $userIndex  // Automatic index variable
+    }
+endfor
+
+// With filter
+foreach $item in $items | filter($.price > 100)
+    // Process expensive items only
+endfor
+```
+
+### While Loop
+```isl
+$i: 0;
+$sum: 0;
+while ($i < 10)
+    $sum: {{ $sum + $i }};
+    $i: {{ $i + 1 }};
+endwhile
+```
+
+### Coalesce Operator
+Return first non-null, non-empty value:
+```isl
+$name: $user.preferredName ?? $user.firstName ?? "Guest";
+$email: $contact.email ?? $contact.alternateEmail ?? "";
+```
+
+## Common Patterns
+
+### Transform Array of Objects
+```isl
+items: $input.lineItems | map({
+    productId: $.product.id | to.string,
+    name: $.product.name | trim | capitalize,
+    quantity: $.qty | to.number,
+    price: $.unitPrice | to.decimal | precision(2),
+    total: {{ $.qty * $.unitPrice }} | precision(2)
+})
+```
+
+### Filter and Aggregate
+```isl
+// Get expensive items total
+$expensiveTotal: $items 
+    | filter($.price > 100)
+    | map($.price)
+    | reduce({{ $acc + $it }});
+
+// Count active users
+$activeCount: $users | filter($.active) | length;
+```
+
+### Conditional Object Properties
+```isl
+{
+    id: $input.id,
+    name: $input.name,
+    // Include email only if present
+    ...if ($input.email) { email: $input.email } else {} endif,
+    // Include address with default
+    address: $input.address ?? "N/A"
+}
+```
+
+### Complex Nested Transformation
+```isl
+fun run($input) {
+    orderId: $input.order.id | to.string;
+    customer: @.This.transformCustomer($input.customer);
+    items: $input.order.items | map(@.This.transformItem($));
+    summary: @.This.calculateSummary($input.order.items);
+}
+
+fun transformCustomer($cust) {
+    return {
+        id: $cust.id | to.string,
+        fullName: `${$cust.firstName} ${$cust.lastName}`,
+        email: $cust.email | lowerCase | trim
+    };
+}
+
+fun transformItem($item) {
+    return {
+        id: $.id,
+        name: $.title | capitalize,
+        price: $.price | to.decimal | precision(2)
+    };
+}
+
+fun calculateSummary($items) {
+    $subtotal: $items 
+        | map({{ $.price * $.quantity }})
+        | reduce({{ $acc + $it }});
+    
+    return {
+        subtotal: $subtotal | precision(2),
+        tax: {{ $subtotal * 0.08 }} | precision(2),
+        total: {{ $subtotal * 1.08 }} | precision(2)
+    };
+}
+```
+
+## Important Rules
+
+### ✅ Do This
+
+1. **Always wrap math in `{{ }}`**
+   ```isl
+   $total: {{ $price * $quantity }};
+   ```
+
+2. **Use modifiers without parentheses in conditions**
+   ```isl
+   if ($text | length > 5) ... endif
+   ```
+
+3. **Return a value from functions**
+   ```isl
+   return $result;
+   return {};  // For empty return
+   ```
+
+4. **Use `$` in foreach array without parentheses**
+   ```isl
+   foreach $item in $array | filter($.active)
+   ```
+
+5. **Proper spacing for readability**
+   ```isl
+   $result: $items | map( $.id ) | filter( $ > 100 );
+   ```
+
+6. **Modifiers inside math expressions are valid**
+    ```isl
+    $result = {{ $items | length + 5 }}
+    ```
+
+7. **if conditions returning true or false as valid**
+    Because ISL has a true-ish/false-ish condition this is valid `return $if( $value ) true else false` (or `!$value` for the negative).
+    This will test if `$value` is true-ish (e.g. not null, empty, zero or false)
+    and return a boolean true or false. 
+
+### ❌ Don't Do This
+
+1. **Don't use naked math expressions**
+   ```isl
+   ❌ $total: $price * $quantity;
+   ✅ $total: {{ $price * $quantity }};
+   ```
+
+2. **Don't wrap modifiers in conditions**
+   ```isl
+   ❌ if (($text | length) > 5) ... endif
+   ✅ if ($text | length > 5) ... endif
+   ```
+
+3. **Don't return without value**
+   ```isl
+   ❌ return;
+   ✅ return {};
+   ```
+
+4. **Don't access properties after modifiers**
+   ```isl
+   ❌ $id: ($items | last).id;
+   ✅ $last: $items | last; $id: $last.id;
+   ```
+
+5. **Don't use JavaScript/Python syntax**
+   ```isl
+   ❌ const user = input.name;
+   ✅ $user: $input.name;
+   
+   ❌ items.map(x => x.id)
+   ✅ $items | map($.id)
+   ```
+
+6. **Don't complain about null access**
+    This syntax `$val = $object.property.property.property` is perfectly valid. 
+    ISL does not suffer from null references if a property in a chain is missing.
+    ISL will simply make $val null if any of the properties in the chain are null.
+
+## File Structure
+
+Standard ISL file structure:
+
+```isl
+// 1. Imports (optional)
+import Utils from 'utils.isl';
+
+// 2. Main entry point (REQUIRED)
+fun run($input) {
+    // Primary transformation logic
+}
+
+// 3. Helper functions
+fun helperFunction($param) {
+    return $result;
+}
+
+// 4. Custom modifiers
+modifier customModifier($value, $param) {
+    return $transformed;
+}
+```
+
+## Learn More
+
+- **Critical that you read this AI-Specific Guide**: https://intuit.github.io/isl/ai/
+- **Full Documentation**: https://intuit.github.io/isl/
+- **Language Reference**: https://intuit.github.io/isl/language/
+- **Complete Modifiers List**: https://intuit.github.io/isl/language/modifiers/
+- **CLI Documentation**: https://intuit.github.io/isl/cli/
+- **Examples**: https://intuit.github.io/isl/examples/
+
+## Getting Help
+
+When working with ISL:
+1. Start with the `fun run($input)` pattern
+2. Break complex logic into helper functions
+3. Use modifiers for data transformations
+4. Check the documentation for available modifiers
+5. Follow the syntax rules strictly
+6. Test incrementally using the CLI or Run button
+
diff --git a/plugin/docs/MODIFIERS.md b/plugin/docs/MODIFIERS.md
new file mode 100644
index 0000000..cb8b8a5
--- /dev/null
+++ b/plugin/docs/MODIFIERS.md
@@ -0,0 +1,256 @@
+# ISL Modifiers Reference
+
+This document lists all built-in modifiers available in ISL.
+
+## String Modifiers
+
+### Trimming
+- `trim` - Trim whitespace from both sides
+- `trimStart` - Trim whitespace from start
+- `trimEnd` - Trim whitespace from end
+
+### Case Conversion
+- `upperCase` - Convert to uppercase
+- `lowerCase` - Convert to lowercase
+- `capitalize` - Capitalize first letter
+- `titleCase` - Convert to title case (capitalize each word)
+- `camelCase` - Convert to camelCase
+- `snakeCase` - Convert to snake_case
+
+### Substring Operations
+- `left(n)` - Get first n characters
+- `right(n)` - Get last n characters
+- `cap(n)` - Cap string at length (alias for left)
+- `substring(start, end)` - Get substring
+- `substringUpto(delimiter)` - Get substring up to delimiter
+- `substringAfter(delimiter)` - Get substring after delimiter
+
+### String Manipulation
+- `replace(find, replace)` - Replace all occurrences
+- `remove(text)` - Remove substring
+- `concat(other, delimiter)` - Concatenate with delimiter
+- `append(values...)` - Append multiple strings
+- `split(delimiter)` - Split into array
+- `truncate(length, suffix)` - Truncate with suffix (default "...")
+- `padStart(length, char)` - Pad at start
+- `padEnd(length, char)` - Pad at end
+- `reverse` - Reverse string
+
+### HTML
+- `html.escape` - Escape HTML entities
+- `html.unescape` - Unescape HTML entities
+
+### CSV
+- `csv.parsemultiline` - Parse CSV string
+- `csv.findrow` - Find specific row in CSV
+
+### Other
+- `sanitizeTid` - Sanitize/validate UUID/TID
+
+## Array Modifiers
+
+### High-Order Functions
+- `filter(condition)` - Filter array by condition (use $fit or $ for current item)
+- `map(expression)` - Transform each element (use $ for current item)
+- `reduce(expression, initial)` - Reduce to single value (use $acc and $it)
+
+### Element Access
+- `first` - Get first element
+- `last` - Get last element
+- `at(index)` - Get element at index (supports negative indices)
+- `take(n)` - Take first n elements
+- `drop(n)` - Drop first n elements
+
+### Search
+- `indexOf(element)` - Find index of element
+- `lastIndexOf(element)` - Find last index of element
+
+### Manipulation
+- `push(item)` - Add item to end
+- `pop` - Remove last item
+- `pushItems(array)` - Append entire array
+- `slice(start, end)` - Extract portion
+- `chunk(size)` - Split into chunks of size
+
+### Transformation
+- `sort` - Sort array
+- `reverse` - Reverse array
+- `unique` - Get unique values
+
+### Checks
+- `isEmpty` - Check if empty
+- `isNotEmpty` - Check if not empty
+- `length` - Get length
+
+## Object Modifiers
+
+### Property Access
+- `keys` - Get object keys as array
+- `kv` - Get key-value pairs as array of {key, value}
+- `getProperty(name)` - Get property (case-insensitive)
+- `setProperty(name, value)` - Set property
+- `has(key)` - Check if object has key
+- `delete(property)` - Delete property
+
+### Object Operations
+- `sort` - Sort object by keys
+- `select(path)` - Select nested property by JSON path
+- `merge(other)` - Merge with another object
+- `pick(keys...)` - Pick specific properties
+- `omit(keys...)` - Omit specific properties
+- `rename(oldName, newName)` - Rename property
+- `default(value)` - Return default if null/empty
+
+## Math Modifiers
+
+### Array Math (use with arrays)
+- `Math.sum(initial)` - Sum all values
+- `Math.average` - Average of values
+- `Math.mean` - Mean of values
+- `Math.min` - Minimum value
+- `Math.max` - Maximum value
+
+### Numeric Operations
+- `Math.mod(divisor)` - Modulo operation
+- `Math.sqrt` - Square root
+- `Math.round` - Round to nearest integer
+- `Math.floor` - Round down
+- `Math.ceil` - Round up
+- `Math.abs` - Absolute value
+
+### Number Modifiers
+- `negate` - Negate number
+- `absolute` - Absolute value
+- `precision(digits)` - Set decimal precision
+- `round.up` - Round up
+- `round.down` - Round down
+- `round.half` - Round half
+
+### Random Numbers
+- `Math.RandInt(min, max)` - Random integer
+- `Math.RandFloat()` - Random float
+- `Math.RandDouble()` - Random double
+
+## Type Conversion Modifiers
+
+### Basic Types
+- `to.string` - Convert to string
+- `to.number` - Convert to number
+- `to.decimal` - Convert to decimal
+- `to.boolean` - Convert to boolean
+- `to.array` - Convert to array
+- `to.object` - Convert to object
+
+### Format Conversions
+- `to.json` - Convert to JSON string
+- `to.yaml` - Convert to YAML string
+- `to.csv` - Convert to CSV string
+- `to.xml(rootName)` - Convert to XML
+- `to.hex` - Convert to hex string
+- `to.bytes` - Convert to byte array
+- `to.epochmillis` - Convert date to epoch milliseconds
+
+### Hex Conversion
+- `hex.tobinary` - Convert hex string to binary
+
+### Join Operations
+- `join.string(delimiter)` - Join array to string
+- `join.path(delimiter)` - Join with URL path encoding
+- `join.query(delimiter)` - Join with URL query encoding
+
+## Parsing Modifiers
+
+### Data Formats
+- `json.parse` - Parse JSON string
+- `yaml.parse` - Parse YAML string
+- `xml.parse` - Parse XML string
+
+### Email
+- `email.parse` - Parse email addresses
+
+## Encoding Modifiers
+
+### Base64
+- `encode.base64` - Encode to Base64
+- `encode.base64url` - Encode to Base64 URL-safe
+- `decode.base64` - Decode from Base64
+- `decode.base64url` - Decode from Base64 URL-safe
+
+### URL Encoding
+- `encode.path` - URL path encoding
+- `encode.query` - URL query encoding
+- `decode.query` - URL query decoding
+
+## Compression Modifiers
+
+- `gzip` - GZip compress
+- `gunzip` - GZip decompress to string
+- `gunzipToByte` - GZip decompress to byte array
+
+## Regex Modifiers
+
+- `regex.find(pattern)` - Find all matches
+- `regex.matches(pattern)` - Test if matches
+- `regex.replace(pattern, replacement)` - Replace all matches
+- `regex.replacefirst(pattern, replacement)` - Replace first match
+
+Options can be passed as third parameter: `{ignoreCase: true, multiLine: true, comments: true}`
+
+## Type Checking
+
+- `typeof` - Get type of value (returns: string, number, boolean, array, object, null, etc.)
+
+## Legacy/Common Modifiers
+
+- `contains(value)` - Check if string contains substring
+- `startsWith(prefix)` - Check if starts with
+- `endsWith(suffix)` - Check if ends with
+
+## Date Modifiers
+
+- `date.parse(format)` - Parse date string
+- `date.add(value, unit)` - Add to date (units: YEARS, MONTHS, DAYS, HOURS, MINUTES, SECONDS)
+- `date.part(part)` - Get date part (YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DAYOFYEAR, DAYOFWEEK)
+- `date.fromEpochSeconds` - Create date from epoch seconds
+- `date.fromEpochMillis` - Create date from epoch milliseconds
+
+## Static Functions (use with @.)
+
+### Math Functions
+- `@.Math.min(values...)` - Minimum of values
+- `@.Math.max(values...)` - Maximum of values
+- `@.Math.mean(values...)` - Mean of values
+- `@.Math.mod(value, divisor)` - Modulo operation
+- `@.Math.sqrt(value)` - Square root
+- `@.Math.RandInt(min, max)` - Random integer
+- `@.Math.RandFloat()` - Random float
+- `@.Math.RandDouble()` - Random double
+
+### Array Functions
+- `@.Array.range(start, count, increment)` - Create numeric range array
+
+### Date Functions
+- `@.Date.now()` - Current date/time
+- `@.Date.parse(string, format)` - Parse date
+- `@.Date.fromEpochSeconds(seconds)` - From epoch seconds
+- `@.Date.fromEpochMillis(millis)` - From epoch milliseconds
+
+## Special Variables in Modifiers
+
+### filter
+- `$fit` or `$` - Current item being filtered
+
+### map
+- `$` - Current item being mapped
+
+### reduce
+- `$acc` - Accumulator
+- `$it` - Current item
+
+### foreach
+- `$item` - Loop variable
+- `$itemIndex` - Zero-based index
+
+### retry
+- `$` - Current value being tested
+
diff --git a/plugin/esbuild.js b/plugin/esbuild.js
new file mode 100644
index 0000000..c6b1a6a
--- /dev/null
+++ b/plugin/esbuild.js
@@ -0,0 +1,30 @@
+const esbuild = require('esbuild');
+
+const production = process.argv.includes('--production');
+const watch = process.argv.includes('--watch');
+
+async function main() {
+  const ctx = await esbuild.context({
+    entryPoints: ['src/extension.ts'],
+    bundle: true,
+    format: 'cjs',
+    minify: production,
+    sourcemap: !production,
+    platform: 'node',
+    outfile: 'out/extension.js',
+    external: ['vscode'],
+    logLevel: 'info',
+  });
+
+  if (watch) {
+    await ctx.watch();
+  } else {
+    await ctx.rebuild();
+    await ctx.dispose();
+  }
+}
+
+main().catch((e) => {
+  console.error(e);
+  process.exit(1);
+});
diff --git a/plugin/images/README.md b/plugin/images/README.md
new file mode 100644
index 0000000..a4cc43f
--- /dev/null
+++ b/plugin/images/README.md
@@ -0,0 +1,25 @@
+# Plugin Images
+
+## icon.png
+
+The official ISL logo has been copied from `../../docs/img/isl_small.png`.
+
+**For VS Code Marketplace**: The icon should be exactly **128x128 pixels**. 
+
+To verify and resize if needed, see `../ICON-SETUP.md` which includes:
+- Scripts for automatic resizing (PowerShell & Bash)
+- Manual resize instructions
+- Online tool recommendations
+- Image editor steps
+
+## file-icon.svg
+
+SVG icon used for `.isl` file type in the VS Code file explorer.
+
+This is a simplified vector version of the ISL branding with:
+- ISL text
+- Decorative brackets
+- Blue gradient background
+
+The SVG format ensures it scales well at all sizes in the VS Code UI.
+
diff --git a/plugin/images/file-icon.svg b/plugin/images/file-icon.svg
new file mode 100644
index 0000000..c5ff0ef
--- /dev/null
+++ b/plugin/images/file-icon.svg
@@ -0,0 +1,22 @@
+
+  
+  
+  
+  
+  
+  
+  
+  
+  
+  
+  
+
diff --git a/plugin/images/icon.png b/plugin/images/icon.png
new file mode 100644
index 0000000..f58217b
Binary files /dev/null and b/plugin/images/icon.png differ
diff --git a/plugin/isl-language.json b/plugin/isl-language.json
new file mode 100644
index 0000000..8b4295a
--- /dev/null
+++ b/plugin/isl-language.json
@@ -0,0 +1,268 @@
+{
+  "modifierValidationPatterns": [
+    "to.*",
+    "hex.*",
+    "join.*",
+    "email.*",
+    "encode.*",
+    "decode.*",
+    "json.*",
+    "yaml.*",
+    "xml.*",
+    "csv.*",
+    "regex.*",
+    "date.*",
+    "round.*",
+    "html.*",
+    "Crypto.*",
+    "zip.*",
+    "retry.*",
+    "Math.*"
+  ],
+  "modifiers": [
+    {"name": "to.string", "detail": "Convert to string", "insertText": "to.string", "returns": {"type": "String"}, "documentation": "Converts value to string.\n\nFor dates: `to.string(format)`", "signature": {"label": "to.string(format?: string)", "parameters": [{"label": "format", "documentation": "For dates: format pattern (e.g., \"yyyy-MM-dd HH:mm:ss\")"}], "documentation": "Converts value to string. For dates, optional format parameter."}, "hover": {"description": "Converts value to string. Also used for date formatting.", "signature": "to.string or to.string(format) for dates", "example": "$id: $num | to.string;\n$date: $timestamp | to.string(\"yyyy-MM-dd\");"}},
+    {"name": "to.number", "detail": "Convert to number", "insertText": "to.number", "returns": {"type": "Number"}, "hover": {"description": "Converts value to integer number.", "signature": "to.number", "example": "$count: $input.count | to.number;"}},
+    {"name": "to.decimal", "detail": "Convert to decimal", "insertText": "to.decimal", "returns": {"type": "Number"}, "hover": {"description": "Converts value to decimal number.", "signature": "to.decimal", "example": "$price: $input.price | to.decimal;"}},
+    {"name": "to.boolean", "detail": "Convert to boolean", "insertText": "to.boolean", "returns": {"type": "Boolean"}, "hover": {"description": "Converts value to boolean.", "signature": "to.boolean", "example": "$active: $input.active | to.boolean;"}},
+    {"name": "to.array", "detail": "Convert to array", "insertText": "to.array", "returns": {"type": "Array"}, "hover": {"description": "Converts value to array.", "signature": "to.array", "example": "$items: $single | to.array;"}},
+    {"name": "to.object", "detail": "Convert to object", "insertText": "to.object", "returns": {"type": "Object"}},
+    {"name": "to.json", "detail": "Convert to JSON string", "insertText": "to.json", "returns": {"type": "String"}, "hover": {"description": "Converts object to JSON string.", "signature": "to.json", "example": "$jsonStr: $object | to.json;"}},
+    {"name": "to.yaml", "detail": "Convert to YAML string", "insertText": "to.yaml"},
+    {"name": "to.csv", "detail": "Convert to CSV string", "insertText": "to.csv"},
+    {"name": "to.xml", "detail": "Convert to XML", "insertText": "to.xml(\"${1:root}\")", "signature": {"label": "to.xml(rootName: string)", "parameters": [{"label": "rootName", "documentation": "Root element name for XML"}], "documentation": "Converts object to XML string"}, "hover": {"description": "Converts object to XML string.", "signature": "to.xml(rootName)", "example": "$xml: $object | to.xml(\"root\");"}},
+    {"name": "to.hex", "detail": "Convert to hex string", "insertText": "to.hex"},
+    {"name": "to.bytes", "detail": "Convert to byte array", "insertText": "to.bytes(\"${1:UTF-8}\")", "signature": {"label": "to.bytes(charset?: string)", "parameters": [{"label": "charset", "documentation": "Character set (default UTF-8)"}], "documentation": "Converts string to byte array"}, "hover": {"description": "Converts value to byte array.", "signature": "to.bytes(charset?)", "example": "$bytes: $str | to.bytes;"}},
+    {"name": "to.epochmillis", "detail": "Convert date to epoch milliseconds", "insertText": "to.epochmillis", "hover": {"description": "Converts date to epoch milliseconds.", "signature": "to.epochmillis", "example": "$timestamp: $date | to.epochmillis;"}},
+    {"name": "trim", "detail": "Trim whitespace", "insertText": "trim", "returns": {"type": "String"}, "hover": {"description": "Removes leading and trailing whitespace from a string.", "signature": "trim", "example": "$name: $input.name | trim;"}},
+    {"name": "trimStart", "detail": "Trim start whitespace", "insertText": "trimStart"},
+    {"name": "trimEnd", "detail": "Trim end whitespace", "insertText": "trimEnd"},
+    {"name": "upperCase", "detail": "Convert to uppercase", "insertText": "upperCase", "hover": {"description": "Converts a string to uppercase.", "signature": "upperCase", "example": "$code: $input.status | upperCase;"}},
+    {"name": "lowerCase", "detail": "Convert to lowercase", "insertText": "lowerCase", "hover": {"description": "Converts a string to lowercase.", "signature": "lowerCase", "example": "$email: $input.email | lowerCase;"}},
+    {"name": "capitalize", "detail": "Capitalize first letter", "insertText": "capitalize", "hover": {"description": "Capitalizes the first letter of a string.", "signature": "capitalize", "example": "$name: \"john\" | capitalize;"}},
+    {"name": "titleCase", "detail": "Convert to title case", "insertText": "titleCase", "hover": {"description": "Converts a string to title case (capitalizes each word).", "signature": "titleCase", "example": "$title: \"hello world\" | titleCase;"}},
+    {"name": "camelCase", "detail": "Convert to camelCase", "insertText": "camelCase"},
+    {"name": "snakeCase", "detail": "Convert to snake_case", "insertText": "snakeCase"},
+    {"name": "left", "detail": "Get left N characters", "insertText": "left(${1:length})"},
+    {"name": "right", "detail": "Get right N characters", "insertText": "right(${1:length})"},
+    {"name": "cap", "detail": "Cap string at length (alias for left)", "insertText": "cap(${1:length})"},
+    {"name": "split", "detail": "Split string", "insertText": "split(\"${1:,}\")", "returns": {"type": "Array"}, "signature": {"label": "split(delimiter: string)", "parameters": [{"label": "delimiter", "documentation": "The string to split on (e.g., \",\", \" \")"}], "documentation": "Splits a string into an array using the delimiter"}, "hover": {"description": "Splits a string into an array using a delimiter.", "signature": "split(delimiter)", "example": "$tags: \"red,blue,green\" | split(\",\");"}},
+    {"name": "replace", "detail": "Replace string", "insertText": "replace(\"${1:find}\", \"${2:replace}\")", "returns": {"type": "String"}, "signature": {"label": "replace(find: string, replaceWith: string)", "parameters": [{"label": "find", "documentation": "The string to find"}, {"label": "replaceWith", "documentation": "The replacement string"}], "documentation": "Replaces all occurrences of find with replaceWith"}, "hover": {"description": "Replaces occurrences of a substring with another string.", "signature": "replace(find, replaceWith)", "example": "$text: $input | replace(\"old\", \"new\");"}},
+    {"name": "remove", "detail": "Remove substring", "insertText": "remove(\"${1:text}\")"},
+    {"name": "substring", "detail": "Get substring", "insertText": "substring(${1:start}, ${2:end})", "returns": {"type": "String"}, "signature": {"label": "substring(start: number, end: number)", "parameters": [{"label": "start", "documentation": "Starting index (0-based)"}, {"label": "end", "documentation": "Ending index (exclusive)"}], "documentation": "Extracts a portion of the string"}, "hover": {"description": "Extracts a portion of a string.", "signature": "substring(start, end)", "example": "$code: $input | substring(0, 5);"}},
+    {"name": "substringUpto", "detail": "Substring up to delimiter", "insertText": "substringUpto(\"${1:delimiter}\")"},
+    {"name": "substringAfter", "detail": "Substring after delimiter", "insertText": "substringAfter(\"${1:delimiter}\")"},
+    {"name": "truncate", "detail": "Truncate string", "insertText": "truncate(${1:length}, \"${2:...}\")", "signature": {"label": "truncate(maxLength: number, suffix: string)", "parameters": [{"label": "maxLength", "documentation": "Maximum length of the string"}, {"label": "suffix", "documentation": "Suffix to add if truncated (e.g., \"...\")"}], "documentation": "Truncates string to maxLength and adds suffix if needed"}, "hover": {"description": "Truncates a string to a maximum length with optional suffix.", "signature": "truncate(maxLength, suffix)", "example": "$short: $longText | truncate(100, \"...\");"}},
+    {"name": "padStart", "detail": "Pad start", "insertText": "padStart(${1:length}, \"${2: }\")", "signature": {"label": "padStart(length: number, padString: string)", "parameters": [{"label": "length", "documentation": "Target length of the string"}, {"label": "padString", "documentation": "String to pad with (e.g., \"0\", \" \")"}], "documentation": "Pads the start of the string to reach target length"}, "hover": {"description": "Pads the start of a string to a target length.", "signature": "padStart(length, padString)", "example": "$id: $number | to.string | padStart(8, \"0\");"}},
+    {"name": "padEnd", "detail": "Pad end", "insertText": "padEnd(${1:length}, \"${2: }\")", "signature": {"label": "padEnd(length: number, padString: string)", "parameters": [{"label": "length", "documentation": "Target length of the string"}, {"label": "padString", "documentation": "String to pad with"}], "documentation": "Pads the end of the string to reach target length"}, "hover": {"description": "Pads the end of a string to a target length.", "signature": "padEnd(length, padString)", "example": "$code: $text | padEnd(10, \" \");"}},
+    {"name": "concat", "detail": "Concatenate strings", "insertText": "concat(${1:\\$other}, \"${2:delimiter}\")"},
+    {"name": "append", "detail": "Append strings", "insertText": "append(${1:\\$value})"},
+    {"name": "reverse", "detail": "Reverse string/array", "insertText": "reverse", "hover": {"description": "Reverses the order of elements in an array.", "signature": "reverse", "example": "$reversed: $array | reverse;"}},
+    {"name": "sanitizeTid", "detail": "Sanitize UUID/TID", "insertText": "sanitizeTid"},
+    {"name": "filter", "detail": "Filter array", "insertText": "filter(${1:\\$.${2:condition}})", "returns": {"type": "Array"}, "documentation": "Filters array based on condition.\n\nUse $fit or $ for current item being filtered.\n\nExample: `$active: $items | filter($fit.active)`", "signature": {"label": "filter(condition: expression)", "parameters": [{"label": "condition", "documentation": "Boolean expression using $ or $item for current element"}], "documentation": "Filters array elements based on condition. Use $ for current item."}, "hover": {"description": "Filters an array based on a condition. Use $ or $it for current item.", "signature": "filter(condition)", "example": "$active: $items | filter( $.status == \"active\" );"}},
+    {"name": "map", "detail": "Map array", "insertText": "map(${1:\\$.${2:property}})", "returns": {"type": "Array"}, "documentation": "Transforms each element.\n\nUse $ for current item.\n\nExample: `$names: $users | map($.name)`", "signature": {"label": "map(expression: any)", "parameters": [{"label": "expression", "documentation": "Transform expression using $ or $item for current element"}], "documentation": "Transforms each array element. Use $ for current item."}, "hover": {"description": "Transforms each element of an array. Use $ or $it for current item.", "signature": "map(expression)", "example": "$names: $users | map($.name);"}},
+    {"name": "reduce", "detail": "Reduce array", "insertText": "reduce({{ \\$acc + \\$it }}, ${1:0})", "returns": {"type": "Any"}, "documentation": "Reduces array to single value.\n\nUse $acc (accumulator) and $it (current item).\n\nExample: `$sum: $numbers | reduce({{ $acc + $it }}, 0)`", "signature": {"label": "reduce(expression: any, initialValue: any)", "parameters": [{"label": "expression", "documentation": "Accumulator expression using $acc and $it"}, {"label": "initialValue", "documentation": "Initial value for the accumulator"}], "documentation": "Reduces array to single value. Use $acc (accumulator) and $it (current item)."}, "hover": {"description": "Reduces an array to a single value. Use $acc for accumulator, $it for current item.", "signature": "reduce(expression, initialValue)", "example": "$sum: [1, 2, 3] | reduce({{ $acc + $it }}, 0);"}},
+    {"name": "sort", "detail": "Sort array/object", "insertText": "sort", "signature": {"label": "sort(options?: object)", "parameters": [{"label": "options", "documentation": "Optional {by: \"field\", order: \"asc\"|\"desc\", caseSensitive: true|false}"}], "documentation": "Sorts array or object; for arrays use by to sort by field"}, "hover": {"description": "Sorts array or object. Options: by (field), order (asc/desc), caseSensitive.", "signature": "sort(options?)", "example": "$sorted: $numbers | sort;\n$byName: $users | sort({by: \"name\", order: \"asc\"});"}},
+    {"name": "unique", "detail": "Get unique values", "insertText": "unique", "signature": {"label": "unique(path?: string)", "parameters": [{"label": "path", "documentation": "Optional JSON path for uniqueness (e.g. $.id)"}], "documentation": "Unique by value or by path"}, "hover": {"description": "Returns unique values from an array. Optional path for uniqueness by field.", "signature": "unique(path?)", "example": "$uniqueTags: $tags | unique;\n$uniqueById: $items | unique($.id);"}},
+    {"name": "slice", "detail": "Slice array", "insertText": "slice(${1:start}, ${2:end})"},
+    {"name": "length", "detail": "Get length", "insertText": "length", "returns": {"type": "Number"}, "hover": {"description": "Returns the length of a string or array.", "signature": "length", "example": "$count: $array | length;"}},
+    {"name": "first", "detail": "Get first element", "insertText": "first", "hover": {"description": "Returns the first element of an array.", "signature": "first", "example": "$firstItem: $array | first;"}},
+    {"name": "last", "detail": "Get last element", "insertText": "last", "hover": {"description": "Returns the last element of an array.", "signature": "last", "example": "$lastItem: $array | last;"}},
+    {"name": "take", "detail": "Take first N elements", "insertText": "take(${1:n})"},
+    {"name": "drop", "detail": "Drop first N elements", "insertText": "drop(${1:n})"},
+    {"name": "at", "detail": "Get element at index", "insertText": "at(${1:index})", "documentation": "Supports negative indices: `at(-1)` gets last element", "signature": {"label": "at(index: number)", "parameters": [{"label": "index", "documentation": "Array index (supports negative for counting from end)"}], "documentation": "Returns element at index. Use -1 for last element."}, "hover": {"description": "Returns the element at a specific index (supports negative indices).", "signature": "at(index)", "example": "$second: $array | at(1);\n$lastItem: $array | at(-1);"}},
+    {"name": "indexOf", "detail": "Find index of element", "insertText": "indexOf(${1:element})"},
+    {"name": "lastIndexOf", "detail": "Find last index of element", "insertText": "lastIndexOf(${1:element})"},
+    {"name": "isEmpty", "detail": "Check if empty", "insertText": "isEmpty", "returns": {"type": "Boolean"}, "hover": {"description": "Returns true if array or string is empty.", "signature": "isEmpty", "example": "if ($array | isEmpty) ... endif"}},
+    {"name": "isNotEmpty", "detail": "Check if not empty", "insertText": "isNotEmpty", "returns": {"type": "Boolean"}, "hover": {"description": "Returns true if array or string is not empty.", "signature": "isNotEmpty", "example": "if ($array | isNotEmpty) ... endif"}},
+    {"name": "push", "detail": "Add item to array", "insertText": "push(${1:item})", "returns": {"type": "Array"}, "signature": {"label": "push(item: any)", "parameters": [{"label": "item", "documentation": "Item to add to array"}], "documentation": "Adds item to end of array"}, "hover": {"description": "Adds an item to the end of an array.", "signature": "push(item)", "example": "$newArray: $array | push($newItem);"}},
+    {"name": "pushItems", "detail": "Push full array to end", "insertText": "pushItems(${1:\\$otherArray})", "documentation": "Appends all items from the provided array to the end of the input array.\n\nExample: `$items | pushItems($moreItems)`"},
+    {"name": "pop", "detail": "Remove last item", "insertText": "pop", "hover": {"description": "Removes and returns the last element from an array.", "signature": "pop", "example": "$item: $array | pop;"}},
+    {"name": "chunk", "detail": "Split into chunks", "insertText": "chunk(${1:size})"},
+    {"name": "date.parse", "detail": "Parse date", "insertText": "date.parse(\"${1:yyyy-MM-dd}\")", "returns": {"type": "Date"}, "documentation": "Parses date string.\n\nOptional locale: `date.parse(format, {locale: \"en_US\"})`", "signature": {"label": "date.parse(format: string, options?: object)", "parameters": [{"label": "format", "documentation": "Date format pattern (e.g., \"yyyy-MM-dd\")"}, {"label": "options", "documentation": "Optional: {locale: \"en_US\"}"}], "documentation": "Parses date string using Java DateTimeFormatter patterns"}, "hover": {"description": "Parses a date string with the given format.", "signature": "date.parse(format, {locale})", "example": "$date: \"2024-01-15\" | date.parse(\"yyyy-MM-dd\");"}},
+    {"name": "date.add", "detail": "Add to date", "insertText": "date.add(${1:value}, \"${2:DAYS}\")", "returns": {"type": "Date"}, "documentation": "Units: YEARS, MONTHS, DAYS, HOURS, MINUTES, SECONDS", "signature": {"label": "date.add(amount: number, unit: string)", "parameters": [{"label": "amount", "documentation": "Amount to add (can be negative)"}, {"label": "unit", "documentation": "Time unit: YEARS, MONTHS, DAYS, HOURS, MINUTES, SECONDS"}], "documentation": "Adds time to a date"}, "hover": {"description": "Adds time to a date. Units: YEARS, MONTHS, DAYS, HOURS, MINUTES, SECONDS.", "signature": "date.add(amount, unit)", "example": "$tomorrow: $date | date.add(1, \"DAYS\");"}},
+    {"name": "date.part", "detail": "Get date part", "insertText": "date.part(\"${1:YEAR}\")", "returns": {"type": "Number"}, "documentation": "Parts: YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DAYOFYEAR, DAYOFWEEK", "signature": {"label": "date.part(part: string)", "parameters": [{"label": "part", "documentation": "Date part: YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DAYOFYEAR, DAYOFWEEK"}], "documentation": "Extracts a specific part from a date"}, "hover": {"description": "Extracts a part of a date. Parts: YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, DAYOFYEAR, DAYOFWEEK.", "signature": "date.part(part)", "example": "$year: $date | date.part(\"YEAR\");"}},
+    {"name": "date.fromEpochSeconds", "detail": "From epoch seconds", "insertText": "date.fromEpochSeconds"},
+    {"name": "date.fromEpochMillis", "detail": "From epoch milliseconds", "insertText": "date.fromEpochMillis"},
+    {"name": "date.totimezone", "detail": "Convert date to timezone", "insertText": "date.totimezone(\"${1:UTC}\")", "signature": {"label": "date.totimezone(timeZone: string)", "parameters": [{"label": "timeZone", "documentation": "Timezone ID (e.g. UTC, America/New_York)"}], "documentation": "Converts date to given timezone"}, "hover": {"description": "Converts date to the specified timezone.", "signature": "date.totimezone(timeZone)", "example": "$utc: $date | date.totimezone(\"UTC\");"}},
+    {"name": "date.toutc", "detail": "Convert date to UTC", "insertText": "date.toutc"},
+    {"name": "date.tolocal", "detail": "Convert date to local timezone", "insertText": "date.tolocal"},
+    {"name": "json.parse", "detail": "Parse JSON", "insertText": "json.parse"},
+    {"name": "yaml.parse", "detail": "Parse YAML", "insertText": "yaml.parse"},
+    {"name": "xml.parse", "detail": "Parse XML", "insertText": "xml.parse"},
+    {"name": "csv.parsemultiline", "detail": "Parse CSV", "insertText": "csv.parsemultiline", "documentation": "Options: {headers: true, separator: \",\", skipLines: 0}", "signature": {"label": "csv.parsemultiline(options?: object)", "parameters": [{"label": "options", "documentation": "Optional: {headers: true, separator: \",\", skipLines: 0}"}], "documentation": "Parses multi-line CSV to array of objects"}, "hover": {"description": "Parses multi-line CSV into array of objects.", "signature": "csv.parsemultiline({headers, separator, skipLines})", "example": "$data: $csv | csv.parsemultiline;"}},
+    {"name": "csv.findrow", "detail": "Find CSV row", "insertText": "csv.findrow"},
+    {"name": "html.escape", "detail": "Escape HTML entities", "insertText": "html.escape"},
+    {"name": "html.unescape", "detail": "Unescape HTML entities", "insertText": "html.unescape"},
+    {"name": "Math.sum", "detail": "Sum values", "insertText": "Math.sum(${1:0})", "returns": {"type": "Number"}, "signature": {"label": "Math.sum(initialValue: number)", "parameters": [{"label": "initialValue", "documentation": "Starting value for sum (usually 0)"}], "documentation": "Sums all numbers in array"}, "hover": {"description": "Sums all numbers in an array.", "signature": "Math.sum(initialValue)", "example": "$total: $prices | Math.sum(0);"}},
+    {"name": "Math.average", "detail": "Average values", "insertText": "Math.average", "hover": {"description": "Calculates the average of numbers in an array.", "signature": "Math.average", "example": "$avg: $scores | Math.average;"}},
+    {"name": "Math.mean", "detail": "Mean of values", "insertText": "Math.mean"},
+    {"name": "Math.min", "detail": "Minimum value", "insertText": "Math.min", "hover": {"description": "Finds the minimum value in an array.", "signature": "Math.min", "example": "$lowest: $prices | Math.min;"}},
+    {"name": "Math.max", "detail": "Maximum value", "insertText": "Math.max", "hover": {"description": "Finds the maximum value in an array.", "signature": "Math.max", "example": "$highest: $scores | Math.max;"}},
+    {"name": "Math.mod", "detail": "Modulo operation", "insertText": "Math.mod(${1:divisor})"},
+    {"name": "Math.sqrt", "detail": "Square root", "insertText": "Math.sqrt"},
+    {"name": "Math.round", "detail": "Round number", "insertText": "Math.round", "hover": {"description": "Rounds a number.", "signature": "Math.round", "example": "$rounded: $value | Math.round;"}},
+    {"name": "Math.floor", "detail": "Round down", "insertText": "Math.floor", "hover": {"description": "Rounds down to nearest integer.", "signature": "Math.floor", "example": "$down: $value | Math.floor;"}},
+    {"name": "Math.ceil", "detail": "Round up", "insertText": "Math.ceil", "hover": {"description": "Rounds up to nearest integer.", "signature": "Math.ceil", "example": "$up: $value | Math.ceil;"}},
+    {"name": "Math.abs", "detail": "Absolute value", "insertText": "Math.abs", "hover": {"description": "Returns absolute value.", "signature": "Math.abs", "example": "$positive: $negative | Math.abs;"}},
+    {"name": "Math.clamp", "detail": "Clamp value", "insertText": "Math.clamp(${1:min}, ${2:max})", "signature": {"label": "Math.clamp(min: number, max: number)", "parameters": [{"label": "min", "documentation": "Minimum allowed value"}, {"label": "max", "documentation": "Maximum allowed value"}], "documentation": "Clamps number to range [min, max]"}, "hover": {"description": "Clamps a number to a range [min, max].", "signature": "Math.clamp(min, max)", "example": "$safe: $value | Math.clamp(0, 100);"}},
+    {"name": "Math.RandInt", "detail": "Random integer", "insertText": "Math.RandInt(${1:min}, ${2:max})", "signature": {"label": "Math.RandInt(min: number, max?: number)", "parameters": [{"label": "min", "documentation": "Minimum value (inclusive)"}, {"label": "max", "documentation": "Maximum value (exclusive); if omitted, 0 to min"}], "documentation": "Random integer in range"}, "hover": {"description": "Generates a random integer.", "signature": "Math.RandInt(min, max?)", "example": "$n: $x | Math.RandInt(1, 100);"}},
+    {"name": "Math.RandFloat", "detail": "Random float", "insertText": "Math.RandFloat"},
+    {"name": "Math.RandDouble", "detail": "Random double", "insertText": "Math.RandDouble", "signature": {"label": "Math.RandDouble(min?: number, max?: number)", "parameters": [{"label": "min", "documentation": "Minimum (optional)"}, {"label": "max", "documentation": "Maximum (optional)"}], "documentation": "Random double in range or [0,1)"}},
+    {"name": "Math.log", "detail": "Logarithm (base e or custom)", "insertText": "Math.log(${1:base})", "signature": {"label": "Math.log(base?: number)", "parameters": [{"label": "base", "documentation": "Optional base; default is e (natural log)"}], "documentation": "Natural log or log with given base"}, "hover": {"description": "Natural logarithm or logarithm with custom base.", "signature": "Math.log(base?)", "example": "$y: $x | Math.log;"}},
+    {"name": "Math.log10", "detail": "Logarithm base 10", "insertText": "Math.log10", "hover": {"description": "Base-10 logarithm.", "signature": "Math.log10", "example": "$y: $x | Math.log10;"}},
+    {"name": "Math.ln", "detail": "Natural logarithm", "insertText": "Math.ln", "hover": {"description": "Natural logarithm (base e).", "signature": "Math.ln", "example": "$y: $x | Math.ln;"}},
+    {"name": "Math.pow", "detail": "Power (value^exponent)", "insertText": "Math.pow(${1:exponent})", "signature": {"label": "Math.pow(exponent: number)", "parameters": [{"label": "exponent", "documentation": "Exponent"}], "documentation": "Raises value to exponent"}, "hover": {"description": "Raises value to the given exponent.", "signature": "Math.pow(exponent)", "example": "$sq: $x | Math.pow(2);"}},
+    {"name": "negate", "detail": "Negate number", "insertText": "negate"},
+    {"name": "absolute", "detail": "Absolute value", "insertText": "absolute"},
+    {"name": "precision", "detail": "Set decimal precision", "insertText": "precision(${1:2})", "signature": {"label": "precision(digits: number)", "parameters": [{"label": "digits", "documentation": "Number of decimal places"}], "documentation": "Sets decimal precision (e.g., precision(2) → 12.99)"}, "hover": {"description": "Sets decimal precision for a number.", "signature": "precision(digits)", "example": "$price: $value | precision(2);"}},
+    {"name": "round.up", "detail": "Round up", "insertText": "round.up(${1:2})", "signature": {"label": "round.up(precision?: number)", "parameters": [{"label": "precision", "documentation": "Decimal places (default 2)"}], "documentation": "Rounds up to given precision"}, "hover": {"description": "Rounds a number up.", "signature": "round.up(precision?)", "example": "$val: $num | round.up(2);"}},
+    {"name": "round.down", "detail": "Round down", "insertText": "round.down(${1:2})", "signature": {"label": "round.down(precision?: number)", "parameters": [{"label": "precision", "documentation": "Decimal places (default 2)"}], "documentation": "Rounds down to given precision"}, "hover": {"description": "Rounds a number down.", "signature": "round.down(precision?)", "example": "$val: $num | round.down(2);"}},
+    {"name": "round.ceiling", "detail": "Round ceiling", "insertText": "round.ceiling(${1:2})"},
+    {"name": "round.floor", "detail": "Round floor", "insertText": "round.floor(${1:2})"},
+    {"name": "round.half", "detail": "Round half (half-even)", "insertText": "round.half(${1:2})"},
+    {"name": "keys", "detail": "Get object keys", "insertText": "keys", "returns": {"type": "Array"}, "hover": {"description": "Returns an array of object keys.", "signature": "keys", "example": "$propNames: $object | keys;"}},
+    {"name": "kv", "detail": "Key-value pairs", "insertText": "kv", "documentation": "Converts object to [{key, value}] array", "hover": {"description": "Converts object to array of key-value pairs.", "signature": "kv", "example": "$pairs: $object | kv;"}},
+    {"name": "delete", "detail": "Delete property", "insertText": "delete(\"${1:property}\")", "signature": {"label": "delete(propertyName: string)", "parameters": [{"label": "propertyName", "documentation": "Name of property to delete"}], "documentation": "Removes property from object"}, "hover": {"description": "Removes a property from an object.", "signature": "delete(propertyName)", "example": "$clean: $object | delete(\"tempField\");"}},
+    {"name": "select", "detail": "Select by JSON path", "insertText": "select(\"${1:path}\")", "documentation": "Example: select(\"user.address.city\")", "signature": {"label": "select(path: string)", "parameters": [{"label": "path", "documentation": "Property path (e.g., \"user.address.city\")"}], "documentation": "Selects nested property by path"}, "hover": {"description": "Selects a nested property by path.", "signature": "select(path)", "example": "$value: $object | select(\"user.address.city\");"}},
+    {"name": "getProperty", "detail": "Get property (case-insensitive)", "insertText": "getProperty(\"${1:name}\")", "signature": {"label": "getProperty(name: string)", "parameters": [{"label": "name", "documentation": "Property name (case-insensitive)"}], "documentation": "Gets property by name (case-insensitive lookup)"}, "hover": {"description": "Gets a property by name (case-insensitive).", "signature": "getProperty(name)", "example": "$value: $object | getProperty(\"Name\");"}},
+    {"name": "setProperty", "detail": "Set property", "insertText": "setProperty(\"${1:name}\", ${2:value})"},
+    {"name": "merge", "detail": "Merge objects", "insertText": "merge(${1:\\$other})"},
+    {"name": "pick", "detail": "Pick properties", "insertText": "pick(\"${1:prop1}\", \"${2:prop2}\")"},
+    {"name": "omit", "detail": "Omit properties", "insertText": "omit(\"${1:prop1}\", \"${2:prop2}\")"},
+    {"name": "rename", "detail": "Rename property", "insertText": "rename(\"${1:oldName}\", \"${2:newName}\")"},
+    {"name": "has", "detail": "Check if has property", "insertText": "has(\"${1:property}\")"},
+    {"name": "default", "detail": "Default value if null/empty", "insertText": "default(${1:value})", "signature": {"label": "default(defaultValue: any)", "parameters": [{"label": "defaultValue", "documentation": "Value to use if input is null/empty"}], "documentation": "Returns default value if input is null or empty"}, "hover": {"description": "Returns a default value if the input is null or empty.", "signature": "default(defaultValue)", "example": "$name: $input.name | default(\"Unknown\");"}},
+    {"name": "regex.find", "detail": "Find regex matches", "insertText": "regex.find(\"${1:pattern}\")", "signature": {"label": "regex.find(pattern: string, options?: object)", "parameters": [{"label": "pattern", "documentation": "Regular expression pattern"}, {"label": "options", "documentation": "Optional: {ignoreCase: true, multiLine: true}"}], "documentation": "Finds all matches of pattern in string"}, "hover": {"description": "Finds all matches of pattern in string.", "signature": "regex.find(pattern)", "example": "$found: $text | regex.find(\"/\\\\d+/\");"}},
+    {"name": "regex.matches", "detail": "Test regex match", "insertText": "regex.matches(\"${1:pattern}\")", "returns": {"type": "Boolean"}, "signature": {"label": "regex.matches(pattern: string, options?: object)", "parameters": [{"label": "pattern", "documentation": "Regular expression pattern"}, {"label": "options", "documentation": "Optional: {ignoreCase: true}"}], "documentation": "Tests if pattern matches string (returns boolean)"}},
+    {"name": "regex.replace", "detail": "Replace with regex", "insertText": "regex.replace(\"${1:pattern}\", \"${2:replacement}\")", "signature": {"label": "regex.replace(pattern: string, replacement: string, options?: object)", "parameters": [{"label": "pattern", "documentation": "Regular expression pattern"}, {"label": "replacement", "documentation": "Replacement string"}, {"label": "options", "documentation": "Optional: {ignoreCase: true}"}], "documentation": "Replaces pattern matches with replacement"}},
+    {"name": "regex.replacefirst", "detail": "Replace first match", "insertText": "regex.replacefirst(\"${1:pattern}\", \"${2:replacement}\")"},
+    {"name": "encode.base64", "detail": "Base64 encode", "insertText": "encode.base64", "signature": {"label": "encode.base64(options?: object)", "parameters": [{"label": "options", "documentation": "Optional {withoutPadding: true}"}], "documentation": "Encodes to Base64 string"}, "hover": {"description": "Encodes value to Base64 string.", "signature": "encode.base64(options?)", "example": "$b64: $bytes | encode.base64;"}},
+    {"name": "encode.base64url", "detail": "Base64 URL encode", "insertText": "encode.base64url", "signature": {"label": "encode.base64url(options?: object)", "parameters": [{"label": "options", "documentation": "Optional {withoutPadding: true}"}], "documentation": "Encodes to Base64 URL-safe string"}, "hover": {"description": "Encodes to Base64 URL-safe string.", "signature": "encode.base64url(options?)", "example": "$b64: $bytes | encode.base64url;"}},
+    {"name": "encode.path", "detail": "URL path encode", "insertText": "encode.path"},
+    {"name": "encode.query", "detail": "URL query encode", "insertText": "encode.query"},
+    {"name": "decode.base64", "detail": "Base64 decode", "insertText": "decode.base64"},
+    {"name": "decode.base64url", "detail": "Base64 URL decode", "insertText": "decode.base64url"},
+    {"name": "decode.query", "detail": "URL query decode", "insertText": "decode.query"},
+    {"name": "hex.tobinary", "detail": "Convert hex to binary", "insertText": "hex.tobinary"},
+    {"name": "join.string", "detail": "Join array to string", "insertText": "join.string(\"${1:,}\")", "signature": {"label": "join.string(itemDelimiter?: string, fieldDelimiter?: string)", "parameters": [{"label": "itemDelimiter", "documentation": "Between items (default \",\")"}, {"label": "fieldDelimiter", "documentation": "Between key=value (for objects, default \"=\")"}], "documentation": "Joins array or object to string"}, "hover": {"description": "Joins array elements to string. For objects: encoding is first param (string/path/query), then delimiters.", "signature": "join.string(delimiter?)", "example": "$s: $arr | join.string(\", \");"}},
+    {"name": "join.path", "detail": "Join for URL path encoding", "insertText": "join.path(\"${1:&}\")", "signature": {"label": "join.path(itemDelimiter?: string)", "parameters": [{"label": "itemDelimiter", "documentation": "Between items"}], "documentation": "Joins with path encoding"}, "hover": {"description": "Joins array/object with URL path encoding.", "signature": "join.path(delimiter?)", "example": "$path: $segments | join.path;"}},
+    {"name": "join.query", "detail": "Join for URL query encoding", "insertText": "join.query(\"${1:&}\")", "signature": {"label": "join.query(itemDelimiter?: string)", "parameters": [{"label": "itemDelimiter", "documentation": "Between key=value pairs"}], "documentation": "Joins with query encoding"}, "hover": {"description": "Joins object to query string with encoding.", "signature": "join.query(delimiter?)", "example": "$qs: $params | join.query(\"&\");"}},
+    {"name": "email.parse", "detail": "Parse email addresses", "insertText": "email.parse"},
+    {"name": "gzip", "detail": "GZip compress", "insertText": "gzip(\"${1:UTF-8}\")", "signature": {"label": "gzip(charset?: string)", "parameters": [{"label": "charset", "documentation": "Charset for string input (default UTF-8)"}], "documentation": "Compresses to gzip bytes"}, "hover": {"description": "Compresses string to gzip byte array.", "signature": "gzip(charset?)", "example": "$compressed: $text | gzip;"}},
+    {"name": "gunzip", "detail": "GZip decompress", "insertText": "gunzip(\"${1:UTF-8}\")", "signature": {"label": "gunzip(charset?: string)", "parameters": [{"label": "charset", "documentation": "Charset for output string (default UTF-8)"}], "documentation": "Decompresses gzip bytes to string"}, "hover": {"description": "Decompresses gzip bytes to string.", "signature": "gunzip(charset?)", "example": "$text: $bytes | gunzip;"}},
+    {"name": "gunzipToByte", "detail": "GZip decompress to bytes", "insertText": "gunzipToByte"},
+    {"name": "Crypto.sha256", "detail": "SHA-256 hash (bytes)", "insertText": "Crypto.sha256"},
+    {"name": "Crypto.sha512", "detail": "SHA-512 hash (bytes)", "insertText": "Crypto.sha512"},
+    {"name": "Crypto.sha1", "detail": "SHA-1 hash (bytes)", "insertText": "Crypto.sha1"},
+    {"name": "Crypto.md5", "detail": "MD5 hash (bytes)", "insertText": "Crypto.md5"},
+    {"name": "Crypto.hmacsha256", "detail": "HMAC-SHA256", "insertText": "Crypto.hmacsha256(${1:key})", "signature": {"label": "Crypto.hmacsha256(key)", "parameters": [{"label": "key", "documentation": "Secret key (bytes)"}], "documentation": "HMAC with SHA-256"}, "hover": {"description": "Computes HMAC-SHA256. Input and key are from pipe/args.", "signature": "Crypto.hmacsha256(key)", "example": "$sig: $payload | Crypto.hmacsha256($key);"}},
+    {"name": "Crypto.hmacsha384", "detail": "HMAC-SHA384", "insertText": "Crypto.hmacsha384(${1:key})"},
+    {"name": "Crypto.hmacsha512", "detail": "HMAC-SHA512", "insertText": "Crypto.hmacsha512(${1:key})"},
+    {"name": "Crypto.hmacsha1", "detail": "HMAC-SHA1", "insertText": "Crypto.hmacsha1(${1:key})"},
+    {"name": "Crypto.rsasha256", "detail": "RSA-SHA256 sign", "insertText": "Crypto.rsasha256(${1:keyStore}, ${2:password}, ${3:keyAlias})"},
+    {"name": "Crypto.tokeystore", "detail": "Load key store from Base64", "insertText": "Crypto.tokeystore(${1:type}, ${2:password})", "signature": {"label": "Crypto.tokeystore(keystoreType: string, password: string)", "parameters": [{"label": "keystoreType", "documentation": "KeyStore type"}, {"label": "password", "documentation": "KeyStore password"}], "documentation": "Decodes Base64 key store bytes"}, "hover": {"description": "Loads a KeyStore from Base64-encoded bytes.", "signature": "Crypto.tokeystore(type, password)", "example": "$ks: $b64 | Crypto.tokeystore(\"PKCS12\", \"secret\");"}},
+    {"name": "Crypto.aes256gcm", "detail": "AES-256-GCM decrypt", "insertText": "Crypto.aes256gcm(${1:key}, ${2:iv}, ${3:tag})"},
+    {"name": "Crypto.decrypt", "detail": "JWE decrypt", "insertText": "Crypto.decrypt(${1:algorithm}, ${2:keyMaterial})", "signature": {"label": "Crypto.decrypt(algorithm: string, keyMaterial: string)", "parameters": [{"label": "algorithm", "documentation": "e.g. RSA_OAEP_256"}, {"label": "keyMaterial", "documentation": "Private key PEM or JWK"}], "documentation": "Decrypts JWE payload"}, "hover": {"description": "Decrypts a JWE payload with private key.", "signature": "Crypto.decrypt(algorithm, keyMaterial)", "example": "$plain: $jwe | Crypto.decrypt(\"RSA_OAEP_256\", $privateKey);"}},
+    {"name": "Crypto.verify", "detail": "JWS verify", "insertText": "Crypto.verify(${1:algorithm}, ${2:keyMaterial})", "signature": {"label": "Crypto.verify(algorithm: string, keyMaterial: string)", "parameters": [{"label": "algorithm", "documentation": "e.g. RS256"}, {"label": "keyMaterial", "documentation": "Public key PEM or JWK"}], "documentation": "Verifies JWS and returns payload"}, "hover": {"description": "Verifies a JWS and returns the payload.", "signature": "Crypto.verify(algorithm, keyMaterial)", "example": "$payload: $jws | Crypto.verify(\"RS256\", $publicKey);"}},
+    {"name": "zip.add", "detail": "Add file to zip", "insertText": "zip.add(\"${1:fileName}\", ${2:content})", "signature": {"label": "zip.add(fileName: string, content: any, charset?: string)", "parameters": [{"label": "fileName", "documentation": "Entry name in zip"}, {"label": "content", "documentation": "File content (string or bytes)"}, {"label": "charset", "documentation": "Optional charset for string content (default UTF-8)"}], "documentation": "Adds a file to the ZipObject"}, "hover": {"description": "Adds a file to an open zip (from @.Zip.Start()).", "signature": "zip.add(fileName, content, charset?)", "example": "@.Zip.Start() | zip.add(\"foo.txt\", \"hello\") | zip.close;"}},
+    {"name": "zip.close", "detail": "Close zip and get bytes", "insertText": "zip.close"},
+    {"name": "zip.inflate", "detail": "Inflate (decompress deflate)", "insertText": "zip.inflate"},
+    {"name": "zip.deflate", "detail": "Deflate (compress)", "insertText": "zip.deflate"},
+    {"name": "unzip", "detail": "Unzip byte array to entries", "insertText": "unzip", "returns": {"type": "Array"}, "signature": {"label": "unzip", "parameters": [], "documentation": "Returns array of {name, content} for each entry"}, "hover": {"description": "Unzips a byte array to an array of {name, content}.", "signature": "unzip", "example": "$entries: $zippedBytes | unzip;"}},
+    {"name": "retry.when", "detail": "Retry when condition", "insertText": "retry.when({{ ${1:condition} }}, { retryCount: ${2:3}, backOff: ${3:false} })", "signature": {"label": "retry.when(condition, options?)", "parameters": [{"label": "condition", "documentation": "Expression; if true after execution, retries"}, {"label": "options", "documentation": "{retryCount, delay, delayFrom, delayTo, backOff}"}], "documentation": "Conditional retry with options"}, "hover": {"description": "Retries while condition is true. Options: retryCount, delay, backOff.", "signature": "retry.when(condition, options)", "example": "$x: retry.when({{ $.status == \"pending\" }}, { retryCount: 3 });"}},
+    {"name": "typeof", "detail": "Get type of value", "insertText": "typeof"}
+  ],
+  "functions": [
+    {"namespace": "Date", "name": "now", "detail": "Get current date/time", "params": "()", "returns": {"type": "Date"}, "documentation": "Returns the current date and time in UTC.", "signature": {"label": "@.Date.Now()", "parameters": [], "documentation": "Returns current date/time in UTC"}},
+    {"namespace": "Date", "name": "parse", "detail": "Parse date string", "params": "(string, format)", "returns": {"type": "Date"}, "documentation": "Parses a date string using the specified format.", "signature": {"label": "@.Date.parse(dateString: string, format: string, options?: object)", "parameters": [{"label": "dateString", "documentation": "String containing the date to parse"}, {"label": "format", "documentation": "Date format pattern (e.g., \"yyyy-MM-dd\")"}, {"label": "options", "documentation": "Optional: {locale: \"en_US\"}"}], "documentation": "Parses a date string into a date object"}},
+    {"namespace": "Date", "name": "format", "detail": "Format date", "params": "(date, format)"},
+    {"namespace": "Date", "name": "fromEpochSeconds", "detail": "Create date from epoch seconds", "params": "(seconds)", "returns": {"type": "Date"}, "documentation": "Creates a date from Unix epoch seconds.", "signature": {"label": "@.Date.fromEpochSeconds(seconds: number)", "parameters": [{"label": "seconds", "documentation": "Unix timestamp in seconds"}], "documentation": "Creates date from Unix timestamp (seconds)"}},
+    {"namespace": "Date", "name": "fromEpochMillis", "detail": "Create date from epoch milliseconds", "params": "(millis)", "returns": {"type": "Date"}, "documentation": "Creates a date from Unix epoch milliseconds.", "signature": {"label": "@.Date.fromEpochMillis(milliseconds: number)", "parameters": [{"label": "milliseconds", "documentation": "Unix timestamp in milliseconds"}], "documentation": "Creates date from Unix timestamp (milliseconds)"}},
+    {"namespace": "Math", "name": "sum", "detail": "Sum values in array", "params": "(initial)", "returns": {"type": "Number"}, "documentation": "Sums all values in an array. Use with pipe: `$prices | Math.sum(0)`", "signature": {"label": "@.Math.sum(array: number[], initialValue: number)", "parameters": [{"label": "array", "documentation": "Array of numbers to sum"}, {"label": "initialValue", "documentation": "Starting value (usually 0)"}], "documentation": "Sums all numbers in array"}},
+    {"namespace": "Math", "name": "mean", "detail": "Calculate mean (average of array)", "params": "()", "returns": {"type": "Number"}, "documentation": "Average of numbers in array. Use with pipe: $arr | Math.mean", "signature": {"label": "@.Math.mean()", "parameters": [], "documentation": "Mean of array (pipe input)"}},
+    {"namespace": "Math", "name": "min", "detail": "Find minimum value", "params": "()", "returns": {"type": "Number"}},
+    {"namespace": "Math", "name": "max", "detail": "Find maximum value", "params": "()", "returns": {"type": "Number"}},
+    {"namespace": "Math", "name": "mod", "detail": "Modulo operation", "params": "(divisor)"},
+    {"namespace": "Math", "name": "sqrt", "detail": "Square root", "params": "()", "returns": {"type": "Number"}, "documentation": "Square root. Use with pipe: $x | Math.sqrt"},
+    {"namespace": "Math", "name": "clamp", "detail": "Clamp value between min and max", "params": "(min, max)", "signature": {"label": "@.Math.clamp(value: number, min: number, max: number)", "parameters": [{"label": "value", "documentation": "Number to clamp (pipe input)"}, {"label": "min", "documentation": "Minimum allowed value"}, {"label": "max", "documentation": "Maximum allowed value"}], "documentation": "Clamps value to range [min, max]"}},
+    {"namespace": "Math", "name": "RandInt", "detail": "Random integer", "params": "(min, max)", "documentation": "Generates a random integer between min and max (inclusive)."},
+    {"namespace": "Math", "name": "RandFloat", "detail": "Random float", "params": "()"},
+    {"namespace": "Math", "name": "RandDouble", "detail": "Random double", "params": "(min?, max?)", "documentation": "Random double; optional min/max range.", "signature": {"label": "@.Math.RandDouble(min?: number, max?: number)", "parameters": [{"label": "min", "documentation": "Optional minimum"}, {"label": "max", "documentation": "Optional maximum"}], "documentation": "Random double in [0,1) or [min,max)"}},
+    {"namespace": "Math", "name": "log", "detail": "Logarithm (base e or custom)", "params": "(base?)", "returns": {"type": "Number"}, "documentation": "Natural log or log with given base. Use with pipe: $x | Math.log or Math.log(base)", "signature": {"label": "@.Math.log(base?: number)", "parameters": [{"label": "base", "documentation": "Optional base (default e)"}], "documentation": "Natural log or custom base"}},
+    {"namespace": "Math", "name": "log10", "detail": "Log base 10", "params": "()", "returns": {"type": "Number"}, "documentation": "Base-10 logarithm. Use with pipe: $x | Math.log10"},
+    {"namespace": "Math", "name": "ln", "detail": "Natural logarithm", "params": "()", "returns": {"type": "Number"}, "documentation": "Natural log (base e). Use with pipe: $x | Math.ln"},
+    {"namespace": "Math", "name": "pow", "detail": "Power (value^exponent)", "params": "(exponent)", "returns": {"type": "Number"}, "documentation": "Raises value to exponent. Use with pipe: $x | Math.pow(2)", "signature": {"label": "@.Math.pow(exponent: number)", "parameters": [{"label": "exponent", "documentation": "Exponent"}], "documentation": "Value to the power of exponent"}},
+    {"namespace": "Run", "name": "Sleep", "detail": "Sleep milliseconds", "params": "(ms)", "documentation": "Pauses execution for given milliseconds. Use: @.Run.Sleep(1000)", "signature": {"label": "@.Run.Sleep(milliseconds: number)", "parameters": [{"label": "milliseconds", "documentation": "Delay in ms"}], "documentation": "Async sleep"}},
+    {"namespace": "UUID", "name": "New", "detail": "Generate new UUID", "params": "()", "returns": {"type": "String"}, "documentation": "Returns a new UUID string. Use: @.UUID.New()", "signature": {"label": "@.UUID.New()", "parameters": [], "documentation": "New UUID string"}},
+    {"namespace": "Zip", "name": "Start", "detail": "Create new zip object", "params": "()", "returns": {"type": "Object"}, "documentation": "Creates an empty zip for zip.add/zip.close. Use: @.Zip.Start() | zip.add(\"f.txt\", $content) | zip.close", "signature": {"label": "@.Zip.Start()", "parameters": [], "documentation": "New ZipObject for building zip"}},
+    {"namespace": "String", "name": "concat", "detail": "Concatenate strings", "params": "(...)", "signature": {"label": "@.String.concat(...strings: string[])", "parameters": [{"label": "strings", "documentation": "Strings to concatenate"}], "documentation": "Concatenates multiple strings"}},
+    {"namespace": "String", "name": "join", "detail": "Join array with separator", "params": "(array, sep)", "signature": {"label": "@.String.join(array: string[], separator: string)", "parameters": [{"label": "array", "documentation": "Array of strings to join"}, {"label": "separator", "documentation": "Separator string (e.g., \", \")"}], "documentation": "Joins array elements into string"}},
+    {"namespace": "String", "name": "split", "detail": "Split string", "params": "(str, sep)", "signature": {"label": "@.String.split(string: string, separator: string)", "parameters": [{"label": "string", "documentation": "String to split"}, {"label": "separator", "documentation": "Separator pattern"}], "documentation": "Splits string into array"}},
+    {"namespace": "Array", "name": "from", "detail": "Create array from", "params": "(...)"},
+    {"namespace": "Array", "name": "of", "detail": "Create array of", "params": "(...)"},
+    {"namespace": "Array", "name": "range", "detail": "Generate range", "params": "(start, count, increment)", "documentation": "Generates an array of numbers. Example: `@.Array.range(0, 10, 1)` creates [0, 1, 2, ..., 9]"},
+    {"namespace": "Array", "name": "slice", "detail": "Slice array", "params": "(start, end)"},
+    {"namespace": "Array", "name": "unique", "detail": "Unique values", "params": "()"},
+    {"namespace": "Json", "name": "parse", "detail": "Parse JSON string", "params": "(string)", "returns": {"type": "Object"}, "signature": {"label": "@.Json.parse(jsonString: string)", "parameters": [{"label": "jsonString", "documentation": "JSON string to parse"}], "documentation": "Parses JSON string into object"}},
+    {"namespace": "Json", "name": "stringify", "detail": "Stringify to JSON", "params": "(object)", "returns": {"type": "String"}, "signature": {"label": "@.Json.stringify(object: any)", "parameters": [{"label": "object", "documentation": "Object to convert to JSON"}], "documentation": "Converts object to JSON string"}},
+    {"namespace": "Xml", "name": "parse", "detail": "Parse XML string", "params": "(string)", "returns": {"type": "Object"}, "signature": {"label": "@.Xml.parse(xmlString: string)", "parameters": [{"label": "xmlString", "documentation": "XML string to parse"}], "documentation": "Parses XML string into JSON object"}},
+    {"namespace": "Xml", "name": "toXml", "detail": "Convert to XML", "params": "(object, rootName)", "signature": {"label": "@.Xml.toXml(object: any, rootName: string)", "parameters": [{"label": "object", "documentation": "Object to convert"}, {"label": "rootName", "documentation": "Root element name"}], "documentation": "Converts object to XML string"}},
+    {"namespace": "Yaml", "name": "parse", "detail": "Parse YAML", "params": "(string)"},
+    {"namespace": "Crypto", "name": "md5", "detail": "MD5 hash", "params": "(string)", "returns": {"type": "String"}, "signature": {"label": "@.Crypto.md5(string: string)", "parameters": [{"label": "string", "documentation": "String to hash"}], "documentation": "Computes MD5 hash (returns hex string)"}},
+    {"namespace": "Crypto", "name": "sha1", "detail": "SHA-1 hash", "params": "(string)", "returns": {"type": "String"}, "signature": {"label": "@.Crypto.sha1(string: string)", "parameters": [{"label": "string", "documentation": "String to hash"}], "documentation": "Computes SHA-1 hash (returns hex string)"}},
+    {"namespace": "Crypto", "name": "sha256", "detail": "SHA-256 hash", "params": "(string)", "returns": {"type": "String"}, "signature": {"label": "@.Crypto.sha256(string: string)", "parameters": [{"label": "string", "documentation": "String to hash"}], "documentation": "Computes SHA-256 hash (returns hex string)"}},
+    {"namespace": "Crypto", "name": "base64encode", "detail": "Base64 encode", "params": "(string)", "returns": {"type": "String"}, "signature": {"label": "@.Crypto.base64encode(string: string)", "parameters": [{"label": "string", "documentation": "String to encode"}], "documentation": "Encodes string to Base64"}},
+    {"namespace": "Crypto", "name": "base64decode", "detail": "Base64 decode", "params": "(string)", "returns": {"type": "String"}, "signature": {"label": "@.Crypto.base64decode(string: string)", "parameters": [{"label": "string", "documentation": "Base64 string to decode"}], "documentation": "Decodes Base64 string"}},
+    {"namespace": "Pagination", "name": "Cursor", "detail": "Cursor pagination", "params": "()", "documentation": "Statement: Cursor-based pagination. Registered in runtime."},
+    {"namespace": "Pagination", "name": "Page", "detail": "Page pagination", "params": "()", "documentation": "Statement: Page-based pagination. Registered in runtime."},
+    {"namespace": "Pagination", "name": "Date", "detail": "Date pagination", "params": "()", "documentation": "Statement: Date-based pagination. Registered in runtime."},
+    {"namespace": "Assert", "name": "equal", "detail": "Assert expected equals actual", "params": "(expected, actual, message?)", "returns": {"type": "null"}, "documentation": "Asserts that expected equals actual (deep equality). Throws on failure. Optional third parameter: custom message.", "signature": {"label": "@.Assert.equal(expected: any, actual: any, message?: string)", "parameters": [{"label": "expected", "documentation": "Expected value"}, {"label": "actual", "documentation": "Actual value to compare"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts deep equality between expected and actual"}},
+    {"namespace": "Assert", "name": "notEqual", "detail": "Assert expected not equals actual", "params": "(expected, actual, message?)", "returns": {"type": "null"}, "documentation": "Asserts that expected does not equal actual. Throws on failure.", "signature": {"label": "@.Assert.notEqual(expected: any, actual: any, message?: string)", "parameters": [{"label": "expected", "documentation": "Value that should not match"}, {"label": "actual", "documentation": "Actual value to compare"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts values are not equal"}},
+    {"namespace": "Assert", "name": "notNull", "detail": "Assert value is not null", "params": "(value, message?)", "returns": {"type": "null"}, "documentation": "Asserts that the value is not null. Throws on failure.", "signature": {"label": "@.Assert.notNull(value: any, message?: string)", "parameters": [{"label": "value", "documentation": "Value that must not be null"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts value is not null"}},
+    {"namespace": "Assert", "name": "isNull", "detail": "Assert value is null", "params": "(value, message?)", "returns": {"type": "null"}, "documentation": "Asserts that the value is null. Throws on failure.", "signature": {"label": "@.Assert.isNull(value: any, message?: string)", "parameters": [{"label": "value", "documentation": "Value that must be null"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts value is null"}},
+    {"namespace": "Assert", "name": "lessThan", "detail": "Assert expected < actual", "params": "(expected, actual, message?)", "returns": {"type": "null"}, "documentation": "Asserts expected is less than actual.", "signature": {"label": "@.Assert.lessThan(expected: any, actual: any, message?: string)", "parameters": [{"label": "expected", "documentation": "Expected (left) value"}, {"label": "actual", "documentation": "Actual (right) value"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts expected < actual"}},
+    {"namespace": "Assert", "name": "lessThanOrEqual", "detail": "Assert expected <= actual", "params": "(expected, actual, message?)", "returns": {"type": "null"}, "documentation": "Asserts expected is less than or equal to actual.", "signature": {"label": "@.Assert.lessThanOrEqual(expected: any, actual: any, message?: string)", "parameters": [{"label": "expected", "documentation": "Expected value"}, {"label": "actual", "documentation": "Actual value"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts expected <= actual"}},
+    {"namespace": "Assert", "name": "greaterThan", "detail": "Assert expected > actual", "params": "(expected, actual, message?)", "returns": {"type": "null"}, "documentation": "Asserts expected is greater than actual.", "signature": {"label": "@.Assert.greaterThan(expected: any, actual: any, message?: string)", "parameters": [{"label": "expected", "documentation": "Expected value"}, {"label": "actual", "documentation": "Actual value"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts expected > actual"}},
+    {"namespace": "Assert", "name": "greaterThanOrEqual", "detail": "Assert expected >= actual", "params": "(expected, actual, message?)", "returns": {"type": "null"}, "documentation": "Asserts expected is greater than or equal to actual.", "signature": {"label": "@.Assert.greaterThanOrEqual(expected: any, actual: any, message?: string)", "parameters": [{"label": "expected", "documentation": "Expected value"}, {"label": "actual", "documentation": "Actual value"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts expected >= actual"}},
+    {"namespace": "Assert", "name": "matches", "detail": "Assert string matches regex", "params": "(regex, string, message?)", "returns": {"type": "null"}, "documentation": "Asserts that the string matches the regex pattern (case-insensitive).", "signature": {"label": "@.Assert.matches(regex: string, string: string, message?: string)", "parameters": [{"label": "regex", "documentation": "Regex pattern"}, {"label": "string", "documentation": "String to test"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts string matches regex"}},
+    {"namespace": "Assert", "name": "notMatches", "detail": "Assert string does not match regex", "params": "(regex, string, message?)", "returns": {"type": "null"}, "documentation": "Asserts that the string does not match the regex pattern.", "signature": {"label": "@.Assert.notMatches(regex: string, string: string, message?: string)", "parameters": [{"label": "regex", "documentation": "Regex pattern"}, {"label": "string", "documentation": "String to test"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts string does not match regex"}},
+    {"namespace": "Assert", "name": "contains", "detail": "Assert container contains item", "params": "(container, item, message?)", "returns": {"type": "null"}, "documentation": "Asserts container (string or array) contains item.", "signature": {"label": "@.Assert.contains(container: string|array, item: any, message?: string)", "parameters": [{"label": "container", "documentation": "String or array to search in"}, {"label": "item", "documentation": "Item or substring to find"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts container contains item"}},
+    {"namespace": "Assert", "name": "notContains", "detail": "Assert container does not contain item", "params": "(container, item, message?)", "returns": {"type": "null"}, "documentation": "Asserts container (string or array) does not contain item.", "signature": {"label": "@.Assert.notContains(container: string|array, item: any, message?: string)", "parameters": [{"label": "container", "documentation": "String or array"}, {"label": "item", "documentation": "Item or substring that must not be present"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts container does not contain item"}},
+    {"namespace": "Assert", "name": "startsWith", "detail": "Assert string starts with prefix", "params": "(prefix, string, message?)", "returns": {"type": "null"}, "documentation": "Asserts that the string starts with the given prefix.", "signature": {"label": "@.Assert.startsWith(prefix: string, string: string, message?: string)", "parameters": [{"label": "prefix", "documentation": "Expected prefix"}, {"label": "string", "documentation": "String to test"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts string starts with prefix"}},
+    {"namespace": "Assert", "name": "notStartsWith", "detail": "Assert string does not start with prefix", "params": "(prefix, string, message?)", "returns": {"type": "null"}, "documentation": "Asserts that the string does not start with the given prefix.", "signature": {"label": "@.Assert.notStartsWith(prefix: string, string: string, message?: string)", "parameters": [{"label": "prefix", "documentation": "Prefix that must not match"}, {"label": "string", "documentation": "String to test"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts string does not start with prefix"}},
+    {"namespace": "Assert", "name": "endsWith", "detail": "Assert string ends with suffix", "params": "(suffix, string, message?)", "returns": {"type": "null"}, "documentation": "Asserts that the string ends with the given suffix.", "signature": {"label": "@.Assert.endsWith(suffix: string, string: string, message?: string)", "parameters": [{"label": "suffix", "documentation": "Expected suffix"}, {"label": "string", "documentation": "String to test"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts string ends with suffix"}},
+    {"namespace": "Assert", "name": "notEndsWith", "detail": "Assert string does not end with suffix", "params": "(suffix, string, message?)", "returns": {"type": "null"}, "documentation": "Asserts that the string does not end with the given suffix.", "signature": {"label": "@.Assert.notEndsWith(suffix: string, string: string, message?: string)", "parameters": [{"label": "suffix", "documentation": "Suffix that must not match"}, {"label": "string", "documentation": "String to test"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts string does not end with suffix"}},
+    {"namespace": "Assert", "name": "in", "detail": "Assert value is in collection", "params": "(value, collection, message?)", "returns": {"type": "null"}, "documentation": "Asserts that value is contained in collection (array or string).", "signature": {"label": "@.Assert.in(value: any, collection: array|string, message?: string)", "parameters": [{"label": "value", "documentation": "Value to find"}, {"label": "collection", "documentation": "Array or string to search in"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts value in collection"}},
+    {"namespace": "Assert", "name": "notIn", "detail": "Assert value is not in collection", "params": "(value, collection, message?)", "returns": {"type": "null"}, "documentation": "Asserts that value is not contained in collection.", "signature": {"label": "@.Assert.notIn(value: any, collection: array|string, message?: string)", "parameters": [{"label": "value", "documentation": "Value that must not be in collection"}, {"label": "collection", "documentation": "Array or string"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts value not in collection"}},
+    {"namespace": "Assert", "name": "isType", "detail": "Assert value is of type", "params": "(value, typeName, message?)", "returns": {"type": "null"}, "documentation": "Asserts value is of the given type. Types: number, date, string, node, array, boolean.", "signature": {"label": "@.Assert.isType(value: any, typeName: string, message?: string)", "parameters": [{"label": "value", "documentation": "Value to check"}, {"label": "typeName", "documentation": "Type: number, date, string, node, array, boolean"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts value is of type"}},
+    {"namespace": "Assert", "name": "isNotType", "detail": "Assert value is not of type", "params": "(value, typeName, message?)", "returns": {"type": "null"}, "documentation": "Asserts value is not of the given type.", "signature": {"label": "@.Assert.isNotType(value: any, typeName: string, message?: string)", "parameters": [{"label": "value", "documentation": "Value to check"}, {"label": "typeName", "documentation": "Type: number, date, string, node, array, boolean"}, {"label": "message", "documentation": "Optional custom failure message"}], "documentation": "Asserts value is not of type"}},
+    {"namespace": "Load", "name": "From", "detail": "Load file as JSON (test context)", "params": "(fileName)", "returns": {"type": "Object"}, "documentation": "Loads .json, .yaml, .yml, or .csv from file path relative to current ISL file. Available in test context only.", "signature": {"label": "@.Load.From(fileName: string)", "parameters": [{"label": "fileName", "documentation": "Path relative to current file (e.g. fixtures/data.json)"}], "documentation": "Loads file and converts to JSON"}},
+    {"namespace": "Mock", "name": "Func", "detail": "Mock a function", "params": "(functionName, returnValue?, param0?, param1?, ...)", "returns": {"type": "Number"}, "documentation": "Registers a mock for a function (e.g. Module.funcName). Returns instance ID. Use Mock.GetFuncCaptures to get captured params.", "signature": {"label": "@.Mock.Func(functionName: string, returnValue?: any, ...params)", "parameters": [{"label": "functionName", "documentation": "Full name e.g. This.myFunc or Module.funcName"}, {"label": "returnValue", "documentation": "Optional return value when mock is called"}, {"label": "params", "documentation": "Optional param matchers by index"}], "documentation": "Mocks a function for testing"}},
+    {"namespace": "Mock", "name": "Annotation", "detail": "Mock an annotation", "params": "(annotationName, returnValue?, param0?, ...)", "returns": {"type": "Number"}, "documentation": "Registers a mock for an annotation. Returns instance ID. Use Mock.GetAnnotationCaptures to get captured params.", "signature": {"label": "@.Mock.Annotation(annotationName: string, returnValue?: any, ...params)", "parameters": [{"label": "annotationName", "documentation": "Annotation name (e.g. test, setup)"}, {"label": "returnValue", "documentation": "Optional return value"}, {"label": "params", "documentation": "Optional param matchers"}], "documentation": "Mocks an annotation for testing"}},
+    {"namespace": "Mock", "name": "StatementFunc", "detail": "Mock a statement function", "params": "(functionName, returnValue?, param0?, ...)", "returns": {"type": "Number"}, "documentation": "Registers a mock for a statement function (block syntax). Returns instance ID.", "signature": {"label": "@.Mock.StatementFunc(functionName: string, returnValue?: any, ...params)", "parameters": [{"label": "functionName", "documentation": "Full name e.g. Module.statementFunc"}, {"label": "returnValue", "documentation": "Optional"}, {"label": "params", "documentation": "Optional param matchers"}], "documentation": "Mocks a statement function"}},
+    {"namespace": "Mock", "name": "GetFuncCaptures", "detail": "Get captured params from function mock", "params": "(functionName, instanceId?)", "returns": {"type": "Array"}, "documentation": "Returns array of captured parameter maps from Mock.Func calls.", "signature": {"label": "@.Mock.GetFuncCaptures(functionName: string, instanceId?: number)", "parameters": [{"label": "functionName", "documentation": "Same name used in Mock.Func"}, {"label": "instanceId", "documentation": "Optional instance ID from Mock.Func"}], "documentation": "Gets captured params from function mock"}},
+    {"namespace": "Mock", "name": "GetAnnotationCaptures", "detail": "Get captured params from annotation mock", "params": "(annotationName, instanceId?)", "returns": {"type": "Array"}, "documentation": "Returns array of captured parameter maps from Mock.Annotation calls.", "signature": {"label": "@.Mock.GetAnnotationCaptures(annotationName: string, instanceId?: number)", "parameters": [{"label": "annotationName", "documentation": "Same name used in Mock.Annotation"}, {"label": "instanceId", "documentation": "Optional instance ID from Mock.Annotation"}], "documentation": "Gets captured params from annotation mock"}},
+    {"namespace": "Mock", "name": "GetStatementFuncCaptures", "detail": "Get captured params from statement func mock", "params": "(functionName, instanceId?)", "returns": {"type": "Array"}, "documentation": "Returns array of captured parameter maps from Mock.StatementFunc calls.", "signature": {"label": "@.Mock.GetStatementFuncCaptures(functionName: string, instanceId?: number)", "parameters": [{"label": "functionName", "documentation": "Same name used in Mock.StatementFunc"}, {"label": "instanceId", "documentation": "Optional instance ID"}], "documentation": "Gets captured params from statement func mock"}}
+  ],
+  "services": [
+    {"name": "This", "detail": "Call functions in current script", "documentation": "Provides access to functions defined in the current ISL file.\n\nType `@.This.` to see available functions."},
+    {"name": "Date", "detail": "Date/time operations", "documentation": "Date and time operations (UTC).\n\n**Methods:**\n- `Now()` - Current date/time\n- `parse(string, format)` - Parse date\n- `fromEpochSeconds(seconds)`\n- `fromEpochMillis(millis)`"},
+    {"name": "Math", "detail": "Math operations", "documentation": "Mathematical operations on arrays and numbers.\n\nUse with arrays: `$total: $prices | Math.sum(0)`\n\nRandom numbers: `Math.RandInt(1, 100)`"},
+    {"name": "String", "detail": "String operations", "documentation": "String manipulation functions."},
+    {"name": "Array", "detail": "Array operations", "documentation": "Array manipulation functions.\n\n`Array.range(0, 10, 1)` creates array [0..9]"},
+    {"name": "Json", "detail": "JSON operations", "documentation": "JSON parsing and serialization.\n\n```isl\n$obj: @.Json.parse($jsonString);\n```"},
+    {"name": "Xml", "detail": "XML operations", "documentation": "XML parsing and generation.\n\nAttributes use @ prefix, text uses #text"},
+    {"name": "Csv", "detail": "CSV operations", "documentation": "CSV parsing via modifiers.\n\n```isl\n$data: $csvText | csv.parsemultiline;\n$rowIndex: $csvText | csv.findrow({seek: [\"id\"], maxRows: 100});\n```"},
+    {"name": "Crypto", "detail": "Cryptography functions", "documentation": "Hash and crypto via modifiers: Crypto.sha256, Crypto.sha1, Crypto.md5, Crypto.hmacsha256, Crypto.decrypt, Crypto.verify, etc."},
+    {"name": "Run", "detail": "Runtime helpers", "documentation": "**Methods:**\n- `Run.Sleep(ms)` - Pause execution"},
+    {"name": "UUID", "detail": "UUID generation", "documentation": "**Methods:**\n- `UUID.New()` - New UUID string"},
+    {"name": "Zip", "detail": "Zip archive", "documentation": "**Methods:**\n- `Zip.Start()` - Create zip; then `| zip.add(\"name\", content) | zip.close`\n- `$bytes | unzip` - Unzip to array of {name, content}"},
+    {"name": "Assert", "detail": "Test assertions (isl-test)", "documentation": "Assertion functions for ISL tests. Use with `isl-test`.\n\n**Equality:** equal, notEqual, isNull, notNull\n**Comparison:** lessThan, lessThanOrEqual, greaterThan, greaterThanOrEqual\n**String:** matches, notMatches, contains, notContains, startsWith, notStartsWith, endsWith, notEndsWith\n**Collection:** in, notIn\n**Type:** isType, isNotType"},
+    {"name": "Load", "detail": "Load resources from files (test context)", "documentation": "Load files relative to current ISL file. Available in test context only.\n\n**Methods:**\n- `Load.From(fileName)` - Load .json, .yaml, .yml, or .csv; converts to JSON"},
+    {"name": "Mock", "detail": "Mock functions and annotations (test context)", "documentation": "Mocking for ISL tests. Available in test context only.\n\n**Methods:**\n- `Mock.Func(functionName, returnValue?, ...params)` - Mock a function\n- `Mock.Annotation(annotationName, returnValue?, ...params)` - Mock an annotation\n- `Mock.StatementFunc(functionName, returnValue?, ...params)` - Mock a statement function\n- `Mock.GetFuncCaptures(functionName, instanceId?)` - Get captured params\n- `Mock.GetAnnotationCaptures(annotationName, instanceId?)` - Get annotation captures\n- `Mock.GetStatementFuncCaptures(functionName, instanceId?)` - Get statement func captures"}
+  ],
+  "annotations": [
+    {"name": "test", "detail": "Mark function as test", "insertText": "test", "documentation": "Marks a function as a test. Discovered by Test Explorer.\n\n**Forms:**\n- `@test` - Use function name as test name\n- `@test(\"Custom name\")` - Custom test name\n- `@test(name, group)` - Name and group\n- `@test({ name: \"x\", group: \"y\" })` - Object form"},
+    {"name": "setup", "detail": "Setup runs before each test", "insertText": "setup", "documentation": "Marks a function as setup. Runs before each test in the file.\n\nUse with `fun setup()` to initialize shared state."}
+  ]
+}
diff --git a/plugin/language-configuration.json b/plugin/language-configuration.json
new file mode 100644
index 0000000..1d26a73
--- /dev/null
+++ b/plugin/language-configuration.json
@@ -0,0 +1,39 @@
+{
+  "comments": {
+    "lineComment": "//",
+    "blockComment": ["/*", "*/"]
+  },
+  "brackets": [
+    ["{", "}"],
+    ["[", "]"],
+    ["(", ")"]
+  ],
+  "autoClosingPairs": [
+    { "open": "{", "close": "}" },
+    { "open": "[", "close": "]" },
+    { "open": "(", "close": ")" },
+    { "open": "\"", "close": "\"", "notIn": ["string"] },
+    { "open": "'", "close": "'", "notIn": ["string"] },
+    { "open": "`", "close": "`", "notIn": ["string"] }
+  ],
+  "surroundingPairs": [
+    ["{", "}"],
+    ["[", "]"],
+    ["(", ")"],
+    ["\"", "\""],
+    ["'", "'"],
+    ["`", "`"]
+  ],
+  "folding": {
+    "markers": {
+      "start": "^\\s*//\\s*#?region\\b",
+      "end": "^\\s*//\\s*#?endregion\\b"
+    }
+  },
+  "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)",
+  "indentationRules": {
+    "increaseIndentPattern": "^((?!\\/\\/).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)$",
+    "decreaseIndentPattern": "^((?!.*?\\/\\*).*\\*/)?\\s*[\\}\\]].*$"
+  }
+}
+
diff --git a/plugin/lib/README.md b/plugin/lib/README.md
new file mode 100644
index 0000000..cf28eb6
--- /dev/null
+++ b/plugin/lib/README.md
@@ -0,0 +1,83 @@
+# ISL Runtime Library
+
+This directory contains the embedded ISL command-line runtime that allows the extension to execute ISL transformations and run tests without requiring users to build the project.
+
+## Contents
+
+- `isl-cmd-all.jar` - ISL command-line tool with all dependencies (shadow JAR, ~35MB)
+- `isl.bat` - Windows script to run the JAR from the command line
+- `isl.sh` - Linux/Mac script to run the JAR from the command line
+
+## How It Works
+
+When you click the "▶ Run" button above an ISL function, the extension:
+
+1. Prompts for input JSON
+2. Saves the input to a temporary file
+3. Executes: `java -jar isl-cmd-all.jar transform .isl -i input.json -f `
+4. Shows the output in the terminal
+
+The ISL CLI also supports running tests: `java -jar isl-cmd-all.jar test [path]`
+
+### Command-line scripts
+
+From a terminal you can use the bundled scripts (no need to type `java -jar`):
+
+- **Windows:** `lib\isl.bat --version`, `lib\isl.bat transform script.isl -i input.json`, `lib\isl.bat test`
+- **Linux/Mac:** `./lib/isl.sh --version`, `./lib/isl.sh transform script.isl -i input.json`, `./lib/isl.sh test`
+
+Ensure Java is on your PATH, or set `JAVA_HOME`. The scripts run the JAR in the same directory.
+
+## Requirements
+
+- **Java 11 or later** must be installed and available in PATH
+- Or configure `isl.execution.javaHome` in VS Code settings
+
+## Publishing
+Before publishing package the [latest ISL](https://central.sonatype.com/search?namespace=com.intuit.isl) into the lib of the plugin.
+```bash
+./gradlew :plugin:build-isl-runtime:buildIslRuntime -PislVersion=X.X.X
+```
+
+## Building
+
+Build the fat JAR and copy it here using:
+
+```bash
+./gradlew buildIslRuntimeLocal
+```
+
+This builds the shadow JAR from `isl-cmd` and copies it to `plugin/lib/isl-cmd-all.jar`.
+
+To build only the JAR (without copying):
+
+```bash
+./gradlew :isl-cmd:shadowJar
+```
+
+The output is `isl-cmd/build/libs/isl--all.jar`.
+
+## Version
+
+Built from ISL version: **1.1.0**
+
+## Distribution
+
+This JAR is included in the extension package (`.vsix`) so end users don't need to:
+- Clone the ISL repository
+- Build the project with Gradle
+- Configure complex paths
+
+Everything works out of the box with just Java installed!
+
+## Size Note
+
+The JAR is approximately 35MB because it includes all dependencies:
+- ISL transform engine
+- ANTLR parser
+- Kotlin runtime
+- Jackson JSON library
+- All other required libraries
+
+This makes the extension self-contained and easy to distribute.
+
diff --git a/plugin/lib/isl b/plugin/lib/isl
new file mode 100644
index 0000000..5ac7e12
--- /dev/null
+++ b/plugin/lib/isl
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+# ISL launcher (no extension) - when lib is on PATH you can run "isl" directly.
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
+exec "$SCRIPT_DIR/isl.sh" "$@"
diff --git a/plugin/lib/isl.bat b/plugin/lib/isl.bat
new file mode 100644
index 0000000..9d61df2
--- /dev/null
+++ b/plugin/lib/isl.bat
@@ -0,0 +1,21 @@
+@echo off
+setlocal
+REM ISL Command Line Runner for Windows (plugin lib - runs embedded JAR)
+REM
+REM Usage: isl.bat [command] [options]
+REM Examples:
+REM   isl.bat --version
+REM   isl.bat info
+REM   isl.bat transform script.isl -i input.json
+REM   isl.bat test [path]
+
+set SCRIPT_DIR=%~dp0
+set JAR_FILE=%SCRIPT_DIR%isl-cmd-all.jar
+
+if not exist "%JAR_FILE%" (
+    echo Error: isl-cmd-all.jar not found in %SCRIPT_DIR%
+    exit /b 1
+)
+
+java -jar "%JAR_FILE%" %*
+endlocal
diff --git a/plugin/lib/isl.sh b/plugin/lib/isl.sh
new file mode 100644
index 0000000..5598dd8
--- /dev/null
+++ b/plugin/lib/isl.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+# ISL Command Line Runner for Linux/Mac (plugin lib - runs embedded JAR)
+#
+# Usage: ./isl.sh [command] [options]
+# Examples:
+#   ./isl.sh --version
+#   ./isl.sh info
+#   ./isl.sh transform script.isl -i input.json
+#   ./isl.sh test [path]
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
+JAR_FILE="$SCRIPT_DIR/isl-cmd-all.jar"
+
+if [ ! -f "$JAR_FILE" ]; then
+    echo "Error: isl-cmd-all.jar not found in $SCRIPT_DIR"
+    exit 1
+fi
+
+exec java -jar "$JAR_FILE" "$@"
diff --git a/plugin/package-lock.json b/plugin/package-lock.json
new file mode 100644
index 0000000..8c3dc3f
--- /dev/null
+++ b/plugin/package-lock.json
@@ -0,0 +1,6957 @@
+{
+  "name": "isl-language-support",
+  "version": "1.2.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "isl-language-support",
+      "version": "1.2.0",
+      "dependencies": {
+        "js-yaml": "^4.1.0",
+        "vscode-languageclient": "^8.1.0"
+      },
+      "devDependencies": {
+        "@types/js-yaml": "^4.0.0",
+        "@types/node": "^18.0.0",
+        "@types/vscode": "^1.75.0",
+        "@typescript-eslint/eslint-plugin": "^6.0.0",
+        "@typescript-eslint/parser": "^6.0.0",
+        "@vscode/vsce": "^2.32.0",
+        "esbuild": "^0.24.0",
+        "eslint": "^8.40.0",
+        "ovsx": "^0.10.7",
+        "typescript": "^5.0.0"
+      },
+      "engines": {
+        "vscode": "^1.75.0"
+      }
+    },
+    "node_modules/@azu/format-text": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz",
+      "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@azu/style-format": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz",
+      "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==",
+      "dev": true,
+      "license": "WTFPL",
+      "dependencies": {
+        "@azu/format-text": "^1.0.1"
+      }
+    },
+    "node_modules/@azure/abort-controller": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
+      "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/@azure/core-auth": {
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz",
+      "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@azure/abort-controller": "^2.1.2",
+        "@azure/core-util": "^1.13.0",
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@azure/core-client": {
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz",
+      "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@azure/abort-controller": "^2.1.2",
+        "@azure/core-auth": "^1.10.0",
+        "@azure/core-rest-pipeline": "^1.22.0",
+        "@azure/core-tracing": "^1.3.0",
+        "@azure/core-util": "^1.13.0",
+        "@azure/logger": "^1.3.0",
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@azure/core-rest-pipeline": {
+      "version": "1.22.2",
+      "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz",
+      "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@azure/abort-controller": "^2.1.2",
+        "@azure/core-auth": "^1.10.0",
+        "@azure/core-tracing": "^1.3.0",
+        "@azure/core-util": "^1.13.0",
+        "@azure/logger": "^1.3.0",
+        "@typespec/ts-http-runtime": "^0.3.0",
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@azure/core-tracing": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz",
+      "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@azure/core-util": {
+      "version": "1.13.1",
+      "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz",
+      "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@azure/abort-controller": "^2.1.2",
+        "@typespec/ts-http-runtime": "^0.3.0",
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@azure/identity": {
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz",
+      "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@azure/abort-controller": "^2.0.0",
+        "@azure/core-auth": "^1.9.0",
+        "@azure/core-client": "^1.9.2",
+        "@azure/core-rest-pipeline": "^1.17.0",
+        "@azure/core-tracing": "^1.0.0",
+        "@azure/core-util": "^1.11.0",
+        "@azure/logger": "^1.0.0",
+        "@azure/msal-browser": "^4.2.0",
+        "@azure/msal-node": "^3.5.0",
+        "open": "^10.1.0",
+        "tslib": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@azure/logger": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz",
+      "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typespec/ts-http-runtime": "^0.3.0",
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@azure/msal-browser": {
+      "version": "4.26.2",
+      "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.26.2.tgz",
+      "integrity": "sha512-F2U1mEAFsYGC5xzo1KuWc/Sy3CRglU9Ql46cDUx8x/Y3KnAIr1QAq96cIKCk/ZfnVxlvprXWRjNKoEpgLJXLhg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@azure/msal-common": "15.13.2"
+      },
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/@azure/msal-common": {
+      "version": "15.13.2",
+      "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.2.tgz",
+      "integrity": "sha512-cNwUoCk3FF8VQ7Ln/MdcJVIv3sF73/OT86cRH81ECsydh7F4CNfIo2OAx6Cegtg8Yv75x4506wN4q+Emo6erOA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/@azure/msal-node": {
+      "version": "3.8.3",
+      "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.3.tgz",
+      "integrity": "sha512-Ul7A4gwmaHzYWj2Z5xBDly/W8JSC1vnKgJ898zPMZr0oSf1ah0tiL15sytjycU/PMhDZAlkWtEL1+MzNMU6uww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@azure/msal-common": "15.13.2",
+        "jsonwebtoken": "^9.0.0",
+        "uuid": "^8.3.0"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+      "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.27.1",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@emnapi/core": {
+      "version": "1.7.1",
+      "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
+      "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/wasi-threads": "1.1.0",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/runtime": {
+      "version": "1.7.1",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
+      "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/wasi-threads": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
+      "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
+      "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
+      "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
+      "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
+      "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
+      "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
+      "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
+      "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
+      "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
+      "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
+      "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
+      "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
+      "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
+      "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
+      "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
+      "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
+      "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
+      "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
+      "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
+      "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
+      "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
+      "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
+      "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
+      "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
+      "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
+      "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils": {
+      "version": "4.9.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+      "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eslint-visitor-keys": "^3.4.3"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+      }
+    },
+    "node_modules/@eslint-community/regexpp": {
+      "version": "4.12.2",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+      "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@eslint/eslintrc": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+      "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^6.12.4",
+        "debug": "^4.3.2",
+        "espree": "^9.6.0",
+        "globals": "^13.19.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "minimatch": "^3.1.2",
+        "strip-json-comments": "^3.1.1"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/@eslint/js": {
+      "version": "8.57.1",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
+      "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@humanwhocodes/config-array": {
+      "version": "0.13.0",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+      "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+      "deprecated": "Use @eslint/config-array instead",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanwhocodes/object-schema": "^2.0.3",
+        "debug": "^4.3.1",
+        "minimatch": "^3.0.5"
+      },
+      "engines": {
+        "node": ">=10.10.0"
+      }
+    },
+    "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.22"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@humanwhocodes/object-schema": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+      "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+      "deprecated": "Use @eslint/object-schema instead",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@isaacs/balanced-match": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
+      "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/@isaacs/brace-expansion": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
+      "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@isaacs/balanced-match": "^4.0.1"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+      "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@isaacs/cliui/node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+      "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/@napi-rs/wasm-runtime": {
+      "version": "0.2.12",
+      "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
+      "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/core": "^1.4.3",
+        "@emnapi/runtime": "^1.4.3",
+        "@tybys/wasm-util": "^0.10.0"
+      }
+    },
+    "node_modules/@node-rs/crc32": {
+      "version": "1.10.6",
+      "resolved": "https://registry.npmjs.org/@node-rs/crc32/-/crc32-1.10.6.tgz",
+      "integrity": "sha512-+llXfqt+UzgoDzT9of5vPQPGqTAVCohU74I9zIBkNo5TH6s2P31DFJOGsJQKN207f0GHnYv5pV3wh3BCY/un/A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      },
+      "optionalDependencies": {
+        "@node-rs/crc32-android-arm-eabi": "1.10.6",
+        "@node-rs/crc32-android-arm64": "1.10.6",
+        "@node-rs/crc32-darwin-arm64": "1.10.6",
+        "@node-rs/crc32-darwin-x64": "1.10.6",
+        "@node-rs/crc32-freebsd-x64": "1.10.6",
+        "@node-rs/crc32-linux-arm-gnueabihf": "1.10.6",
+        "@node-rs/crc32-linux-arm64-gnu": "1.10.6",
+        "@node-rs/crc32-linux-arm64-musl": "1.10.6",
+        "@node-rs/crc32-linux-x64-gnu": "1.10.6",
+        "@node-rs/crc32-linux-x64-musl": "1.10.6",
+        "@node-rs/crc32-wasm32-wasi": "1.10.6",
+        "@node-rs/crc32-win32-arm64-msvc": "1.10.6",
+        "@node-rs/crc32-win32-ia32-msvc": "1.10.6",
+        "@node-rs/crc32-win32-x64-msvc": "1.10.6"
+      }
+    },
+    "node_modules/@node-rs/crc32-android-arm-eabi": {
+      "version": "1.10.6",
+      "resolved": "https://registry.npmjs.org/@node-rs/crc32-android-arm-eabi/-/crc32-android-arm-eabi-1.10.6.tgz",
+      "integrity": "sha512-vZAMuJXm3TpWPOkkhxdrofWDv+Q+I2oO7ucLRbXyAPmXFNDhHtBxbO1rk9Qzz+M3eep8ieS4/+jCL1Q0zacNMQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/crc32-android-arm64": {
+      "version": "1.10.6",
+      "resolved": "https://registry.npmjs.org/@node-rs/crc32-android-arm64/-/crc32-android-arm64-1.10.6.tgz",
+      "integrity": "sha512-Vl/JbjCinCw/H9gEpZveWCMjxjcEChDcDBM8S4hKay5yyoRCUHJPuKr4sjVDBeOm+1nwU3oOm6Ca8dyblwp4/w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/crc32-darwin-arm64": {
+      "version": "1.10.6",
+      "resolved": "https://registry.npmjs.org/@node-rs/crc32-darwin-arm64/-/crc32-darwin-arm64-1.10.6.tgz",
+      "integrity": "sha512-kARYANp5GnmsQiViA5Qu74weYQ3phOHSYQf0G+U5wB3NB5JmBHnZcOc46Ig21tTypWtdv7u63TaltJQE41noyg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/crc32-darwin-x64": {
+      "version": "1.10.6",
+      "resolved": "https://registry.npmjs.org/@node-rs/crc32-darwin-x64/-/crc32-darwin-x64-1.10.6.tgz",
+      "integrity": "sha512-Q99bevJVMfLTISpkpKBlXgtPUItrvTWKFyiqoKH5IvscZmLV++NH4V13Pa17GTBmv9n18OwzgQY4/SRq6PQNVA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/crc32-freebsd-x64": {
+      "version": "1.10.6",
+      "resolved": "https://registry.npmjs.org/@node-rs/crc32-freebsd-x64/-/crc32-freebsd-x64-1.10.6.tgz",
+      "integrity": "sha512-66hpawbNjrgnS9EDMErta/lpaqOMrL6a6ee+nlI2viduVOmRZWm9Rg9XdGTK/+c4bQLdtC6jOd+Kp4EyGRYkAg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/crc32-linux-arm-gnueabihf": {
+      "version": "1.10.6",
+      "resolved": "https://registry.npmjs.org/@node-rs/crc32-linux-arm-gnueabihf/-/crc32-linux-arm-gnueabihf-1.10.6.tgz",
+      "integrity": "sha512-E8Z0WChH7X6ankbVm8J/Yym19Cq3otx6l4NFPS6JW/cWdjv7iw+Sps2huSug+TBprjbcEA+s4TvEwfDI1KScjg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/crc32-linux-arm64-gnu": {
+      "version": "1.10.6",
+      "resolved": "https://registry.npmjs.org/@node-rs/crc32-linux-arm64-gnu/-/crc32-linux-arm64-gnu-1.10.6.tgz",
+      "integrity": "sha512-LmWcfDbqAvypX0bQjQVPmQGazh4dLiVklkgHxpV4P0TcQ1DT86H/SWpMBMs/ncF8DGuCQ05cNyMv1iddUDugoQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/crc32-linux-arm64-musl": {
+      "version": "1.10.6",
+      "resolved": "https://registry.npmjs.org/@node-rs/crc32-linux-arm64-musl/-/crc32-linux-arm64-musl-1.10.6.tgz",
+      "integrity": "sha512-k8ra/bmg0hwRrIEE8JL1p32WfaN9gDlUUpQRWsbxd1WhjqvXea7kKO6K4DwVxyxlPhBS9Gkb5Urq7Y4mXANzaw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/crc32-linux-x64-gnu": {
+      "version": "1.10.6",
+      "resolved": "https://registry.npmjs.org/@node-rs/crc32-linux-x64-gnu/-/crc32-linux-x64-gnu-1.10.6.tgz",
+      "integrity": "sha512-IfjtqcuFK7JrSZ9mlAFhb83xgium30PguvRjIMI45C3FJwu18bnLk1oR619IYb/zetQT82MObgmqfKOtgemEKw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/crc32-linux-x64-musl": {
+      "version": "1.10.6",
+      "resolved": "https://registry.npmjs.org/@node-rs/crc32-linux-x64-musl/-/crc32-linux-x64-musl-1.10.6.tgz",
+      "integrity": "sha512-LbFYsA5M9pNunOweSt6uhxenYQF94v3bHDAQRPTQ3rnjn+mK6IC7YTAYoBjvoJP8lVzcvk9hRj8wp4Jyh6Y80g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/crc32-wasm32-wasi": {
+      "version": "1.10.6",
+      "resolved": "https://registry.npmjs.org/@node-rs/crc32-wasm32-wasi/-/crc32-wasm32-wasi-1.10.6.tgz",
+      "integrity": "sha512-KaejdLgHMPsRaxnM+OG9L9XdWL2TabNx80HLdsCOoX9BVhEkfh39OeahBo8lBmidylKbLGMQoGfIKDjq0YMStw==",
+      "cpu": [
+        "wasm32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@napi-rs/wasm-runtime": "^0.2.5"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@node-rs/crc32-win32-arm64-msvc": {
+      "version": "1.10.6",
+      "resolved": "https://registry.npmjs.org/@node-rs/crc32-win32-arm64-msvc/-/crc32-win32-arm64-msvc-1.10.6.tgz",
+      "integrity": "sha512-x50AXiSxn5Ccn+dCjLf1T7ZpdBiV1Sp5aC+H2ijhJO4alwznvXgWbopPRVhbp2nj0i+Gb6kkDUEyU+508KAdGQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/crc32-win32-ia32-msvc": {
+      "version": "1.10.6",
+      "resolved": "https://registry.npmjs.org/@node-rs/crc32-win32-ia32-msvc/-/crc32-win32-ia32-msvc-1.10.6.tgz",
+      "integrity": "sha512-DpDxQLaErJF9l36aghe1Mx+cOnYLKYo6qVPqPL9ukJ5rAGLtCdU0C+Zoi3gs9ySm8zmbFgazq/LvmsZYU42aBw==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/crc32-win32-x64-msvc": {
+      "version": "1.10.6",
+      "resolved": "https://registry.npmjs.org/@node-rs/crc32-win32-x64-msvc/-/crc32-win32-x64-msvc-1.10.6.tgz",
+      "integrity": "sha512-5B1vXosIIBw1m2Rcnw62IIfH7W9s9f7H7Ma0rRuhT8HR4Xh8QCgw6NJSI2S2MCngsGktYnAhyUvs81b7efTyQw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@secretlint/config-creator": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz",
+      "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@secretlint/types": "^10.2.2"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@secretlint/config-loader": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz",
+      "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@secretlint/profiler": "^10.2.2",
+        "@secretlint/resolver": "^10.2.2",
+        "@secretlint/types": "^10.2.2",
+        "ajv": "^8.17.1",
+        "debug": "^4.4.1",
+        "rc-config-loader": "^4.1.3"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@secretlint/config-loader/node_modules/ajv": {
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+      "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "fast-uri": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/@secretlint/config-loader/node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@secretlint/core": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz",
+      "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@secretlint/profiler": "^10.2.2",
+        "@secretlint/types": "^10.2.2",
+        "debug": "^4.4.1",
+        "structured-source": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@secretlint/formatter": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz",
+      "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@secretlint/resolver": "^10.2.2",
+        "@secretlint/types": "^10.2.2",
+        "@textlint/linter-formatter": "^15.2.0",
+        "@textlint/module-interop": "^15.2.0",
+        "@textlint/types": "^15.2.0",
+        "chalk": "^5.4.1",
+        "debug": "^4.4.1",
+        "pluralize": "^8.0.0",
+        "strip-ansi": "^7.1.0",
+        "table": "^6.9.0",
+        "terminal-link": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@secretlint/formatter/node_modules/ansi-regex": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+      "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/@secretlint/formatter/node_modules/chalk": {
+      "version": "5.6.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+      "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.17.0 || ^14.13 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/@secretlint/formatter/node_modules/strip-ansi": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+      "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/@secretlint/node": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz",
+      "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@secretlint/config-loader": "^10.2.2",
+        "@secretlint/core": "^10.2.2",
+        "@secretlint/formatter": "^10.2.2",
+        "@secretlint/profiler": "^10.2.2",
+        "@secretlint/source-creator": "^10.2.2",
+        "@secretlint/types": "^10.2.2",
+        "debug": "^4.4.1",
+        "p-map": "^7.0.3"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@secretlint/profiler": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz",
+      "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@secretlint/resolver": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz",
+      "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@secretlint/secretlint-formatter-sarif": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz",
+      "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "node-sarif-builder": "^3.2.0"
+      }
+    },
+    "node_modules/@secretlint/secretlint-rule-no-dotenv": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz",
+      "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@secretlint/types": "^10.2.2"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@secretlint/secretlint-rule-preset-recommend": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz",
+      "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@secretlint/source-creator": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz",
+      "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@secretlint/types": "^10.2.2",
+        "istextorbinary": "^9.5.0"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@secretlint/types": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz",
+      "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@sindresorhus/merge-streams": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
+      "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@textlint/ast-node-types": {
+      "version": "15.4.0",
+      "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.4.0.tgz",
+      "integrity": "sha512-IqY8i7IOGuvy05wZxISB7Me1ZyrvhaQGgx6DavfQjH3cfwpPFdDbDYmMXMuSv2xLS1kDB1kYKBV7fL2Vi16lRA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@textlint/linter-formatter": {
+      "version": "15.4.0",
+      "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.4.0.tgz",
+      "integrity": "sha512-rfqOZmnI1Wwc/Pa4LK+vagvVPmvxf9oRsBRqIOB04DwhucingZyAIJI/TyG18DIDYbP2aFXBZ3oOvyAxHe/8PQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@azu/format-text": "^1.0.2",
+        "@azu/style-format": "^1.0.1",
+        "@textlint/module-interop": "15.4.0",
+        "@textlint/resolver": "15.4.0",
+        "@textlint/types": "15.4.0",
+        "chalk": "^4.1.2",
+        "debug": "^4.4.3",
+        "js-yaml": "^3.14.1",
+        "lodash": "^4.17.21",
+        "pluralize": "^2.0.0",
+        "string-width": "^4.2.3",
+        "strip-ansi": "^6.0.1",
+        "table": "^6.9.0",
+        "text-table": "^0.2.0"
+      }
+    },
+    "node_modules/@textlint/linter-formatter/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/@textlint/linter-formatter/node_modules/argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "node_modules/@textlint/linter-formatter/node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/@textlint/linter-formatter/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/@textlint/linter-formatter/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@textlint/linter-formatter/node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@textlint/linter-formatter/node_modules/js-yaml": {
+      "version": "3.14.2",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+      "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/@textlint/linter-formatter/node_modules/pluralize": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz",
+      "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@textlint/linter-formatter/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@textlint/module-interop": {
+      "version": "15.4.0",
+      "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.4.0.tgz",
+      "integrity": "sha512-uGf+SFIfzOLCbZI0gp+2NLsrkSArsvEWulPP6lJuKp7yRHadmy7Xf/YHORe46qhNyyxc8PiAfiixHJSaHGUrGg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@textlint/resolver": {
+      "version": "15.4.0",
+      "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.4.0.tgz",
+      "integrity": "sha512-Vh/QceKZQHFJFG4GxxIsKM1Xhwv93mbtKHmFE5/ybal1mIKHdqF03Z9Guaqt6Sx/AeNUshq0hkMOEhEyEWnehQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@textlint/types": {
+      "version": "15.4.0",
+      "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.4.0.tgz",
+      "integrity": "sha512-ZMwJgw/xjxJufOD+IB7I2Enl9Si4Hxo04B76RwUZ5cKBKzOPcmd6WvGe2F7jqdgmTdGnfMU+Bo/joQrjPNIWqg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@textlint/ast-node-types": "15.4.0"
+      }
+    },
+    "node_modules/@tybys/wasm-util": {
+      "version": "0.10.1",
+      "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+      "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@types/js-yaml": {
+      "version": "4.0.9",
+      "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
+      "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "18.19.130",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
+      "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
+    },
+    "node_modules/@types/normalize-package-data": {
+      "version": "2.4.4",
+      "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
+      "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/sarif": {
+      "version": "2.1.7",
+      "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz",
+      "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/semver": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
+      "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/vscode": {
+      "version": "1.106.1",
+      "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.106.1.tgz",
+      "integrity": "sha512-R/HV8u2h8CAddSbX8cjpdd7B8/GnE4UjgjpuGuHcbp1xV6yh4OeqU4L1pKjlwujCrSFS0MOpwJAIs/NexMB1fQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
+      "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/regexpp": "^4.5.1",
+        "@typescript-eslint/scope-manager": "6.21.0",
+        "@typescript-eslint/type-utils": "6.21.0",
+        "@typescript-eslint/utils": "6.21.0",
+        "@typescript-eslint/visitor-keys": "6.21.0",
+        "debug": "^4.3.4",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.2.4",
+        "natural-compare": "^1.4.0",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
+        "eslint": "^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/parser": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
+      "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "peer": true,
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "6.21.0",
+        "@typescript-eslint/types": "6.21.0",
+        "@typescript-eslint/typescript-estree": "6.21.0",
+        "@typescript-eslint/visitor-keys": "6.21.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/scope-manager": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
+      "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "6.21.0",
+        "@typescript-eslint/visitor-keys": "6.21.0"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/type-utils": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz",
+      "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/typescript-estree": "6.21.0",
+        "@typescript-eslint/utils": "6.21.0",
+        "debug": "^4.3.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
+      "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
+      "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "@typescript-eslint/types": "6.21.0",
+        "@typescript-eslint/visitor-keys": "6.21.0",
+        "debug": "^4.3.4",
+        "globby": "^11.1.0",
+        "is-glob": "^4.0.3",
+        "minimatch": "9.0.3",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/utils": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
+      "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@types/json-schema": "^7.0.12",
+        "@types/semver": "^7.5.0",
+        "@typescript-eslint/scope-manager": "6.21.0",
+        "@typescript-eslint/types": "6.21.0",
+        "@typescript-eslint/typescript-estree": "6.21.0",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
+      "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "6.21.0",
+        "eslint-visitor-keys": "^3.4.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typespec/ts-http-runtime": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.2.tgz",
+      "integrity": "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "http-proxy-agent": "^7.0.0",
+        "https-proxy-agent": "^7.0.0",
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@ungap/structured-clone": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+      "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/@vscode/vsce": {
+      "version": "2.32.0",
+      "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.32.0.tgz",
+      "integrity": "sha512-3EFJfsgrSftIqt3EtdRcAygy/OJ3hstyI1cDmIgkU9CFZW5C+3djr6mfosndCUqcVYuyjmxOK1xmFp/Bq7+NIg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@azure/identity": "^4.1.0",
+        "@vscode/vsce-sign": "^2.0.0",
+        "azure-devops-node-api": "^12.5.0",
+        "chalk": "^2.4.2",
+        "cheerio": "^1.0.0-rc.9",
+        "cockatiel": "^3.1.2",
+        "commander": "^6.2.1",
+        "form-data": "^4.0.0",
+        "glob": "^7.0.6",
+        "hosted-git-info": "^4.0.2",
+        "jsonc-parser": "^3.2.0",
+        "leven": "^3.1.0",
+        "markdown-it": "^12.3.2",
+        "mime": "^1.3.4",
+        "minimatch": "^3.0.3",
+        "parse-semver": "^1.1.1",
+        "read": "^1.0.7",
+        "semver": "^7.5.2",
+        "tmp": "^0.2.1",
+        "typed-rest-client": "^1.8.4",
+        "url-join": "^4.0.1",
+        "xml2js": "^0.5.0",
+        "yauzl": "^2.3.1",
+        "yazl": "^2.2.2"
+      },
+      "bin": {
+        "vsce": "vsce"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "optionalDependencies": {
+        "keytar": "^7.7.0"
+      }
+    },
+    "node_modules/@vscode/vsce-sign": {
+      "version": "2.0.9",
+      "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz",
+      "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "SEE LICENSE IN LICENSE.txt",
+      "optionalDependencies": {
+        "@vscode/vsce-sign-alpine-arm64": "2.0.6",
+        "@vscode/vsce-sign-alpine-x64": "2.0.6",
+        "@vscode/vsce-sign-darwin-arm64": "2.0.6",
+        "@vscode/vsce-sign-darwin-x64": "2.0.6",
+        "@vscode/vsce-sign-linux-arm": "2.0.6",
+        "@vscode/vsce-sign-linux-arm64": "2.0.6",
+        "@vscode/vsce-sign-linux-x64": "2.0.6",
+        "@vscode/vsce-sign-win32-arm64": "2.0.6",
+        "@vscode/vsce-sign-win32-x64": "2.0.6"
+      }
+    },
+    "node_modules/@vscode/vsce-sign-alpine-arm64": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz",
+      "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "SEE LICENSE IN LICENSE.txt",
+      "optional": true,
+      "os": [
+        "alpine"
+      ]
+    },
+    "node_modules/@vscode/vsce-sign-alpine-x64": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz",
+      "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "SEE LICENSE IN LICENSE.txt",
+      "optional": true,
+      "os": [
+        "alpine"
+      ]
+    },
+    "node_modules/@vscode/vsce-sign-darwin-arm64": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz",
+      "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "SEE LICENSE IN LICENSE.txt",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@vscode/vsce-sign-darwin-x64": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz",
+      "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "SEE LICENSE IN LICENSE.txt",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@vscode/vsce-sign-linux-arm": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz",
+      "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "SEE LICENSE IN LICENSE.txt",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@vscode/vsce-sign-linux-arm64": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz",
+      "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "SEE LICENSE IN LICENSE.txt",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@vscode/vsce-sign-linux-x64": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz",
+      "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "SEE LICENSE IN LICENSE.txt",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@vscode/vsce-sign-win32-arm64": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz",
+      "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "SEE LICENSE IN LICENSE.txt",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@vscode/vsce-sign-win32-x64": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz",
+      "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "SEE LICENSE IN LICENSE.txt",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@vscode/vsce/node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/@vscode/vsce/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+      "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ansi-escapes": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
+      "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "environment": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^1.9.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "license": "Python-2.0"
+    },
+    "node_modules/array-union": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/astral-regex": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+      "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/azure-devops-node-api": {
+      "version": "12.5.0",
+      "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz",
+      "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tunnel": "0.0.6",
+        "typed-rest-client": "^1.8.4"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "license": "MIT"
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/binaryextensions": {
+      "version": "6.11.0",
+      "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz",
+      "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==",
+      "dev": true,
+      "license": "Artistic-2.0",
+      "dependencies": {
+        "editions": "^6.21.0"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "funding": {
+        "url": "https://bevry.me/fund"
+      }
+    },
+    "node_modules/bl": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+      "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "buffer": "^5.5.0",
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.4.0"
+      }
+    },
+    "node_modules/boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/boundary": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz",
+      "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==",
+      "dev": true,
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/buffer": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
+      }
+    },
+    "node_modules/buffer-crc32": {
+      "version": "0.2.13",
+      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+      "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/bundle-name": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
+      "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "run-applescript": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/cheerio": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz",
+      "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cheerio-select": "^2.1.0",
+        "dom-serializer": "^2.0.0",
+        "domhandler": "^5.0.3",
+        "domutils": "^3.2.2",
+        "encoding-sniffer": "^0.2.1",
+        "htmlparser2": "^10.0.0",
+        "parse5": "^7.3.0",
+        "parse5-htmlparser2-tree-adapter": "^7.1.0",
+        "parse5-parser-stream": "^7.1.2",
+        "undici": "^7.12.0",
+        "whatwg-mimetype": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=20.18.1"
+      },
+      "funding": {
+        "url": "https://github.com/cheeriojs/cheerio?sponsor=1"
+      }
+    },
+    "node_modules/cheerio-select": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
+      "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "boolbase": "^1.0.0",
+        "css-select": "^5.1.0",
+        "css-what": "^6.1.0",
+        "domelementtype": "^2.3.0",
+        "domhandler": "^5.0.3",
+        "domutils": "^3.0.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/fb55"
+      }
+    },
+    "node_modules/chownr": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+      "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+      "dev": true,
+      "license": "ISC",
+      "optional": true
+    },
+    "node_modules/ci-info": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
+      "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cockatiel": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz",
+      "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "1.1.3"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/commander": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
+      "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/css-select": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
+      "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "boolbase": "^1.0.0",
+        "css-what": "^6.1.0",
+        "domhandler": "^5.0.2",
+        "domutils": "^3.0.1",
+        "nth-check": "^2.0.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/fb55"
+      }
+    },
+    "node_modules/css-what": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+      "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">= 6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/fb55"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/decompress-response": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+      "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "mimic-response": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/deep-extend": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+      "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/default-browser": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz",
+      "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "bundle-name": "^4.1.0",
+        "default-browser-id": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/default-browser-id": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
+      "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/define-data-property": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/define-lazy-prop": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+      "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/define-properties": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+      "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "define-data-property": "^1.0.1",
+        "has-property-descriptors": "^1.0.0",
+        "object-keys": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dir-glob": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-type": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/doctrine": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "esutils": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/dom-serializer": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+      "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "domelementtype": "^2.3.0",
+        "domhandler": "^5.0.2",
+        "entities": "^4.2.0"
+      },
+      "funding": {
+        "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+      }
+    },
+    "node_modules/domelementtype": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+      "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fb55"
+        }
+      ],
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/domhandler": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+      "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "domelementtype": "^2.3.0"
+      },
+      "engines": {
+        "node": ">= 4"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/domhandler?sponsor=1"
+      }
+    },
+    "node_modules/domutils": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+      "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "dom-serializer": "^2.0.0",
+        "domelementtype": "^2.3.0",
+        "domhandler": "^5.0.3"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/domutils?sponsor=1"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/editions": {
+      "version": "6.22.0",
+      "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz",
+      "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==",
+      "dev": true,
+      "license": "Artistic-2.0",
+      "dependencies": {
+        "version-range": "^4.15.0"
+      },
+      "engines": {
+        "ecmascript": ">= es5",
+        "node": ">=4"
+      },
+      "funding": {
+        "url": "https://bevry.me/fund"
+      }
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/encoding-sniffer": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
+      "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "iconv-lite": "^0.6.3",
+        "whatwg-encoding": "^3.1.1"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
+      }
+    },
+    "node_modules/end-of-stream": {
+      "version": "1.4.5",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+      "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "once": "^1.4.0"
+      }
+    },
+    "node_modules/entities": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/environment": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
+      "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
+      "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.24.2",
+        "@esbuild/android-arm": "0.24.2",
+        "@esbuild/android-arm64": "0.24.2",
+        "@esbuild/android-x64": "0.24.2",
+        "@esbuild/darwin-arm64": "0.24.2",
+        "@esbuild/darwin-x64": "0.24.2",
+        "@esbuild/freebsd-arm64": "0.24.2",
+        "@esbuild/freebsd-x64": "0.24.2",
+        "@esbuild/linux-arm": "0.24.2",
+        "@esbuild/linux-arm64": "0.24.2",
+        "@esbuild/linux-ia32": "0.24.2",
+        "@esbuild/linux-loong64": "0.24.2",
+        "@esbuild/linux-mips64el": "0.24.2",
+        "@esbuild/linux-ppc64": "0.24.2",
+        "@esbuild/linux-riscv64": "0.24.2",
+        "@esbuild/linux-s390x": "0.24.2",
+        "@esbuild/linux-x64": "0.24.2",
+        "@esbuild/netbsd-arm64": "0.24.2",
+        "@esbuild/netbsd-x64": "0.24.2",
+        "@esbuild/openbsd-arm64": "0.24.2",
+        "@esbuild/openbsd-x64": "0.24.2",
+        "@esbuild/sunos-x64": "0.24.2",
+        "@esbuild/win32-arm64": "0.24.2",
+        "@esbuild/win32-ia32": "0.24.2",
+        "@esbuild/win32-x64": "0.24.2"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/eslint": {
+      "version": "8.57.1",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
+      "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+      "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.6.1",
+        "@eslint/eslintrc": "^2.1.4",
+        "@eslint/js": "8.57.1",
+        "@humanwhocodes/config-array": "^0.13.0",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@nodelib/fs.walk": "^1.2.8",
+        "@ungap/structured-clone": "^1.2.0",
+        "ajv": "^6.12.4",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.2",
+        "debug": "^4.3.2",
+        "doctrine": "^3.0.0",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^7.2.2",
+        "eslint-visitor-keys": "^3.4.3",
+        "espree": "^9.6.1",
+        "esquery": "^1.4.2",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^6.0.1",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "globals": "^13.19.0",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "is-path-inside": "^3.0.3",
+        "js-yaml": "^4.1.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.4.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3",
+        "strip-ansi": "^6.0.1",
+        "text-table": "^0.2.0"
+      },
+      "bin": {
+        "eslint": "bin/eslint.js"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/eslint/node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/eslint/node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/eslint/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/eslint/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/eslint/node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/eslint/node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/eslint/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/eslint/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/espree": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+      "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "acorn": "^8.9.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^3.4.1"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "bin": {
+        "esparse": "bin/esparse.js",
+        "esvalidate": "bin/esvalidate.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/esquery": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+      "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "estraverse": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/expand-template": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+      "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+      "dev": true,
+      "license": "(MIT OR WTFPL)",
+      "optional": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.8"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-uri": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+      "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/fastq": {
+      "version": "1.19.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+      "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/fd-slicer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+      "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pend": "~1.2.0"
+      }
+    },
+    "node_modules/file-entry-cache": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+      "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flat-cache": "^3.0.4"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/flat-cache": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+      "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.3",
+        "rimraf": "^3.0.2"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+      "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/foreground-child": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+      "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fs-constants": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/fs-extra": {
+      "version": "11.3.2",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz",
+      "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=14.14"
+      }
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/github-from-package": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+      "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/glob/node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/glob/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/globals": {
+      "version": "13.24.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+      "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "type-fest": "^0.20.2"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/globalthis": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+      "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "define-properties": "^1.2.1",
+        "gopd": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/globby": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+      "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "array-union": "^2.1.0",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.2.9",
+        "ignore": "^5.2.0",
+        "merge2": "^1.4.1",
+        "slash": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/has-property-descriptors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-define-property": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/hosted-git-info": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
+      "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/htmlparser2": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
+      "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==",
+      "dev": true,
+      "funding": [
+        "https://github.com/fb55/htmlparser2?sponsor=1",
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fb55"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "domelementtype": "^2.3.0",
+        "domhandler": "^5.0.3",
+        "domutils": "^3.2.1",
+        "entities": "^6.0.0"
+      }
+    },
+    "node_modules/htmlparser2/node_modules/entities": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+      "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/http-proxy-agent": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+      "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "BSD-3-Clause",
+      "optional": true
+    },
+    "node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+      "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/index-to-position": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz",
+      "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/ini": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+      "dev": true,
+      "license": "ISC",
+      "optional": true
+    },
+    "node_modules/is-ci": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
+      "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ci-info": "^2.0.0"
+      },
+      "bin": {
+        "is-ci": "bin.js"
+      }
+    },
+    "node_modules/is-docker": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+      "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "is-docker": "cli.js"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-inside-container": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+      "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-docker": "^3.0.0"
+      },
+      "bin": {
+        "is-inside-container": "cli.js"
+      },
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-it-type": {
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/is-it-type/-/is-it-type-5.1.3.tgz",
+      "integrity": "sha512-AX2uU0HW+TxagTgQXOJY7+2fbFHemC7YFBwN1XqD8qQMKdtfbOC8OC3fUb4s5NU59a3662Dzwto8tWDdZYRXxg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "globalthis": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-path-inside": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-wsl": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
+      "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-inside-container": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/istextorbinary": {
+      "version": "9.5.0",
+      "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz",
+      "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==",
+      "dev": true,
+      "license": "Artistic-2.0",
+      "dependencies": {
+        "binaryextensions": "^6.11.0",
+        "editions": "^6.21.0",
+        "textextensions": "^6.11.0"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "funding": {
+        "url": "https://bevry.me/fund"
+      }
+    },
+    "node_modules/jackspeak": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
+      "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+      "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/jsonc-parser": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
+      "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/jsonfile": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+      "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/jsonwebtoken": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+      "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "jws": "^3.2.2",
+        "lodash.includes": "^4.3.0",
+        "lodash.isboolean": "^3.0.3",
+        "lodash.isinteger": "^4.0.4",
+        "lodash.isnumber": "^3.0.3",
+        "lodash.isplainobject": "^4.0.6",
+        "lodash.isstring": "^4.0.1",
+        "lodash.once": "^4.0.0",
+        "ms": "^2.1.1",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": ">=12",
+        "npm": ">=6"
+      }
+    },
+    "node_modules/jwa": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
+      "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "buffer-equal-constant-time": "^1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/jws": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+      "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^1.4.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/keytar": {
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz",
+      "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "node-addon-api": "^4.3.0",
+        "prebuild-install": "^7.0.1"
+      }
+    },
+    "node_modules/keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "node_modules/leven": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
+      "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/linkify-it": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
+      "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "uc.micro": "^1.0.1"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.includes": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+      "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.isboolean": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+      "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.isinteger": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+      "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.isnumber": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+      "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.isplainobject": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+      "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.isstring": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+      "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.once": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+      "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.truncate": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+      "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/markdown-it": {
+      "version": "12.3.2",
+      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
+      "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1",
+        "entities": "~2.1.0",
+        "linkify-it": "^3.0.1",
+        "mdurl": "^1.0.1",
+        "uc.micro": "^1.0.5"
+      },
+      "bin": {
+        "markdown-it": "bin/markdown-it.js"
+      }
+    },
+    "node_modules/markdown-it/node_modules/entities": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
+      "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mdurl": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
+      "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mimic-response": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+      "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/mkdirp-classic": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+      "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/mute-stream": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+      "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/napi-build-utils": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+      "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/node-abi": {
+      "version": "3.85.0",
+      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz",
+      "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "semver": "^7.3.5"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/node-addon-api": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
+      "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/node-sarif-builder": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.3.0.tgz",
+      "integrity": "sha512-8taRy2nQs1xNs8iO2F0XbkZJEliiijpKgFVcyiwKjJ2+3X59LVI3wY84qRdJwRDpIo5GK8wvb1pxcJ+JVu3jrg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/sarif": "^2.1.7",
+        "fs-extra": "^11.1.1"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/normalize-package-data": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz",
+      "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "hosted-git-info": "^7.0.0",
+        "semver": "^7.3.5",
+        "validate-npm-package-license": "^3.0.4"
+      },
+      "engines": {
+        "node": "^16.14.0 || >=18.0.0"
+      }
+    },
+    "node_modules/normalize-package-data/node_modules/hosted-git-info": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
+      "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "lru-cache": "^10.0.1"
+      },
+      "engines": {
+        "node": "^16.14.0 || >=18.0.0"
+      }
+    },
+    "node_modules/normalize-package-data/node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/nth-check": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+      "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "boolbase": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/nth-check?sponsor=1"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/open": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
+      "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "default-browser": "^5.2.1",
+        "define-lazy-prop": "^3.0.0",
+        "is-inside-container": "^1.0.0",
+        "wsl-utils": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/optionator": {
+      "version": "0.9.4",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+      "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/ovsx": {
+      "version": "0.10.7",
+      "resolved": "https://registry.npmjs.org/ovsx/-/ovsx-0.10.7.tgz",
+      "integrity": "sha512-UjBQlB5xSDD+biAylCZ8Q/k3An9K3y9FYa+hT/HTbJkzOQP+gaNHX20CaOo4lrYT1iJXdiePH9zS2uvCXdDNDA==",
+      "dev": true,
+      "license": "EPL-2.0",
+      "dependencies": {
+        "@vscode/vsce": "^3.2.1",
+        "commander": "^6.2.1",
+        "follow-redirects": "^1.14.6",
+        "is-ci": "^2.0.0",
+        "leven": "^3.1.0",
+        "semver": "^7.6.0",
+        "tmp": "^0.2.3",
+        "yauzl-promise": "^4.0.0"
+      },
+      "bin": {
+        "ovsx": "lib/ovsx"
+      },
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/ovsx/node_modules/@vscode/vsce": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.7.0.tgz",
+      "integrity": "sha512-LY9r2T4joszRjz4d92ZPl6LTBUPS4IWH9gG/3JUv+1QyBJrveZlcVISuiaq0EOpmcgFh0GgVgKD4rD/21Tu8sA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@azure/identity": "^4.1.0",
+        "@secretlint/node": "^10.1.2",
+        "@secretlint/secretlint-formatter-sarif": "^10.1.2",
+        "@secretlint/secretlint-rule-no-dotenv": "^10.1.2",
+        "@secretlint/secretlint-rule-preset-recommend": "^10.1.2",
+        "@vscode/vsce-sign": "^2.0.0",
+        "azure-devops-node-api": "^12.5.0",
+        "chalk": "^4.1.2",
+        "cheerio": "^1.0.0-rc.9",
+        "cockatiel": "^3.1.2",
+        "commander": "^12.1.0",
+        "form-data": "^4.0.0",
+        "glob": "^11.0.0",
+        "hosted-git-info": "^4.0.2",
+        "jsonc-parser": "^3.2.0",
+        "leven": "^3.1.0",
+        "markdown-it": "^14.1.0",
+        "mime": "^1.3.4",
+        "minimatch": "^3.0.3",
+        "parse-semver": "^1.1.1",
+        "read": "^1.0.7",
+        "secretlint": "^10.1.2",
+        "semver": "^7.5.2",
+        "tmp": "^0.2.3",
+        "typed-rest-client": "^1.8.4",
+        "url-join": "^4.0.1",
+        "xml2js": "^0.5.0",
+        "yauzl": "^2.3.1",
+        "yazl": "^2.2.2"
+      },
+      "bin": {
+        "vsce": "vsce"
+      },
+      "engines": {
+        "node": ">= 20"
+      },
+      "optionalDependencies": {
+        "keytar": "^7.7.0"
+      }
+    },
+    "node_modules/ovsx/node_modules/@vscode/vsce/node_modules/commander": {
+      "version": "12.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+      "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/ovsx/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/ovsx/node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/ovsx/node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/ovsx/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/ovsx/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/ovsx/node_modules/glob": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
+      "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "foreground-child": "^3.3.1",
+        "jackspeak": "^4.1.1",
+        "minimatch": "^10.1.1",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^2.0.0"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/ovsx/node_modules/glob/node_modules/minimatch": {
+      "version": "10.1.1",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
+      "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/brace-expansion": "^5.0.0"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/ovsx/node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ovsx/node_modules/linkify-it": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+      "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "uc.micro": "^2.0.0"
+      }
+    },
+    "node_modules/ovsx/node_modules/markdown-it": {
+      "version": "14.1.0",
+      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
+      "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1",
+        "entities": "^4.4.0",
+        "linkify-it": "^5.0.0",
+        "mdurl": "^2.0.0",
+        "punycode.js": "^2.3.1",
+        "uc.micro": "^2.1.0"
+      },
+      "bin": {
+        "markdown-it": "bin/markdown-it.mjs"
+      }
+    },
+    "node_modules/ovsx/node_modules/mdurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+      "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/ovsx/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/ovsx/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ovsx/node_modules/uc.micro": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+      "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-map": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
+      "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0"
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/parse-json": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
+      "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.26.2",
+        "index-to-position": "^1.1.0",
+        "type-fest": "^4.39.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parse-json/node_modules/type-fest": {
+      "version": "4.41.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+      "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parse-semver": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz",
+      "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "semver": "^5.1.0"
+      }
+    },
+    "node_modules/parse-semver/node_modules/semver": {
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver"
+      }
+    },
+    "node_modules/parse5": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+      "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "entities": "^6.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
+      }
+    },
+    "node_modules/parse5-htmlparser2-tree-adapter": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
+      "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "domhandler": "^5.0.3",
+        "parse5": "^7.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
+      }
+    },
+    "node_modules/parse5-parser-stream": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
+      "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "parse5": "^7.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
+      }
+    },
+    "node_modules/parse5/node_modules/entities": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+      "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-scurry": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
+      "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^11.0.0",
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-scurry/node_modules/lru-cache": {
+      "version": "11.2.2",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
+      "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/pend": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+      "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pluralize": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
+      "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/prebuild-install": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+      "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "detect-libc": "^2.0.0",
+        "expand-template": "^2.0.3",
+        "github-from-package": "0.0.0",
+        "minimist": "^1.2.3",
+        "mkdirp-classic": "^0.5.3",
+        "napi-build-utils": "^2.0.0",
+        "node-abi": "^3.3.0",
+        "pump": "^3.0.0",
+        "rc": "^1.2.7",
+        "simple-get": "^4.0.0",
+        "tar-fs": "^2.0.0",
+        "tunnel-agent": "^0.6.0"
+      },
+      "bin": {
+        "prebuild-install": "bin.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/pump": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+      "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/punycode.js": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+      "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.14.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+      "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/rc": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+      "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+      "dev": true,
+      "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+      "optional": true,
+      "dependencies": {
+        "deep-extend": "^0.6.0",
+        "ini": "~1.3.0",
+        "minimist": "^1.2.0",
+        "strip-json-comments": "~2.0.1"
+      },
+      "bin": {
+        "rc": "cli.js"
+      }
+    },
+    "node_modules/rc-config-loader": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.3.tgz",
+      "integrity": "sha512-kD7FqML7l800i6pS6pvLyIE2ncbk9Du8Q0gp/4hMPhJU6ZxApkoLcGD8ZeqgiAlfwZ6BlETq6qqe+12DUL207w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.4",
+        "js-yaml": "^4.1.0",
+        "json5": "^2.2.2",
+        "require-from-string": "^2.0.2"
+      }
+    },
+    "node_modules/rc/node_modules/strip-json-comments": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+      "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/read": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",
+      "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "mute-stream": "~0.0.4"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/read-pkg": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz",
+      "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/normalize-package-data": "^2.4.3",
+        "normalize-package-data": "^6.0.0",
+        "parse-json": "^8.0.0",
+        "type-fest": "^4.6.0",
+        "unicorn-magic": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/read-pkg/node_modules/type-fest": {
+      "version": "4.41.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+      "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "deprecated": "Rimraf versions prior to v4 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/run-applescript": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
+      "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/sax": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
+      "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
+      "dev": true,
+      "license": "BlueOak-1.0.0"
+    },
+    "node_modules/secretlint": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz",
+      "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@secretlint/config-creator": "^10.2.2",
+        "@secretlint/formatter": "^10.2.2",
+        "@secretlint/node": "^10.2.2",
+        "@secretlint/profiler": "^10.2.2",
+        "debug": "^4.4.1",
+        "globby": "^14.1.0",
+        "read-pkg": "^9.0.1"
+      },
+      "bin": {
+        "secretlint": "bin/secretlint.js"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/secretlint/node_modules/globby": {
+      "version": "14.1.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz",
+      "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sindresorhus/merge-streams": "^2.1.0",
+        "fast-glob": "^3.3.3",
+        "ignore": "^7.0.3",
+        "path-type": "^6.0.0",
+        "slash": "^5.1.0",
+        "unicorn-magic": "^0.3.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/secretlint/node_modules/ignore": {
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+      "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/secretlint/node_modules/path-type": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz",
+      "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/secretlint/node_modules/slash": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
+      "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/secretlint/node_modules/unicorn-magic": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+      "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.7.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+      "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/simple-concat": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+      "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/simple-get": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+      "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "decompress-response": "^6.0.0",
+        "once": "^1.3.1",
+        "simple-concat": "^1.0.0"
+      }
+    },
+    "node_modules/simple-invariant": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/simple-invariant/-/simple-invariant-2.0.1.tgz",
+      "integrity": "sha512-1sbhsxqI+I2tqlmjbz99GXNmZtr6tKIyEgGGnJw/MKGblalqk/XoOYYFJlBzTKZCxx8kLaD3FD5s9BEEjx5Pyg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/slice-ansi": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+      "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "astral-regex": "^2.0.0",
+        "is-fullwidth-code-point": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+      }
+    },
+    "node_modules/slice-ansi/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/slice-ansi/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/slice-ansi/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/spdx-correct": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+      "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/spdx-exceptions": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+      "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+      "dev": true,
+      "license": "CC-BY-3.0"
+    },
+    "node_modules/spdx-expression-parse": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+      "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/spdx-license-ids": {
+      "version": "3.0.22",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz",
+      "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==",
+      "dev": true,
+      "license": "CC0-1.0"
+    },
+    "node_modules/sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/structured-source": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz",
+      "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "boundary": "^2.0.0"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/supports-hyperlinks": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz",
+      "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0",
+        "supports-color": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1"
+      }
+    },
+    "node_modules/supports-hyperlinks/node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supports-hyperlinks/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/table": {
+      "version": "6.9.0",
+      "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz",
+      "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "ajv": "^8.0.1",
+        "lodash.truncate": "^4.4.2",
+        "slice-ansi": "^4.0.0",
+        "string-width": "^4.2.3",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/table/node_modules/ajv": {
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+      "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "fast-uri": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/table/node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tar-fs": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+      "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "chownr": "^1.1.1",
+        "mkdirp-classic": "^0.5.2",
+        "pump": "^3.0.0",
+        "tar-stream": "^2.1.4"
+      }
+    },
+    "node_modules/tar-stream": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+      "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "bl": "^4.0.3",
+        "end-of-stream": "^1.4.1",
+        "fs-constants": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.1.1"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/terminal-link": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz",
+      "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-escapes": "^7.0.0",
+        "supports-hyperlinks": "^3.2.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/textextensions": {
+      "version": "6.11.0",
+      "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz",
+      "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==",
+      "dev": true,
+      "license": "Artistic-2.0",
+      "dependencies": {
+        "editions": "^6.21.0"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "funding": {
+        "url": "https://bevry.me/fund"
+      }
+    },
+    "node_modules/tmp": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+      "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.14"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/ts-api-utils": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
+      "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.2.0"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "dev": true,
+      "license": "0BSD"
+    },
+    "node_modules/tunnel": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+      "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
+      }
+    },
+    "node_modules/tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/type-fest": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/typed-rest-client": {
+      "version": "1.8.11",
+      "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz",
+      "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "qs": "^6.9.1",
+        "tunnel": "0.0.6",
+        "underscore": "^1.12.1"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "peer": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/uc.micro": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
+      "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/underscore": {
+      "version": "1.13.7",
+      "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
+      "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/undici": {
+      "version": "7.16.0",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
+      "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.18.1"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/unicorn-magic": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
+      "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/universalify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/url-join": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+      "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/uuid": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
+    "node_modules/version-range": {
+      "version": "4.15.0",
+      "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz",
+      "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==",
+      "dev": true,
+      "license": "Artistic-2.0",
+      "engines": {
+        "node": ">=4"
+      },
+      "funding": {
+        "url": "https://bevry.me/fund"
+      }
+    },
+    "node_modules/vscode-jsonrpc": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz",
+      "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/vscode-languageclient": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz",
+      "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==",
+      "license": "MIT",
+      "dependencies": {
+        "minimatch": "^5.1.0",
+        "semver": "^7.3.7",
+        "vscode-languageserver-protocol": "3.17.3"
+      },
+      "engines": {
+        "vscode": "^1.67.0"
+      }
+    },
+    "node_modules/vscode-languageclient/node_modules/minimatch": {
+      "version": "5.1.6",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+      "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/vscode-languageserver-protocol": {
+      "version": "3.17.3",
+      "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz",
+      "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==",
+      "license": "MIT",
+      "dependencies": {
+        "vscode-jsonrpc": "8.1.0",
+        "vscode-languageserver-types": "3.17.3"
+      }
+    },
+    "node_modules/vscode-languageserver-types": {
+      "version": "3.17.3",
+      "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz",
+      "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==",
+      "license": "MIT"
+    },
+    "node_modules/whatwg-encoding": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+      "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "iconv-lite": "0.6.3"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/whatwg-mimetype": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+      "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/word-wrap": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-regex": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+      "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-styles": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+      "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi/node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/strip-ansi": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+      "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/wsl-utils": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
+      "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-wsl": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/xml2js": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
+      "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "sax": ">=0.6.0",
+        "xmlbuilder": "~11.0.0"
+      },
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/xmlbuilder": {
+      "version": "11.0.1",
+      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+      "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/yauzl": {
+      "version": "2.10.0",
+      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+      "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "buffer-crc32": "~0.2.3",
+        "fd-slicer": "~1.1.0"
+      }
+    },
+    "node_modules/yauzl-promise": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yauzl-promise/-/yauzl-promise-4.0.0.tgz",
+      "integrity": "sha512-/HCXpyHXJQQHvFq9noqrjfa/WpQC2XYs3vI7tBiAi4QiIU1knvYhZGaO1QPjwIVMdqflxbmwgMXtYeaRiAE0CA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@node-rs/crc32": "^1.7.0",
+        "is-it-type": "^5.1.2",
+        "simple-invariant": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/yazl": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz",
+      "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "buffer-crc32": "~0.2.3"
+      }
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    }
+  }
+}
diff --git a/plugin/package.json b/plugin/package.json
new file mode 100644
index 0000000..550c941
--- /dev/null
+++ b/plugin/package.json
@@ -0,0 +1,287 @@
+{
+  "name": "isl-language-support",
+  "displayName": "ISL - Intuitive Scripting Language",
+  "description": "Comprehensive language support for ISL (Intuitive Scripting Language) - JSON transformation language",
+  "version": "1.2.0",
+  "publisher": "isl-team",
+  "icon": "images/icon.png",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/intuit/isl"
+  },
+  "engines": {
+    "vscode": "^1.75.0"
+  },
+  "categories": [
+    "Programming Languages",
+    "Formatters",
+    "Linters"
+  ],
+  "keywords": [
+    "isl",
+    "json",
+    "transformation",
+    "scripting",
+    "jolt"
+  ],
+  "activationEvents": [
+    "workspaceContains:**/*.isl",
+    "workspaceContains:**/*.tests.yaml",
+    "onTerminalProfile:isl.terminal-profile"
+  ],
+  "main": "./out/extension.js",
+  "contributes": {
+    "configurationDefaults": {
+      "[isl]": {
+        "editor.formatOnPaste": false
+      }
+    },
+    "languages": [
+      {
+        "id": "isl",
+        "aliases": [
+          "ISL",
+          "isl"
+        ],
+        "extensions": [
+          ".isl"
+        ],
+        "configuration": "./language-configuration.json",
+        "icon": {
+          "light": "./images/file-icon.svg",
+          "dark": "./images/file-icon.svg"
+        }
+      }
+    ],
+    "grammars": [
+      {
+        "language": "isl",
+        "scopeName": "source.isl",
+        "path": "./syntaxes/isl.tmLanguage.json"
+      },
+      {
+        "scopeName": "markdown.isl.codeblock",
+        "path": "./syntaxes/isl-markdown-injection.tmLanguage.json",
+        "injectTo": [
+          "text.html.markdown"
+        ],
+        "embeddedLanguages": {
+          "meta.embedded.block.isl": "isl"
+        }
+      }
+    ],
+    "configuration": {
+      "title": "ISL",
+      "properties": {
+        "isl.validation.enabled": {
+          "type": "boolean",
+          "default": true,
+          "description": "Enable/disable ISL validation"
+        },
+        "isl.naming.convention": {
+          "type": "string",
+          "enum": [
+            "PascalCase",
+            "camelCase",
+            "snake_case"
+          ],
+          "default": "PascalCase",
+          "description": "Naming convention for functions and modifiers (PascalCase, camelCase, or snake_case)"
+        },
+        "isl.formatting.enabled": {
+          "type": "boolean",
+          "default": true,
+          "description": "Enable/disable ISL formatting"
+        },
+        "isl.formatting.indentSize": {
+          "type": "number",
+          "default": 4,
+          "description": "Number of spaces for indentation"
+        },
+        "isl.formatting.useTabs": {
+          "type": "boolean",
+          "default": false,
+          "description": "Use tabs instead of spaces"
+        },
+        "isl.formatting.alignProperties": {
+          "type": "boolean",
+          "default": false,
+          "description": "Align object property colons"
+        },
+        "isl.linting.enabled": {
+          "type": "boolean",
+          "default": true,
+          "description": "Enable/disable ISL linting"
+        },
+        "isl.execution.islCommand": {
+          "type": "string",
+          "default": "",
+          "description": "Optional. External ISL executable (isl.bat or isl.sh). Only used when the extension's embedded CLI (lib/isl-cmd-all.jar) is not present. Most users leave this empty."
+        },
+        "isl.execution.javaHome": {
+          "type": "string",
+          "default": "",
+          "description": "Path to Java home directory (if not in PATH)"
+        },
+        "isl.extensions.source": {
+          "type": "string",
+          "default": "",
+          "description": "Global source for .islextensions file. Can be a URL (e.g., https://example.com/extensions.json) or absolute file path. Workspace-local .islextensions files take precedence. Leave empty to use only workspace-local files."
+        },
+        "isl.extensions.cacheTTL": {
+          "type": "number",
+          "default": 3600,
+          "description": "Cache TTL in seconds for extensions loaded from URL (default: 3600 = 1 hour)"
+        },
+        "isl.trace.server": {
+          "type": "string",
+          "enum": [
+            "off",
+            "messages",
+            "verbose"
+          ],
+          "default": "off",
+          "description": "Traces the communication between VS Code and the language server"
+        }
+      }
+    },
+    "commands": [
+      {
+        "command": "isl.validate",
+        "title": "ISL: Validate Current File"
+      },
+      {
+        "command": "isl.run",
+        "title": "ISL: Run Transformation"
+      },
+      {
+        "command": "isl.runWithInput",
+        "title": "ISL: Run Transformation with Input File"
+      },
+      {
+        "command": "isl.format",
+        "title": "ISL: Format Document"
+      },
+      {
+        "command": "isl.showDocumentation",
+        "title": "ISL: Open Documentation"
+      },
+      {
+        "command": "isl.compile",
+        "title": "ISL: Compile (Validate) File",
+        "icon": "$(run-all)"
+      },
+      {
+        "command": "isl.runAllTestsInFile",
+        "title": "Run All Tests in File",
+        "icon": "$(beaker)"
+      },
+      {
+        "command": "isl.addMockFromTestError",
+        "title": "ISL: Add Mock from Test Error"
+      }
+    ],
+    "menus": {
+      "editor/title": [
+        {
+          "when": "resourceLangId == isl",
+          "command": "isl.compile",
+          "group": "navigation"
+        },
+        {
+          "when": "isl.isTestFile",
+          "command": "isl.runAllTestsInFile",
+          "group": "navigation"
+        }
+      ],
+      "editor/context": [
+        {
+          "when": "resourceLangId == isl",
+          "command": "isl.compile",
+          "group": "isl@1"
+        },
+        {
+          "when": "resourceLangId == isl",
+          "command": "isl.run",
+          "group": "isl@2"
+        },
+        {
+          "when": "resourceLangId == isl",
+          "command": "isl.runWithInput",
+          "group": "isl@3"
+        }
+      ],
+      "commandPalette": [
+        {
+          "command": "isl.validate",
+          "when": "resourceLangId == isl"
+        },
+        {
+          "command": "isl.run",
+          "when": "resourceLangId == isl"
+        },
+        {
+          "command": "isl.runWithInput",
+          "when": "resourceLangId == isl"
+        },
+        {
+          "command": "isl.format",
+          "when": "resourceLangId == isl"
+        },
+        {
+          "command": "isl.compile",
+          "when": "resourceLangId == isl"
+        },
+        {
+          "command": "isl.runAllTestsInFile",
+          "when": "isl.isTestFile"
+        }
+      ]
+    },
+    "snippets": [
+      {
+        "language": "isl",
+        "path": "./snippets/isl.json"
+      }
+    ],
+    "terminal": {
+      "profiles": [
+        {
+          "id": "isl.terminal-profile",
+          "title": "ISL",
+          "icon": "terminal"
+        }
+      ]
+    }
+  },
+  "scripts": {
+    "vscode:prepublish": "npm run compile:production",
+    "compile": "tsc --noEmit && node esbuild.js",
+    "compile:production": "tsc --noEmit && node esbuild.js --production",
+    "watch": "node esbuild.js --watch",
+    "pretest": "npm run compile",
+    "lint": "eslint src --ext ts",
+    "package": "vsce package",
+    "publish": "vsce publish",
+    "publish:ovsx": "ovsx publish"
+  },
+  "devDependencies": {
+    "@types/js-yaml": "^4.0.0",
+    "@types/node": "^18.0.0",
+    "@types/vscode": "^1.75.0",
+    "@typescript-eslint/eslint-plugin": "^6.0.0",
+    "@typescript-eslint/parser": "^6.0.0",
+    "@vscode/vsce": "^2.32.0",
+    "esbuild": "^0.24.0",
+    "eslint": "^8.40.0",
+    "ovsx": "^0.10.7",
+    "typescript": "^5.0.0"
+  },
+  "dependencies": {
+    "js-yaml": "^4.1.0",
+    "vscode-languageclient": "^8.1.0"
+  },
+  "extensionRecommendations": [
+    "streetsidesoftware.code-spell-checker"
+  ]
+}
diff --git a/plugin/snippets/isl.json b/plugin/snippets/isl.json
new file mode 100644
index 0000000..cdfa6dc
--- /dev/null
+++ b/plugin/snippets/isl.json
@@ -0,0 +1,347 @@
+{
+  "ISL Function": {
+    "prefix": "fun",
+    "body": [
+      "fun ${1:functionName}(${2:\\$input}) {",
+      "\t${3:// function body}",
+      "\treturn {",
+      "\t\t${4:result}: ${5:value}",
+      "\t}",
+      "}"
+    ],
+    "description": "Create an ISL function"
+  },
+  "ISL Modifier Function": {
+    "prefix": "modifier",
+    "body": [
+      "modifier ${1:modifierName}(${2:\\$value}) {",
+      "\treturn ${3:\\$value}",
+      "}"
+    ],
+    "description": "Create an ISL modifier function"
+  },
+  "ISL Run Function": {
+    "prefix": "run",
+    "body": [
+      "fun run(${1:\\$input}) {",
+      "\t${2:// Transform input}",
+      "\t$0",
+      "}"
+    ],
+    "description": "Create an ISL run function (entry point)"
+  },
+  "ISL If Statement": {
+    "prefix": "if",
+    "body": [
+      "if (${1:condition})",
+      "\t${2:// true block}",
+      "else",
+      "\t${3:// false block}",
+      "endif"
+    ],
+    "description": "If-else statement"
+  },
+  "ISL If Expression": {
+    "prefix": "ife",
+    "body": [
+      "if (${1:condition}) ${2:trueValue} else ${3:falseValue} endif"
+    ],
+    "description": "Inline if expression"
+  },
+  "ISL Switch": {
+    "prefix": "switch",
+    "body": [
+      "switch (${1:\\$variable})",
+      "\t${2:value} -> ${3:result};",
+      "\telse -> ${4:default};",
+      "endswitch"
+    ],
+    "description": "Switch-case statement"
+  },
+  "ISL ForEach": {
+    "prefix": "foreach",
+    "body": [
+      "foreach ${1:\\$item} in ${2:\\$array}",
+      "\t{",
+      "\t\t${3:property}: ${4:\\$item.value}",
+      "\t}",
+      "endfor"
+    ],
+    "description": "ForEach loop"
+  },
+  "ISL Parallel ForEach": {
+    "prefix": "parallel",
+    "body": [
+      "parallel foreach ${1:\\$item} in ${2:\\$array}",
+      "\t{",
+      "\t\t${3:property}: ${4:\\$item.value}",
+      "\t}",
+      "endfor"
+    ],
+    "description": "Parallel foreach loop"
+  },
+  "ISL While Loop": {
+    "prefix": "while",
+    "body": [
+      "while (${1:condition})",
+      "\t${2:// loop body}",
+      "endwhile"
+    ],
+    "description": "While loop"
+  },
+  "ISL Import": {
+    "prefix": "import",
+    "body": [
+      "import ${1:ModuleName} from '${2:module.isl}';"
+    ],
+    "description": "Import statement"
+  },
+  "ISL Type Declaration": {
+    "prefix": "type",
+    "body": [
+      "type ${1:TypeName} as {",
+      "\t${2:property}: ${3:String}",
+      "};"
+    ],
+    "description": "Type declaration"
+  },
+  "ISL Variable": {
+    "prefix": "var",
+    "body": [
+      "\\$${1:name} = ${2:value};"
+    ],
+    "description": "Variable declaration"
+  },
+  "ISL Object": {
+    "prefix": "obj",
+    "body": [
+      "{",
+      "\t${1:property}: ${2:value}",
+      "}"
+    ],
+    "description": "Object literal"
+  },
+  "ISL String Interpolation": {
+    "prefix": "interp",
+    "body": [
+      "`${${1:\\$variable}}`"
+    ],
+    "description": "String interpolation"
+  },
+  "ISL Math Expression": {
+    "prefix": "math",
+    "body": [
+      "{{ ${1:expression} }}"
+    ],
+    "description": "Math expression"
+  },
+  "ISL Function Call": {
+    "prefix": "call",
+    "body": [
+      "@.${1:Service}.${2:Method}(${3:args})"
+    ],
+    "description": "Function call"
+  },
+  "ISL Filter": {
+    "prefix": "filter",
+    "body": [
+      "${1:\\$array} | filter(${2:condition})"
+    ],
+    "description": "Filter modifier"
+  },
+  "ISL Map": {
+    "prefix": "map",
+    "body": [
+      "${1:\\$array} | map(${2:expression})"
+    ],
+    "description": "Map modifier"
+  },
+  "ISL Date Now": {
+    "prefix": "now",
+    "body": [
+      "@.Date.Now()"
+    ],
+    "description": "Get current date/time"
+  },
+  "ISL Date Parse": {
+    "prefix": "dateparse",
+    "body": [
+      "${1:\\$dateString} | date.parse(\"${2:yyyy-MM-dd}\")"
+    ],
+    "description": "Parse date string"
+  },
+  "ISL Date Format": {
+    "prefix": "dateformat",
+    "body": [
+      "${1:\\$date} | to.string(\"${2:yyyy-MM-dd HH:mm:ss}\")"
+    ],
+    "description": "Format date"
+  },
+  "ISL Transform Object Keys": {
+    "prefix": "transformkeys",
+    "body": [
+      "foreach \\$key in ${1:\\$object} | keys",
+      "\t`\\${\\$key}`: ${1:\\$object}[\\$key]$0",
+      "endfor"
+    ],
+    "description": "Transform all keys of an object"
+  },
+  "ISL Safe Navigation": {
+    "prefix": "safe",
+    "body": [
+      "${1:\\$value} ?? ${2:defaultValue}"
+    ],
+    "description": "Null coalescing operator"
+  },
+  "ISL Array to Object": {
+    "prefix": "arrtoobj",
+    "body": [
+      "${1:\\$array} | map({",
+      "\t`\\${${2:\\$item.key}}`: ${3:\\$item.value}",
+      "})"
+    ],
+    "description": "Convert array to object"
+  },
+  "ISL Conditional Field": {
+    "prefix": "condfield",
+    "body": [
+      "if (${1:condition})",
+      "\t${2:fieldName}: ${3:value}",
+      "endif"
+    ],
+    "description": "Conditional object field"
+  },
+  "ISL Error Handling": {
+    "prefix": "errorhandle",
+    "body": [
+      "${1:\\$value} | default(${2:fallbackValue})"
+    ],
+    "description": "Provide default value on error"
+  },
+  "ISL Spread Object": {
+    "prefix": "spread",
+    "body": [
+      "{",
+      "\t...${1:\\$object},",
+      "\t${2:newField}: ${3:value}",
+      "}"
+    ],
+    "description": "Spread operator to merge objects"
+  },
+  "ISL Filter and Map": {
+    "prefix": "filtermap",
+    "body": [
+      "${1:\\$array}",
+      "\t| filter(${2:condition})",
+      "\t| map(${3:expression})"
+    ],
+    "description": "Filter then map array"
+  },
+  "ISL Reduce Sum": {
+    "prefix": "reducesum",
+    "body": [
+      "${1:\\$array} | reduce({{ \\$acc + \\$it }}, 0)"
+    ],
+    "description": "Reduce array to sum"
+  },
+  "ISL Template String": {
+    "prefix": "template",
+    "body": [
+      "`${${1:expression}}`"
+    ],
+    "description": "Template string with interpolation"
+  },
+  "ISL Multi-line String": {
+    "prefix": "multiline",
+    "body": [
+      "`${1:line1}",
+      "${2:line2}",
+      "${3:line3}`"
+    ],
+    "description": "Multi-line template string"
+  },
+  "ISL Nested Object": {
+    "prefix": "nested",
+    "body": [
+      "{",
+      "\t${1:field1}: {",
+      "\t\t${2:nested}: ${3:value}",
+      "\t}",
+      "}"
+    ],
+    "description": "Nested object structure"
+  },
+  "ISL Array Destructure": {
+    "prefix": "destruct",
+    "body": [
+      "\\$${1:first} = ${2:\\$array} | first;",
+      "\\$${3:last} = ${2:\\$array} | last;"
+    ],
+    "description": "Destructure array"
+  },
+  "ISL Chain Modifiers": {
+    "prefix": "chain",
+    "body": [
+      "${1:\\$value}",
+      "\t| ${2:modifier1}",
+      "\t| ${3:modifier2}",
+      "\t| ${4:modifier3}"
+    ],
+    "description": "Chain multiple modifiers"
+  },
+  "ISL Fallback Chain": {
+    "prefix": "fallback",
+    "body": [
+      "${1:\\$primary} ?? ${2:\\$secondary} ?? ${3:default}"
+    ],
+    "description": "Multiple fallback values"
+  },
+  "ISL Custom Object Key": {
+    "prefix": "dynamickey",
+    "body": [
+      "{",
+      "\t`\\${${1:expression}}`: ${2:value}",
+      "}"
+    ],
+    "description": "Object with dynamic key"
+  },
+  "ISL Validate and Transform": {
+    "prefix": "validate",
+    "body": [
+      "if (${1:\\$input.field} | isNotEmpty)",
+      "\t${2:result}: ${1:\\$input.field} | ${3:transform}",
+      "else",
+      "\t${2:result}: ${4:defaultValue}",
+      "endif"
+    ],
+    "description": "Validate before transforming"
+  },
+  "ISL Date Range": {
+    "prefix": "daterange",
+    "body": [
+      "\\$start = ${1:\\$date};",
+      "\\$end = ${1:\\$date} | date.add(${2:7}, \"${3:DAYS}\");"
+    ],
+    "description": "Create date range"
+  },
+  "ISL Group By": {
+    "prefix": "groupby",
+    "body": [
+      "foreach \\$item in ${1:\\$array}",
+      "\t\\$key = \\$item.${2:groupField};",
+      "\t\\$grouped[\\$key] = (\\$grouped[\\$key] ?? []) | push(\\$item);",
+      "endfor"
+    ],
+    "description": "Group array by field"
+  },
+  "ISL Batch Process": {
+    "prefix": "batch",
+    "body": [
+      "parallel foreach \\$item in ${1:\\$array}",
+      "\t@.This.${2:processFunction}(\\$item)",
+      "endfor"
+    ],
+    "description": "Parallel batch processing"
+  }
+}
+
diff --git a/plugin/src/codeactions.ts b/plugin/src/codeactions.ts
new file mode 100644
index 0000000..b5901c3
--- /dev/null
+++ b/plugin/src/codeactions.ts
@@ -0,0 +1,1342 @@
+import * as vscode from 'vscode';
+
+export async function renameDuplicateFunction(uri: vscode.Uri, lineNumber: number, functionName: string, kind: 'fun' | 'modifier') {
+    const document = await vscode.workspace.openTextDocument(uri);
+    const newName = await vscode.window.showInputBox({
+        prompt: `Enter a unique name for the duplicate ${kind} (ISL is case-insensitive, so changing casing won't fix the duplicate)`,
+        value: `${functionName}2`,
+        validateInput: (text) => {
+            if (!text.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
+                return 'Invalid name - use letters, numbers, and underscores';
+            }
+            return null;
+        }
+    });
+    if (!newName) return;
+
+    const line = document.lineAt(lineNumber);
+    const funPattern = new RegExp(`^(\\s*${kind}\\s+)(${escapeRegex(functionName)})(\\s*\\()`);
+    const match = line.text.match(funPattern);
+    if (!match) return;
+
+    const nameStartCol = match.index! + match[1].length;
+    const range = new vscode.Range(lineNumber, nameStartCol, lineNumber, nameStartCol + functionName.length);
+
+    const edit = new vscode.WorkspaceEdit();
+    edit.replace(uri, range, newName);
+    await vscode.workspace.applyEdit(edit);
+}
+
+function escapeRegex(s: string): string {
+    return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+export class IslCodeActionProvider implements vscode.CodeActionProvider {
+    
+    public static readonly providedCodeActionKinds = [
+        vscode.CodeActionKind.QuickFix,
+        vscode.CodeActionKind.Refactor,
+        vscode.CodeActionKind.RefactorExtract,
+        vscode.CodeActionKind.RefactorRewrite
+    ];
+    
+    provideCodeActions(
+        document: vscode.TextDocument,
+        range: vscode.Range | vscode.Selection,
+        context: vscode.CodeActionContext,
+        token: vscode.CancellationToken
+    ): vscode.CodeAction[] {
+        const actions: vscode.CodeAction[] = [];
+        
+        // Add quick fixes for diagnostics
+        for (const diagnostic of context.diagnostics) {
+            actions.push(...this.createQuickFixes(document, diagnostic, context.diagnostics));
+        }
+        
+        // Add refactoring actions when text is selected
+        if (!range.isEmpty) {
+            actions.push(...this.createRefactoringActions(document, range));
+        }
+        
+        // Add code improvements for current line
+        const line = document.lineAt(range.start.line);
+        actions.push(...this.createImprovementActions(document, line, range));
+        
+        return actions;
+    }
+    
+    private createQuickFixes(document: vscode.TextDocument, diagnostic: vscode.Diagnostic, allDiagnostics: readonly vscode.Diagnostic[]): vscode.CodeAction[] {
+        const actions: vscode.CodeAction[] = [];
+        const line = document.lineAt(diagnostic.range.start.line).text;
+        
+        // Fix typos in modifier names
+        if (diagnostic.message.includes('uppercase')) {
+            actions.push(this.createFix(
+                'Change to upperCase',
+                document,
+                diagnostic.range,
+                'upperCase',
+                diagnostic
+            ));
+        }
+        
+        if (diagnostic.message.includes('lowercase')) {
+            actions.push(this.createFix(
+                'Change to lowerCase',
+                document,
+                diagnostic.range,
+                'lowerCase',
+                diagnostic
+            ));
+        }
+        
+        // Suggest common typos
+        const typoSuggestions: { [key: string]: string } = {
+            'titlecase': 'titleCase',
+            'capitalize': 'capitalize',
+            'tostring': 'to.string',
+            'tonumber': 'to.number',
+            'todecimal': 'to.decimal',
+            'toboolean': 'to.boolean',
+            'dateparse': 'date.parse',
+            'dateadd': 'date.add',
+            'mathsum': 'Math.sum',
+            'mathavg': 'Math.average',
+            'mathmax': 'Math.max',
+            'mathmin': 'Math.min',
+        };
+        
+        const word = document.getText(diagnostic.range).toLowerCase();
+        if (typoSuggestions[word]) {
+            actions.push(this.createFix(
+                `Did you mean '${typoSuggestions[word]}'?`,
+                document,
+                diagnostic.range,
+                typoSuggestions[word],
+                diagnostic
+            ));
+        }
+        
+        // Fix missing return statement
+        if (diagnostic.message.includes('missing return')) {
+            const action = new vscode.CodeAction(
+                'Add return statement',
+                vscode.CodeActionKind.QuickFix
+            );
+            action.edit = new vscode.WorkspaceEdit();
+            const insertPosition = diagnostic.range.end;
+            action.edit.insert(document.uri, insertPosition, '\n    return {};\n');
+            action.diagnostics = [diagnostic];
+            action.isPreferred = true;
+            actions.push(action);
+        }
+        
+        // Fix unclosed braces
+        if (diagnostic.message.includes('unclosed') || diagnostic.message.includes('unbalanced')) {
+            const closingChar = diagnostic.message.includes('{') ? '}' : 
+                               diagnostic.message.includes('[') ? ']' : ')';
+            const action = new vscode.CodeAction(
+                `Add closing '${closingChar}'`,
+                vscode.CodeActionKind.QuickFix
+            );
+            action.edit = new vscode.WorkspaceEdit();
+            action.edit.insert(document.uri, diagnostic.range.end, closingChar);
+            action.diagnostics = [diagnostic];
+            action.isPreferred = true;
+            actions.push(action);
+        }
+
+        // Format long object declaration
+        if (diagnostic.code === 'format-object') {
+            const line = document.lineAt(diagnostic.range.start.line);
+            const action = new vscode.CodeAction(
+                'Format object on multiple lines',
+                vscode.CodeActionKind.QuickFix
+            );
+            action.command = {
+                command: 'isl.improvement.formatObject',
+                title: 'Format object on multiple lines',
+                arguments: [document, line.range]
+            };
+            action.diagnostics = [diagnostic];
+            action.isPreferred = true;
+            actions.push(action);
+        }
+
+        // Simplify string interpolation
+        if (diagnostic.code === 'simplify-interpolation') {
+            const text = document.getText(diagnostic.range);
+            // Extract the variable from ${$variable}
+            const match = text.match(/\$\{(\$[a-zA-Z_][a-zA-Z0-9_]*)\}/);
+            if (match) {
+                const variable = match[1];
+                const action = new vscode.CodeAction(
+                    `Simplify to ${variable}`,
+                    vscode.CodeActionKind.QuickFix
+                );
+                action.edit = new vscode.WorkspaceEdit();
+                action.edit.replace(document.uri, diagnostic.range, variable);
+                action.diagnostics = [diagnostic];
+                action.isPreferred = true;
+                actions.push(action);
+            }
+        }
+
+        // Unnecessary template literal (whole string is only one ${ ... }) -> replace with inner expression
+        if (diagnostic.code === 'unnecessary-template-literal') {
+            const innerExpression = (diagnostic as vscode.Diagnostic & { innerExpression?: string }).innerExpression;
+            if (innerExpression !== undefined) {
+                const action = new vscode.CodeAction(
+                    'Use expression directly (remove unnecessary interpolation)',
+                    vscode.CodeActionKind.QuickFix
+                );
+                action.edit = new vscode.WorkspaceEdit();
+                action.edit.replace(document.uri, diagnostic.range, innerExpression);
+                action.diagnostics = [diagnostic];
+                action.isPreferred = true;
+                actions.push(action);
+            }
+        }
+
+        // Convert + string concatenation to template literal (ISL string interpolation)
+        if (diagnostic.code === 'no-plus-concatenation') {
+            const rhs = document.getText(diagnostic.range).trim();
+            const converted = this.convertPlusConcatenationToInterpolation(rhs);
+            if (converted !== null) {
+                const action = new vscode.CodeAction(
+                    'Convert to string interpolation (template literal)',
+                    vscode.CodeActionKind.QuickFix
+                );
+                action.edit = new vscode.WorkspaceEdit();
+                action.edit.replace(document.uri, diagnostic.range, converted);
+                action.diagnostics = [diagnostic];
+                action.isPreferred = true;
+                actions.push(action);
+            }
+        }
+
+        // Use coalesce operator
+        if (diagnostic.code === 'use-coalesce-operator') {
+            const line = document.lineAt(diagnostic.range.start.line);
+            const action = new vscode.CodeAction(
+                'Use ?? operator instead',
+                vscode.CodeActionKind.QuickFix
+            );
+            action.command = {
+                command: 'isl.improvement.useCoalesceOperator',
+                title: 'Use ?? operator',
+                arguments: [document, line.range]
+            };
+            action.diagnostics = [diagnostic];
+            action.isPreferred = true;
+            actions.push(action);
+        }
+
+        // Use = instead of : for variable assignment
+        if (diagnostic.code === 'use-equals-assignment') {
+            // Single fix
+            const action = new vscode.CodeAction(
+                'Change : to =',
+                vscode.CodeActionKind.QuickFix
+            );
+            action.edit = new vscode.WorkspaceEdit();
+            action.edit.replace(document.uri, diagnostic.range, '=');
+            action.diagnostics = [diagnostic];
+            action.isPreferred = true;
+            actions.push(action);
+
+            // Fix all in file
+            const fixAllAction = new vscode.CodeAction(
+                'Change all : to = in file',
+                vscode.CodeActionKind.QuickFix
+            );
+            fixAllAction.edit = this.createFixAllColonAssignments(document);
+            fixAllAction.diagnostics = [diagnostic];
+            actions.push(fixAllAction);
+        }
+
+        // Wrap math expression in {{ }}
+        if (diagnostic.code === 'math-outside-braces') {
+            const mathExpression = document.getText(diagnostic.range);
+            const wrappedExpression = `{{ ${mathExpression} }}`;
+            
+            const action = new vscode.CodeAction(
+                `Wrap in {{ }}`,
+                vscode.CodeActionKind.QuickFix
+            );
+            action.edit = new vscode.WorkspaceEdit();
+            action.edit.replace(document.uri, diagnostic.range, wrappedExpression);
+            action.diagnostics = [diagnostic];
+            action.isPreferred = true;
+            actions.push(action);
+            
+            // Also provide "Fix all in file" option
+            const fixAllAction = new vscode.CodeAction(
+                'Fix all math operations in file',
+                vscode.CodeActionKind.QuickFix
+            );
+            fixAllAction.edit = createFixAllMathOperations(document);
+            fixAllAction.diagnostics = [diagnostic];
+            actions.push(fixAllAction);
+        }
+
+        // Combine consecutive filters
+        if (diagnostic.code === 'inefficient-filter') {
+            const diagnosticText = document.getText(diagnostic.range);
+            
+            // Extract filter conditions from the diagnostic range
+            // Pattern: | filter(condition1) | filter(condition2) | filter(condition3) ...
+            const filterPattern = /\|\s*filter\s*\(([^)]+)\)/g;
+            const filterMatches: string[] = [];
+            let match;
+            
+            while ((match = filterPattern.exec(diagnosticText)) !== null) {
+                filterMatches.push(match[1].trim());
+            }
+            
+            if (filterMatches.length >= 2) {
+                // Combine all filter conditions with "and"
+                const combinedCondition = filterMatches.join(' and ');
+                
+                // Replace all consecutive filters with a single combined filter
+                // Match: | filter(...) | filter(...) | filter(...) ...
+                // We need to match the entire sequence of consecutive filters
+                const filterSequencePattern = /(\|\s*filter\s*\([^)]+\)\s*)+/;
+                const replacement = diagnosticText.replace(
+                    filterSequencePattern,
+                    `| filter(${combinedCondition})`
+                );
+                
+                const action = new vscode.CodeAction(
+                    'Combine filters with "and" operator',
+                    vscode.CodeActionKind.QuickFix
+                );
+                action.edit = new vscode.WorkspaceEdit();
+                action.edit.replace(document.uri, diagnostic.range, replacement);
+                action.diagnostics = [diagnostic];
+                action.isPreferred = true;
+                actions.push(action);
+            }
+        }
+
+        // Replace single-push-array pattern with [ item ] and remove intermediary variable
+        if (diagnostic.code === 'single-push-array') {
+            const arrayVarName = (diagnostic as any).arrayVarName as string | undefined;
+            const initLineIndex = (diagnostic as any).initLineIndex as number | undefined;
+            const pushArgument = (diagnostic as any).pushArgument as string | undefined;
+            const usageStartOffset = (diagnostic as any).usageStartOffset as number | undefined;
+            const usageEndOffset = (diagnostic as any).usageEndOffset as number | undefined;
+            if (arrayVarName !== undefined && initLineIndex !== undefined && pushArgument !== undefined &&
+                usageStartOffset !== undefined && usageEndOffset !== undefined) {
+                const action = new vscode.CodeAction(
+                    'Use [ item ] instead of variable and push()',
+                    vscode.CodeActionKind.QuickFix
+                );
+                action.edit = new vscode.WorkspaceEdit();
+                const usageRange = new vscode.Range(
+                    document.positionAt(usageStartOffset),
+                    document.positionAt(usageEndOffset)
+                );
+                action.edit.replace(document.uri, usageRange, `[ ${pushArgument} ]`);
+                const initLine = document.lineAt(initLineIndex);
+                const deleteEnd = initLineIndex + 1 < document.lineCount
+                    ? new vscode.Position(initLineIndex + 1, 0)
+                    : new vscode.Position(initLineIndex, initLine.text.length);
+                action.edit.delete(document.uri, new vscode.Range(initLine.range.start, deleteEnd));
+                action.diagnostics = [diagnostic];
+                action.isPreferred = true;
+                actions.push(action);
+            }
+        }
+
+        // Convert foreach to map
+        if (diagnostic.code === 'foreach-to-map') {
+            const arrayVarName = (diagnostic as any).arrayVarName as string | undefined;
+            const arrayInitLine = (diagnostic as any).arrayInitLine as number | undefined;
+            const arrayVar = (diagnostic as any).arrayVar as string | undefined;
+            const mapExpression = (diagnostic as any).mapExpression as string | undefined;
+            const foreachStartLine = (diagnostic as any).foreachStartLine as number | undefined;
+            const endforLine = (diagnostic as any).endforLine as number | undefined;
+            
+            if (arrayVarName && arrayInitLine !== undefined && arrayVar && mapExpression && 
+                foreachStartLine !== undefined && endforLine !== undefined) {
+                
+                const action = new vscode.CodeAction(
+                    'Convert foreach to map()',
+                    vscode.CodeActionKind.QuickFix
+                );
+                action.edit = new vscode.WorkspaceEdit();
+                
+                // Update the array initialization line to use map
+                // Preserve the original assignment operator (= or :)
+                const initLine = document.lineAt(arrayInitLine);
+                const initLineText = initLine.text;
+                const assignmentOp = initLineText.includes(':') ? ':' : '=';
+                
+                // Get indentation from the original line
+                const indentMatch = initLineText.match(/^(\s*)/);
+                const indent = indentMatch ? indentMatch[1] : '';
+                
+                const newInitLine = `${indent}$${arrayVarName} ${assignmentOp} ${arrayVar} | map(${mapExpression});`;
+                action.edit.replace(document.uri, initLine.range, newInitLine);
+                
+                // Remove the foreach loop (from foreach line to endfor line, including the endfor line)
+                const endforLineObj = document.lineAt(endforLine);
+                // Delete from start of foreach line to end of endfor line
+                // If not the last line, also delete the newline after endfor
+                const foreachRange = new vscode.Range(
+                    foreachStartLine,
+                    0,
+                    endforLine,
+                    endforLineObj.text.length
+                );
+                action.edit.delete(document.uri, foreachRange);
+                
+                action.diagnostics = [diagnostic];
+                action.isPreferred = true;
+                actions.push(action);
+            }
+        }
+
+        // Fix duplicate function - rename to unique name (changing casing won't fix it since ISL is case-insensitive)
+        if (diagnostic.code === 'duplicate-function') {
+            const functionName = (diagnostic as any).functionName as string | undefined;
+            const kind = (diagnostic as any).kind as 'fun' | 'modifier' | undefined;
+            
+            if (functionName && kind) {
+                const action = new vscode.CodeAction(
+                    'Rename to unique name',
+                    vscode.CodeActionKind.QuickFix
+                );
+                action.command = {
+                    command: 'isl.quickFix.renameDuplicateFunction',
+                    title: 'Rename duplicate to unique name',
+                    arguments: [document.uri, diagnostic.range.start.line, functionName, kind]
+                };
+                action.diagnostics = [diagnostic];
+                action.isPreferred = true;
+                actions.push(action);
+            }
+        }
+
+        // Fix naming convention (skip when it's a casing-only change and there's a duplicate - casing won't fix duplicates)
+        if (diagnostic.code === 'naming-convention') {
+            const originalName = (diagnostic as any).originalName as string | undefined;
+            const correctName = (diagnostic as any).correctName as string | undefined;
+            const type = (diagnostic as any).type as 'function' | 'modifier' | undefined;
+            
+            // Don't offer casing-only rename when there's a duplicate - it won't fix the duplicate
+            const isCasingOnlyChange = originalName && correctName && originalName.toLowerCase() === correctName.toLowerCase();
+            const hasDuplicateOnSameRange = isCasingOnlyChange && allDiagnostics.some(d =>
+                d.code === 'duplicate-function' &&
+                d.range.start.line === diagnostic.range.start.line &&
+                d.range.start.character === diagnostic.range.start.character
+            );
+            if (hasDuplicateOnSameRange) {
+                // Skip - the duplicate quick fix is the right one
+            } else if (originalName && correctName && type) {
+                // Rename in declaration
+                const action = new vscode.CodeAction(
+                    `Rename ${type} to '${correctName}'`,
+                    vscode.CodeActionKind.QuickFix
+                );
+                action.edit = new vscode.WorkspaceEdit();
+                
+                // Replace the name in the declaration
+                action.edit.replace(document.uri, diagnostic.range, correctName);
+                
+                // Also rename all usages in the document
+                const text = document.getText();
+                const lines = text.split('\n');
+                
+                for (let i = 0; i < lines.length; i++) {
+                    const line = lines[i];
+                    
+                    // Skip the declaration line (already handled)
+                    if (i === diagnostic.range.start.line) {
+                        continue;
+                    }
+                    
+                    // Find usages of the function/modifier
+                    // For functions: @.This.functionName( or @.ModuleName.functionName(
+                    // For modifiers: | modifierName(
+                    let pattern: RegExp;
+                    const escapedName = originalName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+                    if (type === 'function') {
+                        // Match: @.This.functionName( or @.ModuleName.functionName(
+                        pattern = new RegExp(`@\\.(?:This|[A-Z][a-zA-Z0-9_]*)\\.${escapedName}\\s*\\(`, 'g');
+                    } else {
+                        // Match: | modifierName(
+                        pattern = new RegExp(`\\|\\s*${escapedName}\\s*\\(`, 'g');
+                    }
+                    
+                    let match;
+                    while ((match = pattern.exec(line)) !== null) {
+                        const matchStart = match.index;
+                        const matchEnd = matchStart + match[0].length;
+                        
+                        // Find the position of the name within the match
+                        const nameStart = line.indexOf(originalName, matchStart);
+                        if (nameStart !== -1 && nameStart < matchEnd) {
+                            const nameRange = new vscode.Range(
+                                i,
+                                nameStart,
+                                i,
+                                nameStart + originalName.length
+                            );
+                            action.edit.replace(document.uri, nameRange, correctName);
+                        }
+                    }
+                }
+                
+                action.diagnostics = [diagnostic];
+                action.isPreferred = true;
+                actions.push(action);
+            }
+        }
+
+        // Convert function to modifier
+        if (diagnostic.code === 'function-to-modifier') {
+            const functionName = (diagnostic as any).functionName as string | undefined;
+            const definitionLine = (diagnostic as any).definitionLine as number | undefined;
+            
+            if (functionName && definitionLine !== undefined) {
+                const action = new vscode.CodeAction(
+                    `Convert function '${functionName}' to modifier`,
+                    vscode.CodeActionKind.QuickFix
+                );
+                action.edit = new vscode.WorkspaceEdit();
+                
+                // Change 'fun' to 'modifier' in the definition
+                const defLine = document.lineAt(definitionLine);
+                const newDefLine = defLine.text.replace(/^\s*fun\s+/, (match) => match.replace('fun', 'modifier'));
+                action.edit.replace(document.uri, defLine.range, newDefLine);
+                
+                // Update all usages: @.This.functionName(...) to | functionName(...)
+                const text = document.getText();
+                const lines = text.split('\n');
+                const escapedName = functionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+                
+                for (let i = 0; i < lines.length; i++) {
+                    const line = lines[i];
+                    
+                    // Skip the definition line (already handled)
+                    if (i === definitionLine) {
+                        continue;
+                    }
+                    
+                    // Skip comments
+                    const commentIndex = Math.min(
+                        line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+                        line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+                    );
+                    const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+                    
+                    // Find @.This.functionName(...) calls
+                    // Since we only flag functions always used with pipe, we expect | before the call
+                    const functionCallPattern = new RegExp(`@\\.This\\.${escapedName}\\s*\\(`, 'g');
+                    let match;
+                    
+                    while ((match = functionCallPattern.exec(codeOnlyLine)) !== null) {
+                        const callStart = match.index;
+                        
+                        // Extract the function call with parameters
+                        // We need to find the matching closing parenthesis
+                        let parenCount = 1;
+                        let paramStart = callStart + match[0].length;
+                        let paramEnd = paramStart;
+                        let foundParams = false;
+                        
+                        for (let j = paramStart; j < codeOnlyLine.length; j++) {
+                            const char = codeOnlyLine[j];
+                            if (char === '(') {
+                                parenCount++;
+                            } else if (char === ')') {
+                                parenCount--;
+                                if (parenCount === 0) {
+                                    paramEnd = j + 1;
+                                    foundParams = true;
+                                    break;
+                                }
+                            }
+                        }
+                        
+                        if (foundParams) {
+                            // Replace @.This.functionName with just functionName
+                            // The pipe should already be there since we only flag functions used with pipe
+                            const fullCallRange = new vscode.Range(
+                                i,
+                                callStart,
+                                i,
+                                paramEnd
+                            );
+                            
+                            const fullCall = codeOnlyLine.substring(callStart, paramEnd);
+                            const newCall = fullCall.replace(new RegExp(`@\\.This\\.${escapedName}`), functionName);
+                            action.edit.replace(document.uri, fullCallRange, newCall);
+                        }
+                    }
+                }
+                
+                action.diagnostics = [diagnostic];
+                action.isPreferred = true;
+                actions.push(action);
+            }
+        }
+
+        // Fix inconsistent spacing
+        if (diagnostic.code === 'inconsistent-spacing') {
+            const line = document.lineAt(diagnostic.range.start.line);
+            const lineText = line.text;
+            const diagnosticPos = diagnostic.range.start.character;
+            const charAtPos = lineText[diagnosticPos];
+            
+            let fixedText = lineText;
+            
+            // Fix based on what character is at the diagnostic position
+            if (charAtPos === ':') {
+                // Missing space after :
+                if (lineText[diagnosticPos + 1] !== ' ') {
+                    fixedText = lineText.substring(0, diagnosticPos + 1) + ' ' + lineText.substring(diagnosticPos + 1);
+                }
+            } else if (charAtPos === '|') {
+                // Missing spaces around |
+                const beforePipe = lineText[diagnosticPos - 1];
+                const afterPipe = lineText[diagnosticPos + 1];
+                
+                if (beforePipe !== ' ') {
+                    fixedText = fixedText.substring(0, diagnosticPos) + ' ' + fixedText.substring(diagnosticPos);
+                }
+                if (afterPipe !== ' ') {
+                    const insertPos = beforePipe !== ' ' ? diagnosticPos + 2 : diagnosticPos + 1;
+                    fixedText = fixedText.substring(0, insertPos) + ' ' + fixedText.substring(insertPos);
+                }
+            }
+            
+            if (fixedText !== lineText) {
+                const action = new vscode.CodeAction(
+                    'Fix spacing',
+                    vscode.CodeActionKind.QuickFix
+                );
+                action.edit = new vscode.WorkspaceEdit();
+                action.edit.replace(document.uri, line.range, fixedText);
+                action.diagnostics = [diagnostic];
+                action.isPreferred = true;
+                actions.push(action);
+            }
+        }
+
+        // Fix foreach variable scoping
+        if (diagnostic.code === 'foreach-variable-scoping') {
+            const loopVar = (diagnostic as any).loopVar as string | undefined;
+            const foreachStartLine = (diagnostic as any).foreachStartLine as number | undefined;
+            
+            if (loopVar && foreachStartLine !== undefined) {
+                const action = new vscode.CodeAction(
+                    `Convert foreach to map() to fix variable scoping`,
+                    vscode.CodeActionKind.QuickFix
+                );
+                action.command = {
+                    command: 'editor.action.showReferences',
+                    title: 'Show foreach loop',
+                    arguments: [document.uri, diagnostic.range.start, [{ uri: document.uri, range: new vscode.Range(foreachStartLine, 0, foreachStartLine, 0) }]]
+                };
+                action.diagnostics = [diagnostic];
+                actions.push(action);
+            }
+        }
+
+        // Fix implicit type conversion
+        if (diagnostic.code === 'implicit-type-conversion') {
+            const conversionModifier = (diagnostic as any).conversionModifier as string | undefined;
+            const rightVar = (diagnostic as any).rightVar as string | undefined;
+            
+            if (conversionModifier && rightVar) {
+                const line = document.lineAt(diagnostic.range.start.line);
+                const lineText = line.text;
+                
+                // Find the right variable in the line and add the conversion modifier after it
+                const rightVarIndex = diagnostic.range.start.character;
+                const rightVarEnd = diagnostic.range.end.character;
+                
+                // Check what comes after the variable
+                const afterVar = lineText.substring(rightVarEnd);
+                const trimmedAfter = afterVar.trim();
+                
+                // Add the conversion modifier after the variable
+                const replacement = ` | ${conversionModifier}`;
+                const insertPos = new vscode.Position(
+                    diagnostic.range.start.line,
+                    rightVarEnd
+                );
+                
+                const action = new vscode.CodeAction(
+                    `Add explicit conversion: | ${conversionModifier}`,
+                    vscode.CodeActionKind.QuickFix
+                );
+                action.edit = new vscode.WorkspaceEdit();
+                action.edit.insert(document.uri, insertPos, replacement);
+                action.diagnostics = [diagnostic];
+                action.isPreferred = true;
+                actions.push(action);
+            }
+        }
+        
+        return actions;
+    }
+    
+    private createRefactoringActions(document: vscode.TextDocument, range: vscode.Range): vscode.CodeAction[] {
+        const actions: vscode.CodeAction[] = [];
+        const selectedText = document.getText(range);
+        
+        // Extract to variable
+        if (this.canExtractToVariable(selectedText)) {
+            const action = new vscode.CodeAction(
+                'Extract to variable',
+                vscode.CodeActionKind.RefactorExtract
+            );
+            action.command = {
+                command: 'isl.refactor.extractVariable',
+                title: 'Extract to variable',
+                arguments: [document, range]
+            };
+            actions.push(action);
+        }
+        
+        // Extract to function
+        if (this.canExtractToFunction(selectedText, document, range)) {
+            const action = new vscode.CodeAction(
+                'Extract to function',
+                vscode.CodeActionKind.RefactorExtract
+            );
+            action.command = {
+                command: 'isl.refactor.extractFunction',
+                title: 'Extract to function',
+                arguments: [document, range]
+            };
+            actions.push(action);
+        }
+        
+        // Convert to template string
+        if (selectedText.match(/\$\w+(\.\w+)*\s*\+\s*["']/)) {
+            const action = new vscode.CodeAction(
+                'Convert to template string',
+                vscode.CodeActionKind.RefactorRewrite
+            );
+            action.command = {
+                command: 'isl.refactor.toTemplateString',
+                title: 'Convert to template string',
+                arguments: [document, range]
+            };
+            actions.push(action);
+        }
+        
+        return actions;
+    }
+    
+    private createImprovementActions(document: vscode.TextDocument, line: vscode.TextLine, range: vscode.Range): vscode.CodeAction[] {
+        const actions: vscode.CodeAction[] = [];
+        const lineText = line.text;
+        
+        // Suggest simplifying string interpolation for simple variables
+        const unnecessaryInterpolation = /\$\{(\$[a-zA-Z_][a-zA-Z0-9_]*)\}/g;
+        let match;
+        while ((match = unnecessaryInterpolation.exec(lineText)) !== null) {
+            const fullMatch = match[0]; // ${$variable}
+            const variable = match[1];  // $variable
+            
+            // Only suggest if it's a simple variable (no dots)
+            if (!variable.includes('.')) {
+                const startPos = line.range.start.character + match.index;
+                const endPos = startPos + fullMatch.length;
+                const replaceRange = new vscode.Range(
+                    line.lineNumber,
+                    startPos,
+                    line.lineNumber,
+                    endPos
+                );
+                
+                const action = new vscode.CodeAction(
+                    `Simplify to ${variable} (remove unnecessary braces)`,
+                    vscode.CodeActionKind.RefactorRewrite
+                );
+                action.edit = new vscode.WorkspaceEdit();
+                action.edit.replace(document.uri, replaceRange, variable);
+                action.isPreferred = true;
+                actions.push(action);
+            }
+        }
+        
+        // Suggest using ?? instead of | default()
+        if (lineText.match(/\|\s*default\s*\(/)) {
+            const action = new vscode.CodeAction(
+                'Use ?? operator instead of default()',
+                vscode.CodeActionKind.QuickFix
+            );
+            action.command = {
+                command: 'isl.improvement.useCoalesceOperator',
+                title: 'Use ?? operator',
+                arguments: [document, line.range]
+            };
+            actions.push(action);
+        }
+        
+        // Suggest simplifying nested ifs
+        if (lineText.match(/^\s*if\s*\(/) && this.hasNestedIf(document, line.lineNumber)) {
+            const action = new vscode.CodeAction(
+                'Simplify nested conditions',
+                vscode.CodeActionKind.RefactorRewrite
+            );
+            action.command = {
+                command: 'isl.improvement.simplifyNestedIfs',
+                title: 'Simplify conditions',
+                arguments: [document, line.lineNumber]
+            };
+            actions.push(action);
+        }
+        
+        // Suggest using Math.sum instead of reduce
+        if (lineText.match(/\|\s*reduce\s*\(\s*\{\{\s*\$acc\s*\+\s*\$it\s*\}\}/)) {
+            const action = new vscode.CodeAction(
+                'Use Math.sum() instead of reduce',
+                vscode.CodeActionKind.QuickFix
+            );
+            action.command = {
+                command: 'isl.improvement.useMathSum',
+                title: 'Use Math.sum()',
+                arguments: [document, line.range]
+            };
+            action.isPreferred = true;
+            actions.push(action);
+        }
+        
+        // Suggest formatting long modifier chains
+        if (this.hasLongModifierChain(lineText)) {
+            const action = new vscode.CodeAction(
+                'Format modifier chain on multiple lines',
+                vscode.CodeActionKind.RefactorRewrite
+            );
+            action.command = {
+                command: 'isl.improvement.formatChain',
+                title: 'Format chain',
+                arguments: [document, line.range]
+            };
+            actions.push(action);
+        }
+        
+        // Suggest formatting long object declarations
+        if (this.hasLongObjectDeclaration(lineText)) {
+            const action = new vscode.CodeAction(
+                'Format object on multiple lines',
+                vscode.CodeActionKind.RefactorRewrite
+            );
+            action.command = {
+                command: 'isl.improvement.formatObject',
+                title: 'Format object',
+                arguments: [document, line.range]
+            };
+            actions.push(action);
+        }
+        
+        return actions;
+    }
+    
+    /**
+     * Parse RHS like "accountId=" + $acc.accountId + "&startTime=" + $startTime
+     * and convert to `accountId=${ $acc.accountId }&startTime=${ $startTime }`
+     */
+    private convertPlusConcatenationToInterpolation(rhs: string): string | null {
+        type Segment = { type: 'string'; value: string } | { type: 'expr'; value: string };
+        const segments: Segment[] = [];
+        let i = 0;
+        const s = rhs.trim();
+        while (i < s.length) {
+            while (i < s.length && /\s/.test(s[i])) i++;
+            if (i >= s.length) break;
+            if (s[i] === '+') {
+                i++;
+                continue;
+            }
+            if (s[i] === '"' || s[i] === "'") {
+                const q = s[i];
+                i++;
+                let lit = '';
+                while (i < s.length && s[i] !== q) {
+                    if (s[i] === '\\') {
+                        i++;
+                        if (i < s.length) lit += s[i++];
+                    } else {
+                        lit += s[i++];
+                    }
+                }
+                if (i < s.length) i++;
+                segments.push({ type: 'string', value: lit });
+                continue;
+            }
+            if (s[i] === '$') {
+                const exprStart = i;
+                i++;
+                while (i < s.length && /[a-zA-Z0-9_.]/.test(s[i])) i++;
+                const expr = s.substring(exprStart, i).trim();
+                if (expr) segments.push({ type: 'expr', value: expr });
+                continue;
+            }
+            return null;
+        }
+        let out = '`';
+        for (const seg of segments) {
+            if (seg.type === 'string') {
+                out += seg.value.replace(/\\/g, '\\\\').replace(/`/g, '\\`');
+            } else {
+                out += '${ ' + seg.value + ' }';
+            }
+        }
+        out += '`';
+        return out;
+    }
+
+    private createFix(
+        title: string,
+        document: vscode.TextDocument,
+        range: vscode.Range,
+        replacement: string,
+        diagnostic: vscode.Diagnostic
+    ): vscode.CodeAction {
+        const action = new vscode.CodeAction(title, vscode.CodeActionKind.QuickFix);
+        action.edit = new vscode.WorkspaceEdit();
+        action.edit.replace(document.uri, range, replacement);
+        action.diagnostics = [diagnostic];
+        action.isPreferred = true;
+        return action;
+    }
+    
+    private canExtractToVariable(text: string): boolean {
+        // Can extract expressions, not simple variables
+        return text.trim().length > 2 && 
+               !text.match(/^\$\w+$/) && 
+               (text.includes('|') || text.includes('{{') || text.includes('@.'));
+    }
+    
+    private canExtractToFunction(text: string, document: vscode.TextDocument, range: vscode.Range): boolean {
+        // Can extract multi-line blocks or complex expressions
+        const lines = text.split('\n');
+        return lines.length > 1 || this.canExtractToVariable(text);
+    }
+    
+    private hasNestedIf(document: vscode.TextDocument, lineNumber: number): boolean {
+        // Check if there's a nested if within the next few lines
+        for (let i = lineNumber + 1; i < Math.min(lineNumber + 10, document.lineCount); i++) {
+            const line = document.lineAt(i).text.trim();
+            if (line.startsWith('if (')) {
+                return true;
+            }
+            if (line.startsWith('endif')) {
+                return false;
+            }
+        }
+        return false;
+    }
+    
+    private hasLongModifierChain(line: string): boolean {
+        // Check if line has 3+ modifiers or is longer than 100 chars with modifiers
+        const pipeCount = (line.match(/\|/g) || []).length;
+        return pipeCount >= 3 || (pipeCount >= 2 && line.length > 100);
+    }
+    
+    private hasLongObjectDeclaration(line: string): boolean {
+        // Check if line has an object declaration that's too long
+        // Look for { ... } pattern with multiple properties
+        if (line.length < 100) {
+            return false;
+        }
+        
+        // Check if line contains object with multiple properties
+        const objectMatch = line.match(/\{[^}]+:[^}]+:[^}]+\}/);
+        return objectMatch !== null;
+    }
+
+    private createFixAllColonAssignments(document: vscode.TextDocument): vscode.WorkspaceEdit {
+        const edit = new vscode.WorkspaceEdit();
+        const text = document.getText();
+        const lines = text.split('\n');
+
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            
+            // Skip comments
+            const commentIndex = Math.min(
+                line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+                line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+            );
+            const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+
+            // Check for variable assignment using : instead of =
+            // Match: $varName: (at the start of line, optionally with whitespace)
+            const colonAssignmentPattern = /^(\s*)(\$[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)(\s*)(:)/;
+            const match = codeOnlyLine.match(colonAssignmentPattern);
+
+            if (match) {
+                // Skip typed assignment: $var : type = ... (colon is type separator)
+                const afterColon = codeOnlyLine.substring(match[0].length);
+                if (/^\s*[a-zA-Z_][a-zA-Z0-9_.:]*\s*=/.test(afterColon)) {
+                    continue;
+                }
+                const colonPos = match[1].length + match[2].length + match[3].length;
+                const range = new vscode.Range(i, colonPos, i, colonPos + 1);
+                edit.replace(document.uri, range, '=');
+            }
+        }
+
+        return edit;
+    }
+}
+
+// Refactoring command implementations
+export async function extractVariable(document: vscode.TextDocument, range: vscode.Range) {
+    const selectedText = document.getText(range);
+    
+    const varName = await vscode.window.showInputBox({
+        prompt: 'Enter variable name',
+        value: 'extracted',
+        validateInput: (text) => {
+            if (!text.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
+                return 'Invalid variable name';
+            }
+            return null;
+        }
+    });
+    
+    if (!varName) {
+        return;
+    }
+    
+    const edit = new vscode.WorkspaceEdit();
+    
+    // Find the start of the statement/line to insert before it
+    const line = document.lineAt(range.start.line);
+    const indent = line.text.match(/^\s*/)?.[0] || '';
+    const insertPosition = new vscode.Position(range.start.line, 0);
+    
+    // Insert variable declaration
+    edit.insert(document.uri, insertPosition, `${indent}$${varName} = ${selectedText.trim()};\n`);
+    
+    // Replace selected text with variable reference
+    edit.replace(document.uri, range, `$${varName}`);
+    
+    await vscode.workspace.applyEdit(edit);
+}
+
+export async function extractFunction(document: vscode.TextDocument, range: vscode.Range) {
+    const selectedText = document.getText(range);
+    
+    const funcName = await vscode.window.showInputBox({
+        prompt: 'Enter function name',
+        value: 'extracted',
+        validateInput: (text) => {
+            if (!text.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
+                return 'Invalid function name';
+            }
+            return null;
+        }
+    });
+    
+    if (!funcName) {
+        return;
+    }
+    
+    // Find variables used in selection
+    const variables = findVariablesInText(selectedText);
+    const params = variables.join(', ');
+    
+    const edit = new vscode.WorkspaceEdit();
+    
+    // Insert function at the top of the file
+    const funcDeclaration = `fun ${funcName}(${params}) {\n    return ${selectedText.trim()};\n}\n\n`;
+    edit.insert(document.uri, new vscode.Position(0, 0), funcDeclaration);
+    
+    // Replace selected text with function call
+    const args = variables.join(', ');
+    edit.replace(document.uri, range, `@.This.${funcName}(${args})`);
+    
+    await vscode.workspace.applyEdit(edit);
+}
+
+export async function convertToTemplateString(document: vscode.TextDocument, range: vscode.Range) {
+    const selectedText = document.getText(range);
+    
+    // Convert string concatenation to template string
+    const converted = selectedText
+        .replace(/\$(\w+(?:\.\w+)*)\s*\+\s*["']([^"']*)["']/g, '`${$$$1}$2`')
+        .replace(/["']([^"']*)["']\s*\+\s*\$(\w+(?:\.\w+)*)/g, '`$1${$$$2}`')
+        .replace(/\$(\w+(?:\.\w+)*)/g, '${$$$1}');
+    
+    const edit = new vscode.WorkspaceEdit();
+    edit.replace(document.uri, range, converted);
+    
+    await vscode.workspace.applyEdit(edit);
+}
+
+export async function useCoalesceOperator(document: vscode.TextDocument, range: vscode.Range) {
+    const lineText = document.getText(range);
+    
+    // Convert | default(value) to ?? value
+    const converted = lineText.replace(/\|\s*default\s*\(\s*([^)]+)\s*\)/g, '?? $1');
+    
+    const edit = new vscode.WorkspaceEdit();
+    edit.replace(document.uri, range, converted);
+    
+    await vscode.workspace.applyEdit(edit);
+}
+
+export async function useMathSum(document: vscode.TextDocument, range: vscode.Range) {
+    const lineText = document.getText(range);
+    
+    // Convert reduce({{ $acc + $it }}, 0) to Math.sum(0)
+    const converted = lineText.replace(/\|\s*reduce\s*\(\s*\{\{\s*\$acc\s*\+\s*\$it\s*\}\}\s*,\s*(\d+)\s*\)/g, '| Math.sum($1)');
+    
+    const edit = new vscode.WorkspaceEdit();
+    edit.replace(document.uri, range, converted);
+    
+    await vscode.workspace.applyEdit(edit);
+}
+
+export async function formatChain(document: vscode.TextDocument, range: vscode.Range) {
+    const lineText = document.getText(range);
+    
+    // Split long chains into multiple lines
+    const indent = lineText.match(/^\s*/)?.[0] || '';
+    const parts = lineText.split('|').map(p => p.trim()).filter(p => p);
+    
+    if (parts.length === 0) {
+        return;
+    }
+    
+    const firstPart = parts[0];
+    const modifiers = parts.slice(1);
+    
+    const formatted = `${indent}${firstPart}\n${modifiers.map(m => `${indent}    | ${m}`).join('\n')}`;
+    
+    const edit = new vscode.WorkspaceEdit();
+    edit.replace(document.uri, range, formatted);
+    
+    await vscode.workspace.applyEdit(edit);
+}
+
+export async function formatObject(document: vscode.TextDocument, range: vscode.Range) {
+    const lineText = document.getText(range);
+    
+    // Find the opening brace
+    const openBraceMatch = lineText.match(/^(\s*)(.*?)(\{)/);
+    if (!openBraceMatch) {
+        return;
+    }
+    
+    const indent = openBraceMatch[1];
+    const beforeBrace = openBraceMatch[2];
+    
+    // Find matching closing brace (accounting for strings and nesting)
+    let depth = 0;
+    let inString = false;
+    let stringChar = '';
+    let objectStart = -1;
+    let objectEnd = -1;
+    
+    for (let i = 0; i < lineText.length; i++) {
+        const char = lineText[i];
+        const prevChar = i > 0 ? lineText[i - 1] : '';
+        
+        // Track string boundaries
+        if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
+            if (!inString) {
+                inString = true;
+                stringChar = char;
+            } else if (char === stringChar) {
+                inString = false;
+            }
+        }
+        
+        // Track braces outside of strings
+        if (!inString) {
+            if (char === '{') {
+                if (depth === 0) {
+                    objectStart = i;
+                }
+                depth++;
+            } else if (char === '}') {
+                depth--;
+                if (depth === 0) {
+                    objectEnd = i;
+                    break;
+                }
+            }
+        }
+    }
+    
+    if (objectStart === -1 || objectEnd === -1) {
+        return;
+    }
+    
+    const objectContent = lineText.substring(objectStart + 1, objectEnd);
+    const afterBrace = lineText.substring(objectEnd + 1);
+    
+    // Parse properties - reset tracking variables
+    const properties: string[] = [];
+    let currentProp = '';
+    depth = 0;
+    inString = false;
+    stringChar = '';
+    
+    for (let i = 0; i < objectContent.length; i++) {
+        const char = objectContent[i];
+        const prevChar = i > 0 ? objectContent[i - 1] : '';
+        
+        // Track string boundaries
+        if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
+            if (!inString) {
+                inString = true;
+                stringChar = char;
+            } else if (char === stringChar) {
+                inString = false;
+            }
+        }
+        
+        // Track nested braces/brackets
+        if (!inString) {
+            if (char === '{' || char === '[' || char === '(') {
+                depth++;
+            } else if (char === '}' || char === ']' || char === ')') {
+                depth--;
+            }
+        }
+        
+        // Split on comma at depth 0
+        if (char === ',' && depth === 0 && !inString) {
+            properties.push(currentProp.trim());
+            currentProp = '';
+        } else {
+            currentProp += char;
+        }
+    }
+    
+    // Add the last property
+    if (currentProp.trim()) {
+        properties.push(currentProp.trim());
+    }
+    
+    // Format as multi-line object
+    const formattedProperties = properties
+        .map(prop => `${indent}    ${prop}`)
+        .join(',\n');
+    
+    const formatted = `${indent}${beforeBrace}{\n${formattedProperties}\n${indent}}${afterBrace}`;
+    
+    const edit = new vscode.WorkspaceEdit();
+    edit.replace(document.uri, range, formatted);
+    
+    await vscode.workspace.applyEdit(edit);
+}
+
+function findVariablesInText(text: string): string[] {
+    const variables = new Set();
+    const varPattern = /\$([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)/g;
+    let match;
+    
+    while ((match = varPattern.exec(text)) !== null) {
+        variables.add('$' + match[1]);
+    }
+    
+    return Array.from(variables);
+}
+
+// Helper function to fix all math operations in a file
+function createFixAllMathOperations(document: vscode.TextDocument): vscode.WorkspaceEdit {
+    const edit = new vscode.WorkspaceEdit();
+    const text = document.getText();
+    const lines = text.split('\n');
+
+    for (let i = 0; i < lines.length; i++) {
+        const line = lines[i];
+        
+        // Skip comments
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+
+        if (codeOnlyLine.trim() === '') {
+            continue;
+        }
+
+        // Find all {{ ... }} blocks to exclude them
+        const mathBlockPattern = /\{\{([^}]+)\}\}/g;
+        const mathBlocks: Array<{ start: number; end: number }> = [];
+        let mathBlockMatch;
+        while ((mathBlockMatch = mathBlockPattern.exec(codeOnlyLine)) !== null) {
+            mathBlocks.push({
+                start: mathBlockMatch.index,
+                end: mathBlockMatch.index + mathBlockMatch[0].length
+            });
+        }
+
+        // Find all template strings to exclude them
+        const backtickPattern = /`(?:[^`\\]|\\.)*`/g;
+        const templateStrings: Array<{ start: number; end: number }> = [];
+        let backtickMatch;
+        while ((backtickMatch = backtickPattern.exec(codeOnlyLine)) !== null) {
+            templateStrings.push({
+                start: backtickMatch.index,
+                end: backtickMatch.index + backtickMatch[0].length
+            });
+        }
+
+        const allExclusionBlocks = [...mathBlocks, ...templateStrings];
+        const isInsideExclusionBlocks = (start: number, end: number): boolean => {
+            return allExclusionBlocks.some(block => start >= block.start && end <= block.end);
+        };
+
+        // Find math operations
+        const mathExpressionPattern = /((?:\$[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*|\d+(?:\.\d+)?))\s*([+\-*/%])\s*((?:\$[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*|\d+(?:\.\d+)?))/g;
+        let mathMatch;
+        
+        while ((mathMatch = mathExpressionPattern.exec(codeOnlyLine)) !== null) {
+            const matchStart = mathMatch.index;
+            const matchEnd = matchStart + mathMatch[0].length;
+            
+            if (isInsideExclusionBlocks(matchStart, matchEnd)) {
+                continue;
+            }
+
+            // Check if it's a comparison operator
+            const beforeOp = codeOnlyLine.substring(Math.max(0, matchStart - 1), matchStart);
+            const afterOp = codeOnlyLine.substring(matchEnd, Math.min(codeOnlyLine.length, matchEnd + 1));
+            const operator = mathMatch[2];
+            
+            if (operator === '=' && (beforeOp === '=' || beforeOp === '!' || afterOp === '=')) {
+                continue;
+            }
+            if (operator === '<' && afterOp === '=') {
+                continue;
+            }
+            if (operator === '>' && afterOp === '=') {
+                continue;
+            }
+            if (operator === '<' || operator === '>') {
+                const beforeMatch = codeOnlyLine.substring(0, matchStart);
+                if (beforeMatch.match(/\b(if|while|switch|filter|map)\s*\(/)) {
+                    continue;
+                }
+            }
+
+            // Wrap in {{ }}
+            const mathExpression = mathMatch[0];
+            const wrappedExpression = `{{ ${mathExpression} }}`;
+            const range = new vscode.Range(i, matchStart, i, matchEnd);
+            edit.replace(document.uri, range, wrappedExpression);
+        }
+    }
+
+    return edit;
+}
+
+
diff --git a/plugin/src/codelens.ts b/plugin/src/codelens.ts
new file mode 100644
index 0000000..e0e2d2e
--- /dev/null
+++ b/plugin/src/codelens.ts
@@ -0,0 +1,545 @@
+import * as vscode from 'vscode';
+import * as path from 'path';
+import * as fs from 'fs';
+
+export class IslCodeLensProvider implements vscode.CodeLensProvider {
+    private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter();
+    public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event;
+
+    constructor() {}
+
+    public async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): Promise {
+        const codeLenses: vscode.CodeLens[] = [];
+        const text = document.getText();
+        const lines = text.split('\n');
+
+        // Find all function declarations
+        const functionPattern = /^\s*(fun|modifier)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\)/;
+        const functions: { name: string, type: string, line: number, params: string }[] = [];
+
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            const match = line.match(functionPattern);
+
+            if (match) {
+                const funcType = match[1]; // 'fun' or 'modifier'
+                const funcName = match[2];
+                const params = match[3].trim();
+
+                functions.push({ name: funcName, type: funcType, line: i, params });
+            }
+        }
+
+        // Add CodeLens for each function
+        for (const func of functions) {
+            const range = new vscode.Range(func.line, 0, func.line, lines[func.line].length);
+            const isTestOrSetup = this.isTestOrSetupFunction(lines, func.line);
+
+            if (!isTestOrSetup) {
+                // Count usages including imports (async)
+                const usageCount = await this.countUsagesAsync(document, func.name, func.type);
+
+                // Add "Run" button
+                const runCommand: vscode.Command = {
+                    title: `▶ Run`,
+                    command: 'isl.runFunction',
+                    arguments: [document.uri, func.name, func.params]
+                };
+                codeLenses.push(new vscode.CodeLens(range, runCommand));
+
+                // Add usage count
+                if (usageCount > 0) {
+                    const usageCommand: vscode.Command = {
+                        title: `📊 ${usageCount} ${usageCount === 1 ? 'usage' : 'usages'}`,
+                        command: 'isl.showUsages',
+                        arguments: [document.uri, func.name, func.type]
+                    };
+                    codeLenses.push(new vscode.CodeLens(range, usageCommand));
+                } else if (func.name !== 'run') {
+                    // Show "No usages" for functions other than 'run'
+                    const noUsageCommand: vscode.Command = {
+                        title: `⚠️ No usages found`,
+                        command: '',
+                        tooltip: 'This function is not being called anywhere'
+                    };
+                    codeLenses.push(new vscode.CodeLens(range, noUsageCommand));
+                }
+            }
+        }
+
+        return codeLenses;
+    }
+    
+    /**
+     * Returns true if the function at the given line is annotated with @setup or @test.
+     */
+    private isTestOrSetupFunction(lines: string[], functionLine: number): boolean {
+        for (let i = Math.max(0, functionLine - 5); i < functionLine; i++) {
+            const trimmed = lines[i].trim();
+            if (/^@setup\s*$/.test(trimmed)) return true;
+            if (/^@test\s*(?:\([^)]*\))?\s*$/.test(trimmed)) return true;
+        }
+        return false;
+    }
+
+    private countUsages(text: string, functionName: string, functionType: string): number {
+        let count = 0;
+        
+        if (functionType === 'fun') {
+            // Count @.This.functionName() calls
+            const functionCallPattern = new RegExp(`@\\.This\\.${functionName}\\s*\\(`, 'g');
+            const matches = text.match(functionCallPattern);
+            count += matches ? matches.length : 0;
+        } else if (functionType === 'modifier') {
+            // Count | functionName usages
+            const modifierPattern = new RegExp(`\\|\\s*${functionName}(?:\\s*\\(|\\s|$)`, 'g');
+            const matches = text.match(modifierPattern);
+            count += matches ? matches.length : 0;
+        }
+        
+        return count;
+    }
+
+    private async countUsagesAsync(document: vscode.TextDocument, functionName: string, functionType: string): Promise {
+        // Start with local usages
+        const text = document.getText();
+        let count = this.countUsages(text, functionName, functionType);
+        
+        // Find all files that import this file and get the module names they use
+        const importInfo = await this.findFilesAndModuleNamesThatImport(document.uri);
+        
+        // Count usages in imported files using the correct module name for each file
+        for (const { fileUri, moduleName } of importInfo) {
+            try {
+                const importedText = fs.readFileSync(fileUri.fsPath, 'utf-8');
+                
+                if (functionType === 'fun') {
+                    // Count @.ModuleName.functionName() calls (using the import name, not filename)
+                    const functionCallPattern = new RegExp(`@\\.${moduleName}\\.${functionName}\\s*\\(`, 'g');
+                    const matches = importedText.match(functionCallPattern);
+                    count += matches ? matches.length : 0;
+                } else if (functionType === 'modifier') {
+                    // Count | ModuleName.modifierName usages (using the import name, not filename)
+                    const modifierPattern = new RegExp(`\\|\\s*${moduleName}\\.${functionName}(?:\\s*\\(|\\s|$)`, 'g');
+                    const matches = importedText.match(modifierPattern);
+                    count += matches ? matches.length : 0;
+                }
+            } catch (error) {
+                // Silently skip files that can't be read
+                console.warn(`Could not read file ${fileUri.fsPath} for usage counting: ${error}`);
+            }
+        }
+        
+        return count;
+    }
+
+    /**
+     * Finds all files that import the given file and returns the module name each file uses
+     */
+    private async findFilesAndModuleNamesThatImport(uri: vscode.Uri): Promise> {
+        const results: Array<{ fileUri: vscode.Uri, moduleName: string }> = [];
+        const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
+        
+        if (!workspaceFolder) {
+            return results;
+        }
+        
+        // Search for all .isl files in the workspace
+        const pattern = new vscode.RelativePattern(workspaceFolder, '**/*.isl');
+        const allIslFiles = await vscode.workspace.findFiles(pattern, null, 1000);
+        
+        // Check each file for imports that point to the current file
+        for (const fileUri of allIslFiles) {
+            // Skip the current file
+            if (fileUri.fsPath === uri.fsPath) {
+                continue;
+            }
+            
+            try {
+                const fileText = fs.readFileSync(fileUri.fsPath, 'utf-8');
+                const lines = fileText.split('\n');
+                
+                // Check each line for import statements
+                for (const line of lines) {
+                    // Pattern: import ModuleName from 'file.isl' or import ModuleName from "file.isl"
+                    const importMatch = line.match(/import\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+from\s+['"]([^'"]+)['"]/);
+                    if (importMatch) {
+                        const moduleName = importMatch[1];
+                        const importPath = importMatch[2];
+                        
+                        // Resolve the import path relative to the importing file
+                        const importingDir = path.dirname(fileUri.fsPath);
+                        let resolvedPath: string;
+                        
+                        if (path.isAbsolute(importPath)) {
+                            resolvedPath = importPath;
+                        } else {
+                            resolvedPath = path.resolve(importingDir, importPath);
+                        }
+                        
+                        // Try with .isl extension if not present
+                        if (!resolvedPath.endsWith('.isl')) {
+                            const withExtension = resolvedPath + '.isl';
+                            if (fs.existsSync(withExtension)) {
+                                resolvedPath = withExtension;
+                            }
+                        }
+                        
+                        // Check if the resolved path matches the current file
+                        if (fs.existsSync(resolvedPath) && path.resolve(resolvedPath) === path.resolve(uri.fsPath)) {
+                            results.push({ fileUri, moduleName });
+                            break; // Found the import, no need to check more lines in this file
+                        }
+                    }
+                }
+            } catch (error) {
+                // Silently skip files that can't be read
+                console.warn(`Could not check file ${fileUri.fsPath} for imports: ${error}`);
+            }
+        }
+        
+        return results;
+    }
+
+
+    public resolveCodeLens(codeLens: vscode.CodeLens, token: vscode.CancellationToken): vscode.CodeLens | Thenable {
+        return codeLens;
+    }
+
+    public refresh(): void {
+        this._onDidChangeCodeLenses.fire();
+    }
+}
+
+export async function runIslFunction(uri: vscode.Uri, functionName: string, params: string, context: vscode.ExtensionContext) {
+    const document = await vscode.workspace.openTextDocument(uri);
+    const filePath = uri.fsPath;
+
+    // Parse parameters to create input JSON
+    const paramList = params
+        .split(',')
+        .map(p => p.trim())
+        .filter(p => p.length > 0)
+        .map(p => {
+            // Extract parameter name (remove type annotations if present)
+            const paramName = p.split(':')[0].trim().replace('$', '');
+            return paramName;
+        });
+
+    // Create input JSON based on parameters
+    let inputJson = '{}';
+    if (paramList.length > 0) {
+        // Prompt user for input values
+        const userInput = await vscode.window.showInputBox({
+            prompt: `Enter input JSON for ${functionName}(${params})`,
+            placeHolder: createSampleInput(paramList),
+            value: createSampleInput(paramList),
+            ignoreFocusOut: true
+        });
+
+        if (userInput === undefined) {
+            return; // User cancelled
+        }
+
+        inputJson = userInput;
+    }
+
+    // Save document if dirty
+    if (document.isDirty) {
+        await document.save();
+    }
+
+    // Find Java
+    const javaPath = await findJava();
+    if (!javaPath) {
+        vscode.window.showErrorMessage(
+            'Java not found. Please install Java 11+ or configure isl.execution.javaHome',
+            'Open Settings'
+        ).then(selection => {
+            if (selection === 'Open Settings') {
+                vscode.commands.executeCommand('workbench.action.openSettings', 'isl.execution.javaHome');
+            }
+        });
+        return;
+    }
+
+    // Get embedded JAR path
+    const jarPath = path.join(context.extensionPath, 'lib', 'isl-cmd-all.jar');
+    
+    if (!fs.existsSync(jarPath)) {
+        vscode.window.showErrorMessage(
+            'ISL runtime not found in extension. The extension may be corrupted. Please reinstall.'
+        );
+        return;
+    }
+
+    // Create terminal
+    const isWindows = process.platform === 'win32';
+    const shellPath = isWindows ? 'powershell.exe' : undefined;
+    
+    const terminal = vscode.window.createTerminal({
+        name: `ISL: ${functionName}`,
+        cwd: path.dirname(filePath),
+        shellPath: shellPath
+    });
+
+    terminal.show();
+
+    // Write input to temporary file
+    const tempDir = path.join(path.dirname(filePath), '.isl-temp');
+    if (!fs.existsSync(tempDir)) {
+        fs.mkdirSync(tempDir, { recursive: true });
+    }
+
+    const tempInputFile = path.join(tempDir, `input-${functionName}.json`);
+    fs.writeFileSync(tempInputFile, inputJson);
+
+    // Build command
+    let command: string;
+
+    if (isWindows) {
+        // PowerShell command
+        const javaCmd = escapeForPowerShell(javaPath);
+        const jarFile = escapeForPowerShell(jarPath);
+        const islFile = escapeForPowerShell(filePath);
+        const inputFile = escapeForPowerShell(tempInputFile);
+        
+        command = `& "${javaCmd}" -jar "${jarFile}" transform "${islFile}" -i "${inputFile}" --function ${functionName} --pretty`;
+    } else {
+        // Unix/Mac command
+        const javaCmd = escapeForBash(javaPath);
+        const jarFile = escapeForBash(jarPath);
+        const islFile = escapeForBash(filePath);
+        const inputFile = escapeForBash(tempInputFile);
+        
+        command = `"${javaCmd}" -jar "${jarFile}" transform "${islFile}" -i "${inputFile}" --function ${functionName} --pretty`;
+    }
+
+    terminal.sendText(command);
+
+    // Show notification
+    vscode.window.showInformationMessage(`Running ${functionName}...`);
+
+    // Clean up temp file after a delay
+    setTimeout(() => {
+        try {
+            if (fs.existsSync(tempInputFile)) {
+                fs.unlinkSync(tempInputFile);
+            }
+        } catch (e) {
+            // Ignore cleanup errors
+        }
+    }, 10000);
+}
+
+async function findJava(): Promise {
+    // Check configured JAVA_HOME
+    const config = vscode.workspace.getConfiguration('isl.execution');
+    const configuredJavaHome = config.get('javaHome');
+    
+    if (configuredJavaHome) {
+        const javaPath = path.join(configuredJavaHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
+        if (fs.existsSync(javaPath)) {
+            return javaPath;
+        }
+    }
+
+    // Check JAVA_HOME environment variable
+    const javaHome = process.env.JAVA_HOME;
+    if (javaHome) {
+        const javaPath = path.join(javaHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
+        if (fs.existsSync(javaPath)) {
+            return javaPath;
+        }
+    }
+
+    // Try java in PATH
+    return process.platform === 'win32' ? 'java.exe' : 'java';
+}
+
+function escapeForPowerShell(filepath: string): string {
+    // For PowerShell, escape single quotes
+    return filepath.replace(/'/g, "''");
+}
+
+function escapeForBash(filepath: string): string {
+    // For bash, escape special characters
+    return filepath.replace(/(["\s'$`\\])/g, '\\$1');
+}
+
+function createSampleInput(paramNames: string[]): string {
+    if (paramNames.length === 0) {
+        return '{}';
+    }
+
+    const inputObj: any = {};
+    for (const param of paramNames) {
+        // Create sample values based on parameter names
+        const paramLower = param.toLowerCase();
+        if (paramLower.includes('id')) {
+            inputObj[param] = 123;
+        } else if (paramLower.includes('name')) {
+            inputObj[param] = 'Sample Name';
+        } else if (paramLower.includes('email')) {
+            inputObj[param] = 'user@example.com';
+        } else if (paramLower.includes('price') || paramLower.includes('amount')) {
+            inputObj[param] = 99.99;
+        } else if (paramLower.includes('count') || paramLower.includes('quantity')) {
+            inputObj[param] = 1;
+        } else if (paramLower.includes('active') || paramLower.includes('enabled')) {
+            inputObj[param] = true;
+        } else if (paramLower.includes('date')) {
+            inputObj[param] = '2024-01-15';
+        } else if (paramLower.includes('items') || paramLower.includes('list') || paramLower.includes('array')) {
+            inputObj[param] = [];
+        } else if (paramLower.includes('input') || paramLower.includes('data')) {
+            inputObj[param] = { value: 'example' };
+        } else {
+            inputObj[param] = 'value';
+        }
+    }
+
+    return JSON.stringify(inputObj, null, 2);
+}
+
+export async function showUsages(uri: vscode.Uri, functionName: string, functionType: string) {
+    const document = await vscode.workspace.openTextDocument(uri);
+    const text = document.getText();
+    const locations: vscode.Location[] = [];
+    
+    // Find usages in current file
+    const lines = text.split('\n');
+    
+    for (let i = 0; i < lines.length; i++) {
+        const line = lines[i];
+        let match;
+        
+        if (functionType === 'fun') {
+            const pattern = new RegExp(`@\\.This\\.${functionName}\\s*\\(`, 'g');
+            while ((match = pattern.exec(line)) !== null) {
+                const startPos = new vscode.Position(i, match.index);
+                const endPos = new vscode.Position(i, match.index + match[0].length);
+                locations.push(new vscode.Location(uri, new vscode.Range(startPos, endPos)));
+            }
+        } else if (functionType === 'modifier') {
+            const pattern = new RegExp(`\\|\\s*${functionName}(?:\\s*\\(|\\s|$)`, 'g');
+            while ((match = pattern.exec(line)) !== null) {
+                const startPos = new vscode.Position(i, match.index);
+                const endPos = new vscode.Position(i, match.index + match[0].length);
+                locations.push(new vscode.Location(uri, new vscode.Range(startPos, endPos)));
+            }
+        }
+    }
+    
+    // Find all files that import this file and get the module names they use
+    const importInfo = await findFilesAndModuleNamesThatImport(uri);
+    
+    for (const { fileUri, moduleName } of importInfo) {
+        try {
+            const importedText = fs.readFileSync(fileUri.fsPath, 'utf-8');
+            const importedLines = importedText.split('\n');
+            
+            for (let i = 0; i < importedLines.length; i++) {
+                const line = importedLines[i];
+                let match;
+                
+                if (functionType === 'fun') {
+                    // Look for @.ModuleName.functionName() calls (using the import name)
+                    const pattern = new RegExp(`@\\.${moduleName}\\.${functionName}\\s*\\(`, 'g');
+                    while ((match = pattern.exec(line)) !== null) {
+                        const startPos = new vscode.Position(i, match.index);
+                        const endPos = new vscode.Position(i, match.index + match[0].length);
+                        locations.push(new vscode.Location(fileUri, new vscode.Range(startPos, endPos)));
+                    }
+                } else if (functionType === 'modifier') {
+                    // Look for | ModuleName.modifierName usages (using the import name)
+                    const pattern = new RegExp(`\\|\\s*${moduleName}\\.${functionName}(?:\\s*\\(|\\s|$)`, 'g');
+                    while ((match = pattern.exec(line)) !== null) {
+                        const startPos = new vscode.Position(i, match.index);
+                        const endPos = new vscode.Position(i, match.index + match[0].length);
+                        locations.push(new vscode.Location(fileUri, new vscode.Range(startPos, endPos)));
+                    }
+                }
+            }
+        } catch (error) {
+            // Silently skip files that can't be read
+            console.warn(`Could not read file ${fileUri.fsPath} for usage display: ${error}`);
+        }
+    }
+    
+    if (locations.length > 0) {
+        vscode.commands.executeCommand('editor.action.showReferences', uri, locations[0].range.start, locations);
+    } else {
+        vscode.window.showInformationMessage(`No usages found for ${functionName}`);
+    }
+}
+
+async function findFilesAndModuleNamesThatImport(uri: vscode.Uri): Promise> {
+    const results: Array<{ fileUri: vscode.Uri, moduleName: string }> = [];
+    const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
+    
+    if (!workspaceFolder) {
+        return results;
+    }
+    
+    // Search for all .isl files in the workspace
+    const pattern = new vscode.RelativePattern(workspaceFolder, '**/*.isl');
+    const allIslFiles = await vscode.workspace.findFiles(pattern, null, 1000);
+    
+    // Check each file for imports that point to the current file
+    for (const fileUri of allIslFiles) {
+        // Skip the current file
+        if (fileUri.fsPath === uri.fsPath) {
+            continue;
+        }
+        
+        try {
+            const fileText = fs.readFileSync(fileUri.fsPath, 'utf-8');
+            const lines = fileText.split('\n');
+            
+            // Check each line for import statements
+            for (const line of lines) {
+                // Pattern: import ModuleName from 'file.isl' or import ModuleName from "file.isl"
+                const importMatch = line.match(/import\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+from\s+['"]([^'"]+)['"]/);
+                if (importMatch) {
+                    const moduleName = importMatch[1];
+                    const importPath = importMatch[2];
+                    
+                    // Resolve the import path relative to the importing file
+                    const importingDir = path.dirname(fileUri.fsPath);
+                    let resolvedPath: string;
+                    
+                    if (path.isAbsolute(importPath)) {
+                        resolvedPath = importPath;
+                    } else {
+                        resolvedPath = path.resolve(importingDir, importPath);
+                    }
+                    
+                    // Try with .isl extension if not present
+                    if (!resolvedPath.endsWith('.isl')) {
+                        const withExtension = resolvedPath + '.isl';
+                        if (fs.existsSync(withExtension)) {
+                            resolvedPath = withExtension;
+                        }
+                    }
+                    
+                    // Check if the resolved path matches the current file
+                    if (fs.existsSync(resolvedPath) && path.resolve(resolvedPath) === path.resolve(uri.fsPath)) {
+                        results.push({ fileUri, moduleName });
+                        break; // Found the import, no need to check more lines in this file
+                    }
+                }
+            }
+        } catch (error) {
+            // Silently skip files that can't be read
+            console.warn(`Could not check file ${fileUri.fsPath} for imports: ${error}`);
+        }
+    }
+    
+    return results;
+}
+
+
+export async function testFunction(uri: vscode.Uri, functionName: string, params: string, context: vscode.ExtensionContext) {
+    await runIslFunction(uri, functionName, params, context);
+}
diff --git a/plugin/src/completion.ts b/plugin/src/completion.ts
new file mode 100644
index 0000000..b368104
--- /dev/null
+++ b/plugin/src/completion.ts
@@ -0,0 +1,783 @@
+import * as vscode from 'vscode';
+import { IslExtensionsManager, IslFunctionDefinition, IslModifierDefinition } from './extensions';
+import { getModifiersMap, getFunctionsByNamespace, getServicesMap, getAnnotations, type BuiltInModifier, type BuiltInFunction } from './language';
+import { IslTypeManager } from './types';
+import type { SchemaInfo } from './types';
+
+export class IslCompletionProvider implements vscode.CompletionItemProvider {
+    
+    constructor(
+        private extensionsManager: IslExtensionsManager,
+        private typeManager?: IslTypeManager,
+        private outputChannel?: vscode.OutputChannel
+    ) {}
+    
+    async provideCompletionItems(
+        document: vscode.TextDocument,
+        position: vscode.Position,
+        token: vscode.CancellationToken,
+        context: vscode.CompletionContext
+    ): Promise {
+        const linePrefix = document.lineAt(position).text.substr(0, position.character);
+        
+        // Type-based object literal completions (root and nested, e.g. billingAddress inside order)
+        if (this.typeManager) {
+            const schemaAt = await this.typeManager.getSchemaForObjectAt(document, position);
+            if (schemaAt) {
+                const { typeName, propertyPath, schema } = schemaAt;
+                const pathStr = propertyPath.length > 0 ? `.${propertyPath.join('.')}` : '';
+                const msg = `[ISL Completion] Schema at ${typeName}${pathStr}: ${Object.keys(schema.properties).length} properties`;
+                this.outputChannel?.appendLine(msg);
+                return this.getTypeBasedCompletions(document, position, schema, linePrefix);
+            }
+        }
+        
+        // Load custom extensions
+        const extensions = await this.extensionsManager.getExtensionsForDocument(document);
+        
+        // Check for pagination cursor property access: $cursor.
+        const paginationPropertyMatch = linePrefix.match(/\$([a-zA-Z_][a-zA-Z0-9_]*)\.(\w*)$/);
+        if (paginationPropertyMatch) {
+            const varName = paginationPropertyMatch[1];
+            const paginationType = this.getPaginationType(document, varName);
+            if (paginationType) {
+                return this.getPaginationPropertyCompletions(paginationType);
+            }
+        }
+        
+        // Check for @.This. - only same-file functions and modifiers (not global extensions)
+        if (linePrefix.match(/@\.This\.[\w]*$/)) {
+            return this.getFunctionsFromDocument(document);
+        }
+        // Check for @.ServiceName. - show methods for that service (built-in) or single call for global extension
+        const serviceMethodMatch = linePrefix.match(/@\.([A-Za-z_][a-zA-Z0-9_]*)\.(\w*)$/);
+        if (serviceMethodMatch) {
+            const serviceName = serviceMethodMatch[1];
+            const methodPrefix = serviceMethodMatch[2] || '';
+            return this.getServiceMethodCompletions(serviceName, methodPrefix, extensions);
+        }
+        // @. -> built-in services (Date, Math, This, ...) + global extension function names (called as @.name())
+        else if (linePrefix.endsWith('@.')) {
+            return this.getServiceCompletions(extensions);
+        }
+        // @ at line start -> annotations (@test, @setup)
+        else if (linePrefix.match(/^\s*@(\w*)$/)) {
+            return this.getAnnotationCompletions(linePrefix);
+        } else if (linePrefix.match(/\|\s*[\w.]*$/)) {
+            return this.getModifierCompletions(document, extensions);
+        } else if (linePrefix.match(/\$\w*$/)) {
+            return this.getVariableCompletions(document);
+        } else {
+            return this.getKeywordCompletions();
+        }
+    }
+
+    /** Same-file only: fun/modifier in this document. Used for @.This. (not global extensions). */
+    private getFunctionsFromDocument(document: vscode.TextDocument): vscode.CompletionItem[] {
+        const functions: vscode.CompletionItem[] = [];
+        const text = document.getText();
+        const lines = text.split('\n');
+        
+        const functionPattern = /^\s*(fun|modifier)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\)/;
+        
+        for (let i = 0; i < lines.length; i++) {
+            const match = lines[i].match(functionPattern);
+            if (match) {
+                const funcType = match[1];
+                const funcName = match[2];
+                const params = match[3].trim();
+                
+                const item = new vscode.CompletionItem(funcName, vscode.CompletionItemKind.Function);
+                item.detail = `${funcType} ${funcName}(${params})`;
+                
+                const paramNames = params
+                    .split(',')
+                    .map(p => p.trim())
+                    .filter(p => p.length > 0)
+                    .map((p, idx) => {
+                        const paramName = p.split(':')[0].trim();
+                        return `\${${idx + 1}:${paramName}}`;
+                    });
+                
+                if (paramNames.length > 0) {
+                    item.insertText = new vscode.SnippetString(`${funcName}(${paramNames.join(', ')})`);
+                } else {
+                    item.insertText = new vscode.SnippetString(`${funcName}()`);
+                }
+                
+                const docs = this.getDocumentationForFunction(lines, i);
+                if (docs) {
+                    item.documentation = new vscode.MarkdownString(docs);
+                }
+                
+                functions.push(item);
+            }
+        }
+        
+        return functions;
+    }
+
+    /** Modifiers defined in this document (for | name completion). */
+    private getSameFileModifiers(document: vscode.TextDocument): vscode.CompletionItem[] {
+        const items: vscode.CompletionItem[] = [];
+        const lines = document.getText().split('\n');
+        const modifierPattern = /^\s*modifier\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\)/;
+        for (let i = 0; i < lines.length; i++) {
+            const match = lines[i].match(modifierPattern);
+            if (match) {
+                const name = match[1];
+                const params = match[2].trim();
+                const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Method);
+                item.detail = `modifier ${name}(${params}) (same file)`;
+                const paramNames = params.split(',').map(p => p.trim().split(':')[0].trim()).filter(Boolean);
+                const paramSnippets = paramNames.map((p, idx) => `\${${idx + 1}:${p}}`);
+                item.insertText = paramSnippets.length > 0
+                    ? new vscode.SnippetString(`${name}(${paramSnippets.join(', ')})`)
+                    : new vscode.SnippetString(name);
+                items.push(item);
+            }
+        }
+        return items;
+    }
+
+    private formatFunctionSignature(func: IslFunctionDefinition): string {
+        const params = func.parameters.map(p => {
+            let result = `${p.name}`;
+            if (p.type) {
+                result += `: ${p.type}`;
+            }
+            if (p.optional) {
+                result += '?';
+            }
+            return result;
+        }).join(', ');
+        
+        return `function ${func.name}(${params})${func.returns?.type ? ': ' + func.returns.type : ''} (custom)`;
+    }
+
+    private formatFunctionDocumentation(func: IslFunctionDefinition): vscode.MarkdownString {
+        const md = new vscode.MarkdownString();
+        md.isTrusted = true;
+        
+        if (func.description) {
+            md.appendMarkdown(`${func.description}\n\n`);
+        }
+        
+        if (func.parameters.length > 0) {
+            md.appendMarkdown('**Parameters:**\n');
+            for (const param of func.parameters) {
+                const optional = param.optional ? ' (optional)' : '';
+                const type = param.type ? `: ${param.type}` : '';
+                const desc = param.description ? ` - ${param.description}` : '';
+                md.appendMarkdown(`- \`${param.name}${type}\`${optional}${desc}\n`);
+            }
+            md.appendMarkdown('\n');
+        }
+        
+        if (func.returns) {
+            md.appendMarkdown('**Returns:**');
+            if (func.returns.type) {
+                md.appendMarkdown(` \`${func.returns.type}\``);
+            }
+            if (func.returns.description) {
+                md.appendMarkdown(` - ${func.returns.description}`);
+            }
+            md.appendMarkdown('\n\n');
+        }
+        
+        if (func.examples && func.examples.length > 0) {
+            md.appendMarkdown('**Examples:**\n');
+            for (const example of func.examples) {
+                md.appendMarkdown('```isl\n' + example + '\n```\n');
+            }
+        }
+        
+        md.appendMarkdown('\n*Custom function from .islextensions*');
+        
+        return md;
+    }
+
+    private getDocumentationForFunction(lines: string[], functionLineIndex: number): string | undefined {
+        // Look for comments immediately above the function
+        const docs: string[] = [];
+        for (let i = functionLineIndex - 1; i >= 0; i--) {
+            const line = lines[i].trim();
+            if (line.startsWith('//')) {
+                docs.unshift(line.substring(2).trim());
+            } else if (line.startsWith('#')) {
+                docs.unshift(line.substring(1).trim());
+            } else if (line === '') {
+                // Allow blank lines
+                continue;
+            } else {
+                // Stop at first non-comment, non-blank line
+                break;
+            }
+        }
+        return docs.length > 0 ? docs.join('\n') : undefined;
+    }
+
+    /** @ at line start -> @test, @setup annotations */
+    private getAnnotationCompletions(linePrefix: string): vscode.CompletionItem[] {
+        const match = linePrefix.match(/^\s*@(\w*)$/);
+        const prefix = match ? match[1].toLowerCase() : '';
+        const annotations = getAnnotations();
+        return annotations
+            .filter(a => a.name.toLowerCase().startsWith(prefix))
+            .map(a => {
+                const item = new vscode.CompletionItem(`@${a.name}`, vscode.CompletionItemKind.Property);
+                item.detail = a.detail;
+                item.documentation = a.documentation ? new vscode.MarkdownString(a.documentation) : undefined;
+                item.insertText = a.insertText ? `@${a.insertText}` : `@${a.name}`;
+                return item;
+            });
+    }
+
+    private getKeywordCompletions(): vscode.CompletionItem[] {
+        const keywords = [
+            { label: 'fun', kind: vscode.CompletionItemKind.Keyword, detail: 'Function declaration', insertText: 'fun ${1:name}(${2:\\$param}) {\n\t${3:// body}\n\treturn ${4:value}\n}' },
+            { label: 'modifier', kind: vscode.CompletionItemKind.Keyword, detail: 'Modifier function', insertText: 'modifier ${1:name}(${2:\\$value}) {\n\treturn ${3:\\$value}\n}' },
+            { label: 'if', kind: vscode.CompletionItemKind.Keyword, detail: 'If statement', insertText: 'if (${1:condition})\n\t${2:// true block}\nendif' },
+            { label: 'ifelse', kind: vscode.CompletionItemKind.Keyword, detail: 'If-else statement', insertText: 'if (${1:condition})\n\t${2:// true block}\nelse\n\t${3:// false block}\nendif' },
+            { label: 'foreach', kind: vscode.CompletionItemKind.Keyword, detail: 'ForEach loop', insertText: 'foreach ${1:\\$item} in ${2:\\$array}\n\t${3:// loop body}\nendfor' },
+            { label: 'while', kind: vscode.CompletionItemKind.Keyword, detail: 'While loop', insertText: 'while (${1:condition})\n\t${2:// loop body}\nendwhile' },
+            { label: 'switch', kind: vscode.CompletionItemKind.Keyword, detail: 'Switch statement', insertText: 'switch (${1:\\$var})\n\t${2:value} -> ${3:result};\n\telse -> ${4:default};\nendswitch' },
+            { label: 'return', kind: vscode.CompletionItemKind.Keyword, detail: 'Return statement', insertText: 'return ${1:value}' },
+            { label: 'import', kind: vscode.CompletionItemKind.Keyword, detail: 'Import module', insertText: 'import ${1:Module} from \'${2:file.isl}\';' },
+            { label: 'type', kind: vscode.CompletionItemKind.Keyword, detail: 'Type declaration (inline)', insertText: 'type ${1:TypeName} as {\n\t${2:prop}: ${3:String}\n};' },
+            { label: 'type from', kind: vscode.CompletionItemKind.Keyword, detail: 'Type from schema URL', insertText: 'type ${1:ns}:${2:TypeName} from \'${3:https://...}\';' },
+            { label: 'parallel', kind: vscode.CompletionItemKind.Keyword, detail: 'Parallel execution', insertText: 'parallel ' },
+            { label: 'cache', kind: vscode.CompletionItemKind.Keyword, detail: 'Cache function result', insertText: 'cache ' },
+        ];
+
+        return keywords.map(k => {
+            const item = new vscode.CompletionItem(k.label, k.kind);
+            item.detail = k.detail;
+            if (k.insertText) {
+                item.insertText = new vscode.SnippetString(k.insertText);
+            }
+            return item;
+        });
+    }
+
+    /** @. -> built-in services (Date, Math, This, ...) + global extension function names (called as @.name()) */
+    private getServiceCompletions(extensions: import('./extensions').IslExtensions): vscode.CompletionItem[] {
+        const servicesMap = getServicesMap();
+        const items: vscode.CompletionItem[] = [];
+        items.push(new vscode.CompletionItem('This', vscode.CompletionItemKind.Class));
+        const thisInfo = servicesMap.get('This');
+        if (thisInfo) {
+            items[0].detail = thisInfo.detail;
+            items[0].documentation = thisInfo.documentation ? new vscode.MarkdownString(thisInfo.documentation) : undefined;
+        }
+        items[0].insertText = 'This';
+
+        for (const [name, info] of servicesMap) {
+            if (name === 'This') continue;
+            const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Class);
+            item.detail = info.detail;
+            item.documentation = info.documentation ? new vscode.MarkdownString(info.documentation) : undefined;
+            item.insertText = name;
+            items.push(item);
+        }
+
+        // Global extension functions: called directly as @.functionName(), same as built-ins
+        for (const [name, funcDef] of extensions.functions) {
+            const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Function);
+            item.detail = this.formatFunctionSignature(funcDef);
+            item.documentation = this.formatFunctionDocumentation(funcDef);
+            const paramSnippets = funcDef.parameters.map((param, idx) => `\${${idx + 1}:${param.name}}`);
+            item.insertText = paramSnippets.length > 0
+                ? new vscode.SnippetString(`${name}(${paramSnippets.join(', ')})`)
+                : new vscode.SnippetString(`${name}()`);
+            items.push(item);
+        }
+        return items;
+    }
+
+    private getServiceMethodCompletions(serviceName: string, methodPrefix: string = '', extensions?: import('./extensions').IslExtensions): vscode.CompletionItem[] {
+        const byNamespace = getFunctionsByNamespace();
+        let methods = byNamespace.get(serviceName);
+        // If no built-in namespace and this is a global extension function name, no "methods" (call is @.name())
+        if (!methods && extensions?.functions.has(serviceName)) {
+            return [];
+        }
+        if (!methods) {
+            return [];
+        }
+
+        const filteredMethods = methodPrefix
+            ? methods.filter(m => m.name.toLowerCase().startsWith(methodPrefix.toLowerCase()))
+            : methods;
+
+        return filteredMethods.map((m: BuiltInFunction) => {
+            const item = new vscode.CompletionItem(m.name, vscode.CompletionItemKind.Method);
+            item.detail = this.formatBuiltInFunctionDetail(m);
+            let docText = m.documentation ?? '';
+            if (m.returns) {
+                if (docText) docText += '\n\n';
+                docText += '**Returns:**';
+                if (m.returns.type) docText += ` \`${m.returns.type}\``;
+                if (m.returns.description) docText += ` - ${m.returns.description}`;
+            }
+            if (docText) {
+                item.documentation = new vscode.MarkdownString(docText);
+            }
+
+            const params = m.params || '()';
+            const paramMatch = params.match(/\(([^)]*)\)/);
+            if (paramMatch && paramMatch[1]) {
+                const paramList = paramMatch[1];
+                if (paramList === '...') {
+                    item.insertText = new vscode.SnippetString(`${m.name}(\${1:arg1}, \${2:arg2})`);
+                } else if (paramList.includes(',')) {
+                    const snippetParams = paramList.split(',').map((p, idx) => {
+                        const paramName = p.trim();
+                        return `\${${idx + 1}:${paramName}}`;
+                    });
+                    item.insertText = new vscode.SnippetString(`${m.name}(${snippetParams.join(', ')})`);
+                } else if (paramList) {
+                    item.insertText = new vscode.SnippetString(`${m.name}(\${1:${paramList.trim()}})`);
+                } else {
+                    item.insertText = new vscode.SnippetString(`${m.name}()`);
+                }
+            } else {
+                item.insertText = new vscode.SnippetString(`${m.name}()`);
+            }
+
+            return item;
+        });
+    }
+
+    /** | -> built-in modifiers + same-file modifiers + global extension modifiers */
+    private getModifierCompletions(document: vscode.TextDocument, extensions: import('./extensions').IslExtensions): vscode.CompletionItem[] {
+        const modifiersMap = getModifiersMap();
+        const completionItems: vscode.CompletionItem[] = [];
+
+        // Same-file modifiers first (shortcut: | name)
+        const sameFileModifiers = this.getSameFileModifiers(document);
+        for (const mod of sameFileModifiers) {
+            completionItems.push(mod);
+        }
+
+        for (const [_name, m] of modifiersMap) {
+            const item = new vscode.CompletionItem(m.name, vscode.CompletionItemKind.Method);
+            item.detail = this.formatBuiltInModifierDetail(m);
+            item.insertText = new vscode.SnippetString(m.insertText);
+            let docText = m.documentation ?? '';
+            if (m.returns) {
+                if (docText) docText += '\n\n';
+                docText += '**Returns:**';
+                if (m.returns.type) docText += ` \`${m.returns.type}\``;
+                if (m.returns.description) docText += ` - ${m.returns.description}`;
+            }
+            if (docText) {
+                item.documentation = new vscode.MarkdownString(docText);
+            }
+            completionItems.push(item);
+        }
+
+        // Add custom modifiers from extensions
+        for (const [name, modDef] of extensions.modifiers) {
+            const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Method);
+            item.detail = this.formatModifierSignature(modDef);
+            
+            // Create snippet with parameters
+            const paramSnippets = modDef.parameters.map((param, idx) => {
+                if (param.defaultValue) {
+                    return `\${${idx + 1}:${param.defaultValue}}`;
+                }
+                return `\${${idx + 1}:${param.name}}`;
+            });
+            
+            if (paramSnippets.length > 0) {
+                item.insertText = new vscode.SnippetString(`${name}(${paramSnippets.join(', ')})`);
+            } else {
+                item.insertText = new vscode.SnippetString(name);
+            }
+            
+            // Add documentation
+            item.documentation = this.formatModifierDocumentation(modDef);
+            
+            completionItems.push(item);
+        }
+
+        return completionItems;
+    }
+
+    private formatBuiltInModifierDetail(m: BuiltInModifier): string {
+        const base = m.detail ?? m.name;
+        return m.returns?.type ? `${base} → ${m.returns.type}` : base;
+    }
+
+    private formatBuiltInFunctionDetail(m: BuiltInFunction): string {
+        const base = m.detail ?? m.name;
+        return m.returns?.type ? `${base} → ${m.returns.type}` : base;
+    }
+
+    private formatModifierSignature(mod: IslModifierDefinition): string {
+        if (mod.parameters.length === 0) {
+            return `modifier ${mod.name} (custom)`;
+        }
+        
+        const params = mod.parameters.map(p => {
+            let result = `${p.name}`;
+            if (p.type) {
+                result += `: ${p.type}`;
+            }
+            if (p.optional) {
+                result += '?';
+            }
+            return result;
+        }).join(', ');
+        
+        return `modifier ${mod.name}(${params})${mod.returns?.type ? ': ' + mod.returns.type : ''} (custom)`;
+    }
+
+    private formatModifierDocumentation(mod: IslModifierDefinition): vscode.MarkdownString {
+        const md = new vscode.MarkdownString();
+        md.isTrusted = true;
+        
+        if (mod.description) {
+            md.appendMarkdown(`${mod.description}\n\n`);
+        }
+        
+        if (mod.parameters.length > 0) {
+            md.appendMarkdown('**Parameters:**\n');
+            for (const param of mod.parameters) {
+                const optional = param.optional ? ' (optional)' : '';
+                const type = param.type ? `: ${param.type}` : '';
+                const desc = param.description ? ` - ${param.description}` : '';
+                const defaultVal = param.defaultValue ? ` (default: ${param.defaultValue})` : '';
+                md.appendMarkdown(`- \`${param.name}${type}\`${optional}${defaultVal}${desc}\n`);
+            }
+            md.appendMarkdown('\n');
+        }
+        
+        if (mod.returns) {
+            md.appendMarkdown('**Returns:**');
+            if (mod.returns.type) {
+                md.appendMarkdown(` \`${mod.returns.type}\``);
+            }
+            if (mod.returns.description) {
+                md.appendMarkdown(` - ${mod.returns.description}`);
+            }
+            md.appendMarkdown('\n\n');
+        }
+        
+        if (mod.examples && mod.examples.length > 0) {
+            md.appendMarkdown('**Examples:**\n');
+            for (const example of mod.examples) {
+                md.appendMarkdown('```isl\n' + example + '\n```\n');
+            }
+        }
+        
+        md.appendMarkdown('\n*Custom modifier from .islextensions*');
+        
+        return md;
+    }
+
+    private getVariableCompletions(document: vscode.TextDocument): vscode.CompletionItem[] {
+        const variables: Map = new Map();
+        const text = document.getText();
+        
+        // Find all variable declarations and usages
+        const varPattern = /\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
+        let match;
+        
+        while ((match = varPattern.exec(text)) !== null) {
+            const varName = match[1];
+            if (!variables.has(varName)) {
+                const item = new vscode.CompletionItem('$' + varName, vscode.CompletionItemKind.Variable);
+                item.detail = 'Variable';
+                item.insertText = '$' + varName; // Include $ in the insert text
+                item.filterText = varName; // Filter without $ for better matching
+                variables.set(varName, item);
+            }
+        }
+
+        // Also add common input variable
+        if (!variables.has('input')) {
+            const inputItem = new vscode.CompletionItem('$input', vscode.CompletionItemKind.Variable);
+            inputItem.detail = 'Input parameter';
+            inputItem.insertText = '$input';
+            inputItem.filterText = 'input';
+            variables.set('input', inputItem);
+        }
+
+        return Array.from(variables.values());
+    }
+
+    /**
+     * Completions when cursor is inside a typed object literal: $var : TypeName = { ... }
+     * Offers "Fill all mandatory fields" snippet and property name completions from the schema.
+     */
+    private getTypeBasedCompletions(
+        document: vscode.TextDocument,
+        position: vscode.Position,
+        schema: SchemaInfo,
+        linePrefix: string
+    ): vscode.CompletionItem[] {
+        const items: vscode.CompletionItem[] = [];
+        const existingProps = this.getExistingPropertiesInObjectLiteral(document, position);
+        const indent = this.getIndentAtPosition(document, position);
+        const indentUnit = this.getIndentUnit(document, indent);
+        const requiredSet = new Set(schema.required);
+        const missingRequired = schema.required.filter(p => !existingProps.has(p));
+
+        // "Fill all mandatory fields" – insert only required properties not yet present
+        if (missingRequired.length > 0) {
+            const fillItem = new vscode.CompletionItem('Fill all mandatory fields', vscode.CompletionItemKind.Snippet);
+            fillItem.detail = `Insert required: ${missingRequired.join(', ')}`;
+            fillItem.documentation = new vscode.MarkdownString('Inserts all required properties from the schema with placeholders.');
+            const snippetParts: string[] = [];
+            let tabIndex = 1;
+            for (const prop of missingRequired) {
+                const propInfo = schema.properties[prop];
+                if (propInfo?.schema) {
+                    snippetParts.push(`${indentUnit}${prop}: {\n${indent}${indentUnit}\${${tabIndex++}:}\n${indent}}`);
+                } else if (propInfo?.enum?.length) {
+                    snippetParts.push(`${indentUnit}${prop}: "\${${tabIndex++}:${propInfo.enum[0]}}"`);
+                } else {
+                    const placeholder = this.schemaTypeToPlaceholder(propInfo?.type ?? 'any');
+                    snippetParts.push(`${indentUnit}${prop}: \${${tabIndex++}:${placeholder}}`);
+                }
+            }
+            fillItem.insertText = new vscode.SnippetString(snippetParts.join(`\n${indent}`));
+            items.push(fillItem);
+        }
+
+        // Value completion: after "propName: " show enum values for that property
+        const valueCompletion = this.getEnumValueCompletions(linePrefix, schema, indent);
+        if (valueCompletion.length > 0) {
+            return valueCompletion;
+        }
+
+        // Property name completions (exclude already declared in this block)
+        const propPrefix = this.getPropertyNamePrefix(linePrefix);
+        for (const [propName, propInfo] of Object.entries(schema.properties)) {
+            if (existingProps.has(propName)) continue;
+            if (propPrefix && !propName.toLowerCase().startsWith(propPrefix.toLowerCase())) continue;
+            const isRequired = requiredSet.has(propName);
+            if (propInfo.enum && propInfo.enum.length > 0) {
+                for (const enumVal of propInfo.enum) {
+                    const item = new vscode.CompletionItem(`${propName}: ${enumVal}`, vscode.CompletionItemKind.EnumMember);
+                    item.detail = `${propInfo.type ?? 'string'}${isRequired ? ' (required)' : ' (optional)'}`;
+                    if (propInfo.description) item.documentation = new vscode.MarkdownString(propInfo.description);
+                    item.insertText = new vscode.SnippetString(`${propName}: "${enumVal}"`);
+                    item.filterText = `${propName} ${enumVal}`;
+                    items.push(item);
+                }
+            } else {
+                const item = new vscode.CompletionItem(propName, vscode.CompletionItemKind.Property);
+                item.detail = `${propInfo.type ?? 'any'}${isRequired ? ' (required)' : ' (optional)'}`;
+                if (propInfo.description) item.documentation = new vscode.MarkdownString(propInfo.description);
+                if (propInfo.schema) {
+                    item.insertText = new vscode.SnippetString(`${propName}: {\n${indent}${indentUnit}$0\n${indent}}`);
+                } else {
+                    item.insertText = new vscode.SnippetString(`${propName}: \${1:${this.schemaTypeToPlaceholder(propInfo.type ?? 'any')}}`);
+                }
+                items.push(item);
+            }
+        }
+
+        return items;
+    }
+
+    private schemaTypeToPlaceholder(type: string): string {
+        const t = type.toLowerCase();
+        if (t === 'string' || t === 'text') return '""';
+        if (t === 'number' || t === 'integer') return '0';
+        if (t === 'boolean') return 'true';
+        if (t === 'date' || t === 'datetime') return '""';
+        return '""';
+    }
+
+    private getIndentAtPosition(document: vscode.TextDocument, position: vscode.Position): string {
+        const line = document.lineAt(position).text;
+        const match = line.match(/^\s*/);
+        return match ? match[0] : '';
+    }
+
+    /** One indent level: tab if file uses tabs, else spaces per editor.tabSize. */
+    private getIndentUnit(document: vscode.TextDocument, baseIndent: string): string {
+        const config = vscode.workspace.getConfiguration('editor', document.uri);
+        const insertSpaces = config.get('insertSpaces', true);
+        const tabSize = config.get('tabSize', 4);
+        if (!insertSpaces || /\t/.test(baseIndent)) return '\t';
+        return ' '.repeat(tabSize);
+    }
+
+    /** Returns the partial property name being typed (e.g. "acc" from "  acc|") */
+    private getPropertyNamePrefix(linePrefix: string): string {
+        const m = linePrefix.match(/(?:^|[\s{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)$/);
+        return m ? m[1] : '';
+    }
+
+    /** When cursor is after "propName: " or "propName: partial", return enum value completions for that property. */
+    private getEnumValueCompletions(linePrefix: string, schema: SchemaInfo, indent: string): vscode.CompletionItem[] {
+        const match = linePrefix.match(/([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*([^\s]*)$/);
+        if (!match) return [];
+        const propName = match[1];
+        const valuePrefix = match[2].replace(/^["']|["']$/g, '');
+        const propInfo = schema.properties[propName];
+        if (!propInfo?.enum?.length) return [];
+        const items: vscode.CompletionItem[] = [];
+        for (const enumVal of propInfo.enum) {
+            if (valuePrefix && !enumVal.toLowerCase().startsWith(valuePrefix.toLowerCase())) continue;
+            const item = new vscode.CompletionItem(enumVal, vscode.CompletionItemKind.EnumMember);
+            item.detail = propInfo.type ?? 'string';
+            if (propInfo.description) item.documentation = new vscode.MarkdownString(propInfo.description);
+            item.insertText = `"${enumVal}"`;
+            items.push(item);
+        }
+        return items;
+    }
+
+    /** Collects property names already present in the object literal containing position */
+    private getExistingPropertiesInObjectLiteral(document: vscode.TextDocument, position: vscode.Position): Set {
+        const text = document.getText();
+        const offset = document.offsetAt(position);
+        const containingPairs: { start: number; end: number }[] = [];
+        const openBraces: number[] = [];
+        for (let i = 0; i < text.length; i++) {
+            const ch = text[i];
+            if (ch === '{') openBraces.push(i);
+            else if (ch === '}') {
+                if (openBraces.length > 0) {
+                    const start = openBraces.pop()!;
+                    if (offset >= start && offset <= i) {
+                        containingPairs.push({ start, end: i });
+                    }
+                }
+            }
+        }
+        if (containingPairs.length === 0) return new Set();
+        let best = containingPairs[0];
+        for (const p of containingPairs) {
+            if (p.end - p.start < best.end - best.start) best = p;
+        }
+        const objectContent = text.substring(best.start + 1, best.end);
+        const names = new Set();
+        const propRe = /([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g;
+        let match;
+        while ((match = propRe.exec(objectContent)) !== null) {
+            names.add(match[1]);
+        }
+        return names;
+    }
+    
+    private getPaginationType(document: vscode.TextDocument, varName: string): string | null {
+        const text = document.getText();
+        const lines = text.split('\n');
+        
+        // Look for @.Pagination.[Type]( $varName, ... )
+        for (const line of lines) {
+            const cursorMatch = line.match(/@\.Pagination\.Cursor\s*\(\s*\$([a-zA-Z_][a-zA-Z0-9_]*)/);
+            if (cursorMatch && cursorMatch[1] === varName) {
+                return 'Cursor';
+            }
+            
+            const offsetMatch = line.match(/@\.Pagination\.Offset\s*\(\s*\$([a-zA-Z_][a-zA-Z0-9_]*)/);
+            if (offsetMatch && offsetMatch[1] === varName) {
+                return 'Offset';
+            }
+            
+            const pageMatch = line.match(/@\.Pagination\.Page\s*\(\s*\$([a-zA-Z_][a-zA-Z0-9_]*)/);
+            if (pageMatch && pageMatch[1] === varName) {
+                return 'Page';
+            }
+            
+            const keysetMatch = line.match(/@\.Pagination\.Keyset\s*\(\s*\$([a-zA-Z_][a-zA-Z0-9_]*)/);
+            if (keysetMatch && keysetMatch[1] === varName) {
+                return 'Keyset';
+            }
+            
+            const dateMatch = line.match(/@\.Pagination\.Date\s*\(\s*\$([a-zA-Z_][a-zA-Z0-9_]*)/);
+            if (dateMatch && dateMatch[1] === varName) {
+                return 'Date';
+            }
+        }
+        
+        return null;
+    }
+    
+    private getPaginationPropertyCompletions(paginationType: string): vscode.CompletionItem[] {
+        if (paginationType === 'Cursor') {
+            return [
+                this.createPaginationProperty(
+                    'current',
+                    'The value of the current cursor for the current page',
+                    'On the first loop this is null.'
+                ),
+                this.createPaginationProperty(
+                    'next',
+                    'The next value for the cursor on the next loop',
+                    'This needs to be set in the loop. If this value is the same as the previous value (e.g. value was not set) or the value is null, the pagination loop exits.'
+                )
+            ];
+        } else if (paginationType === 'Page') {
+            return [
+                this.createPaginationProperty(
+                    'startIndex',
+                    'The starting index as declared in the pagination',
+                    'Default is 0. This is the value passed in the pagination configuration.'
+                ),
+                this.createPaginationProperty(
+                    'pageSize',
+                    'Size of the page as declared in the pagination',
+                    'Default is 100. This is the value passed in the pagination configuration.'
+                ),
+                this.createPaginationProperty(
+                    'page',
+                    'Index of the current page',
+                    'Starting at startIndex. Increments with each iteration of the pagination loop.'
+                ),
+                this.createPaginationProperty(
+                    'fromOffset',
+                    'Start offset for the current page',
+                    'Calculated as: page * pageSize'
+                ),
+                this.createPaginationProperty(
+                    'toOffset',
+                    'End offset for the current page',
+                    'Calculated as: (page + 1) * pageSize'
+                ),
+                this.createPaginationProperty(
+                    'hasMorePages',
+                    'Whether there are more pages to fetch',
+                    'Set to false by default. In order to continue the pagination loop this needs to be set to true.'
+                )
+            ];
+        } else if (paginationType === 'Date') {
+            return [
+                this.createPaginationProperty(
+                    'startDate',
+                    'Start date of the current period',
+                    'The beginning of the current pagination period based on the duration.'
+                ),
+                this.createPaginationProperty(
+                    'endDate',
+                    'End date of the current period',
+                    'The end of the current pagination period based on the duration.'
+                ),
+                this.createPaginationProperty(
+                    'page',
+                    'Zero-based page index for the current page',
+                    'Increments with each iteration: 0, 1, 2, ...'
+                )
+            ];
+        }
+        
+        // Placeholder for other pagination types (Offset, Keyset)
+        return [];
+    }
+    
+    private createPaginationProperty(name: string, detail: string, docs: string): vscode.CompletionItem {
+        const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Property);
+        item.detail = detail;
+        item.documentation = new vscode.MarkdownString(docs);
+        item.insertText = name;
+        return item;
+    }
+}
diff --git a/plugin/src/controlFlowMatcher.ts b/plugin/src/controlFlowMatcher.ts
new file mode 100644
index 0000000..3afcc08
--- /dev/null
+++ b/plugin/src/controlFlowMatcher.ts
@@ -0,0 +1,483 @@
+import * as vscode from 'vscode';
+
+/**
+ * Control flow statement pairs in ISL
+ */
+export interface ControlFlowPair {
+    start: string;
+    end: string;
+    // Regex pattern to match the start keyword (must use word boundaries)
+    startPattern: RegExp;
+    // Regex pattern to match the end keyword (must use word boundaries)
+    endPattern: RegExp;
+}
+
+/**
+ * Configuration for control flow matching
+ */
+export interface ControlFlowConfig {
+    // Pattern to match block statements (at start of line)
+    blockStartPattern: RegExp;
+    // Pattern to match inline statements (after =, :, or ->)
+    inlineStartPattern: RegExp;
+}
+
+/**
+ * All control flow pairs in ISL
+ */
+export const CONTROL_FLOW_PAIRS: { [key: string]: ControlFlowPair } = {
+    if: {
+        start: 'if',
+        end: 'endif',
+        startPattern: /\bif[\s(]/,
+        endPattern: /\bendif\b/
+    },
+    foreach: {
+        start: 'foreach',
+        end: 'endfor',
+        startPattern: /\bforeach\s/,
+        endPattern: /\bendfor\b/
+    },
+    while: {
+        start: 'while',
+        end: 'endwhile',
+        startPattern: /\bwhile[\s(]/,
+        endPattern: /\bendwhile\b/
+    },
+    switch: {
+        start: 'switch',
+        end: 'endswitch',
+        startPattern: /\bswitch[\s(]/,
+        endPattern: /\bendswitch\b/
+    }
+};
+
+/**
+ * Control flow configurations for each type
+ */
+export const CONTROL_FLOW_CONFIGS: { [key: string]: ControlFlowConfig } = {
+    if: {
+        blockStartPattern: /^\s*if[\s(]/,
+        inlineStartPattern: /[=:>]\s*if[\s(]/  // Matches after =, :, or ->
+    },
+    foreach: {
+        blockStartPattern: /^\s*foreach\s/,
+        inlineStartPattern: /[=:>]\s*foreach\s/  // Matches after =, :, or ->
+    },
+    while: {
+        blockStartPattern: /^\s*while[\s(]/,
+        inlineStartPattern: /[=:>]\s*while[\s(]/  // Matches after =, :, or ->
+    },
+    switch: {
+        blockStartPattern: /^\s*switch[\s(]/,
+        inlineStartPattern: /[=:>]\s*switch[\s(]/  // Matches after =, :, or ->
+    }
+};
+
+/**
+ * Checks if a control flow statement appears after 'return' keyword
+ * This is a special case that should be treated like a block statement
+ */
+export function isAfterReturn(line: string, keywordIndex: number): boolean {
+    const beforeKeyword = line.substring(0, keywordIndex);
+    // Check if 'return' appears before the keyword (with whitespace between)
+    // Match: "return " or "    return " (with word boundary to avoid matching "returnValue")
+    return /\breturn\s+$/.test(beforeKeyword);
+}
+
+/**
+ * Represents a control flow statement in the stack
+ */
+export interface ControlFlowStackItem {
+    type: string;
+    line: number;
+    isBlock: boolean;
+    startColumn?: number;
+}
+
+/**
+ * Represents a matched keyword position
+ */
+export interface KeywordMatch {
+    keyword: string;
+    index: number;
+    line: number;
+    isStart: boolean;
+}
+
+/**
+ * Removes comments from a line
+ */
+export function stripComments(line: string): string {
+    const commentIndex = Math.min(
+        line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+        line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+    );
+    return commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+}
+
+/**
+ * Finds all keyword matches in a line for a given control flow pair
+ * This properly handles multi-line statements by checking the entire line
+ */
+export function findKeywordMatchesInLine(
+    line: string,
+    lineNumber: number,
+    pair: ControlFlowPair,
+    excludeCurrentPosition?: { line: number; column: number }
+): KeywordMatch[] {
+    const codeLine = stripComments(line);
+    const matches: KeywordMatch[] = [];
+
+    // Find all start keywords
+    let match;
+    const startRegex = new RegExp(pair.startPattern.source, 'g');
+    while ((match = startRegex.exec(codeLine)) !== null) {
+        // Skip if this is the current position we're excluding
+        if (excludeCurrentPosition && 
+            lineNumber === excludeCurrentPosition.line && 
+            match.index === excludeCurrentPosition.column) {
+            continue;
+        }
+        matches.push({
+            keyword: pair.start,
+            index: match.index,
+            line: lineNumber,
+            isStart: true
+        });
+    }
+
+    // Find all end keywords
+    const endRegex = new RegExp(pair.endPattern.source, 'g');
+    while ((match = endRegex.exec(codeLine)) !== null) {
+        matches.push({
+            keyword: pair.end,
+            index: match.index,
+            line: lineNumber,
+            isStart: false
+        });
+    }
+
+    // Sort by position in line
+    matches.sort((a, b) => a.index - b.index);
+    return matches;
+}
+
+/**
+ * Finds all control flow keywords in a line across all types
+ */
+export function findAllControlFlowKeywordsInLine(
+    line: string,
+    lineNumber: number,
+    excludeCurrentPosition?: { line: number; column: number }
+): KeywordMatch[] {
+    const allMatches: KeywordMatch[] = [];
+    
+    for (const [type, pair] of Object.entries(CONTROL_FLOW_PAIRS)) {
+        const matches = findKeywordMatchesInLine(line, lineNumber, pair, excludeCurrentPosition);
+        allMatches.push(...matches);
+    }
+    
+    // Sort by position in line
+    allMatches.sort((a, b) => a.index - b.index);
+    return allMatches;
+}
+
+/**
+ * Determines if a control flow statement is a block statement or inline
+ */
+export function isBlockStatement(line: string, type: string): boolean {
+    const config = CONTROL_FLOW_CONFIGS[type];
+    if (!config) {
+        return false;
+    }
+    return config.blockStartPattern.test(line);
+}
+
+/**
+ * Determines if a control flow statement is an inline statement
+ */
+export function isInlineStatement(line: string, type: string): boolean {
+    const config = CONTROL_FLOW_CONFIGS[type];
+    if (!config) {
+        return false;
+    }
+    return config.inlineStartPattern.test(line);
+}
+
+/**
+ * Checks if an inline statement is complete on the current line
+ * (has terminator ; or , and no end keyword)
+ */
+export function isInlineStatementComplete(line: string): boolean {
+    const codeLine = stripComments(line);
+    const hasTerminator = codeLine.includes(';') || codeLine.includes(',');
+    
+    if (!hasTerminator) {
+        return false;
+    }
+    
+    // Check if there's an end keyword on this line
+    for (const pair of Object.values(CONTROL_FLOW_PAIRS)) {
+        if (pair.endPattern.test(codeLine)) {
+            return false;
+        }
+    }
+    
+    return true;
+}
+
+/**
+ * Returns true if the (trimmed) line looks like the start of a new statement.
+ * Used to implicitly close inline if/switch that don't use endif/endswitch:
+ * e.g. next line "$var = ..." or "return" or block "foreach"/"if" at line start.
+ */
+export function isNewStatementStart(trimmedLine: string): boolean {
+    if (!trimmedLine) return false;
+    // Assignment: $var = ... or $var: ...
+    if (/^\$[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*\s*[=:]/.test(trimmedLine)) {
+        return true;
+    }
+    // return, or block-level control flow at start of line
+    if (/^return\s/.test(trimmedLine)) return true;
+    if (/^(foreach|if|while|switch)\s/.test(trimmedLine)) return true;
+    // End keywords start a new "statement" in the sense that we're done with previous
+    if (/^(endif|endfor|endwhile|endswitch)\b/.test(trimmedLine)) return true;
+    return false;
+}
+
+/**
+ * Finds the matching control flow keyword by tracking depth
+ * Works for multi-line statements and handles nested structures
+ */
+export function findMatchingControlFlowKeyword(
+    document: vscode.TextDocument,
+    startLine: number,
+    startColumn: number,
+    keyword: string
+): vscode.Position | undefined {
+    // Find which control flow pair this keyword belongs to
+    let pair: ControlFlowPair | undefined;
+    let isStartKeyword = false;
+    
+    for (const [type, p] of Object.entries(CONTROL_FLOW_PAIRS)) {
+        if (p.start === keyword) {
+            pair = p;
+            isStartKeyword = true;
+            break;
+        } else if (p.end === keyword) {
+            pair = p;
+            isStartKeyword = false;
+            break;
+        }
+    }
+    
+    if (!pair) {
+        return undefined;
+    }
+    
+    const text = document.getText();
+    const lines = text.split('\n');
+    
+    // Determine search direction
+    const searchForward = isStartKeyword;
+    const increment = searchForward ? 1 : -1;
+    const end = searchForward ? lines.length : -1;
+    
+    // When searching backward from an end keyword, we start with depth 1
+    // because we're closing the current statement
+    // When searching forward from a start keyword, we start with depth 0
+    let depth = searchForward ? 0 : 1;
+    
+    // Start from the line after/before the current position
+    const start = searchForward ? startLine + 1 : startLine - 1;
+    
+    for (let i = start; searchForward ? i < end : i > end; i += increment) {
+        const line = lines[i];
+        const codeLine = stripComments(line);
+        
+        // Find all matches for this control flow pair in this line
+        const matches = findKeywordMatchesInLine(
+            line,
+            i,
+            pair,
+            undefined // Don't exclude anything since we're not on the start line anymore
+        );
+        
+        // Process matches in order
+        for (const match of matches) {
+            if (match.isStart) {
+                // Found a start keyword
+                if (searchForward) {
+                    // Searching forward: start keywords increase depth
+                    depth++;
+                } else {
+                    // Searching backward: start keywords decrease depth
+                    depth--;
+                    if (depth === 0) {
+                        // Found the matching start!
+                        return new vscode.Position(i, match.index);
+                    }
+                }
+            } else {
+                // Found an end keyword
+                if (searchForward) {
+                    // Searching forward: end keywords decrease depth
+                    if (depth === 0) {
+                        // Found the matching end!
+                        return new vscode.Position(i, match.index);
+                    }
+                    depth--;
+                } else {
+                    // Searching backward: end keywords increase depth
+                    depth++;
+                }
+            }
+        }
+    }
+    
+    return undefined;
+}
+
+/**
+ * Validates control flow balance across the entire document
+ * Returns diagnostics for unbalanced statements
+ */
+export function validateControlFlowBalance(
+    document: vscode.TextDocument
+): Array<{ item: ControlFlowStackItem; diagnostic: vscode.Diagnostic }> {
+    const text = document.getText();
+    const lines = text.split('\n');
+    const stack: ControlFlowStackItem[] = [];
+    const errors: Array<{ item: ControlFlowStackItem; diagnostic: vscode.Diagnostic }> = [];
+    
+    for (let i = 0; i < lines.length; i++) {
+        const fullLine = lines[i];
+        const codeLineWithoutComments = stripComments(fullLine);
+        const codeLineTrimmed = codeLineWithoutComments.trim();
+        
+        // Skip empty lines
+        if (!codeLineTrimmed) {
+            continue;
+        }
+        
+        // Inline if/switch don't require endif/endswitch. When we see a new statement
+        // (e.g. next assignment or endfor), implicitly close any open inline items.
+        // BUT: don't pop an item if this line contains its matching end keyword
+        // (e.g. endswitch) - we'll handle that in the match processing below.
+        if (isNewStatementStart(codeLineTrimmed)) {
+            while (stack.length > 0 && !stack[stack.length - 1].isBlock) {
+                const top = stack[stack.length - 1];
+                const pair = CONTROL_FLOW_PAIRS[top.type];
+                if (pair && pair.endPattern.test(codeLineWithoutComments)) {
+                    // This line has the matching end keyword - don't pop, handle it below
+                    break;
+                }
+                stack.pop();
+            }
+        }
+        
+        // Find all control flow keywords in this line
+        const allMatches = findAllControlFlowKeywordsInLine(fullLine, i);
+        
+        // Process each match in order
+        for (const match of allMatches) {
+            // Find the pair for this keyword
+            let pair: ControlFlowPair | undefined;
+            let type: string | undefined;
+            
+            for (const [t, p] of Object.entries(CONTROL_FLOW_PAIRS)) {
+                if (p.start === match.keyword || p.end === match.keyword) {
+                    pair = p;
+                    type = t;
+                    break;
+                }
+            }
+            
+            if (!pair || !type) {
+                continue;
+            }
+            
+            const isStart = match.keyword === pair.start;
+            
+            if (isStart) {
+                // Determine if this is a block or inline statement
+                // Use the line without comments but with whitespace for block detection
+                const isBlock = isBlockStatement(codeLineWithoutComments, type);
+                const isInline = isInlineStatement(codeLineWithoutComments, type);
+                const isAfterReturnKeyword = isAfterReturn(codeLineWithoutComments, match.index);
+                
+                // Determine if we should push this to the stack
+                let shouldPush = false;
+                let treatAsBlock = false;
+                
+                if (isBlock) {
+                    // Block statement - always push
+                    shouldPush = true;
+                    treatAsBlock = true;
+                } else if (isInline) {
+                    // Inline statement - only push if not complete on this line
+                    const isComplete = isInlineStatementComplete(codeLineWithoutComments);
+                    shouldPush = !isComplete;
+                    treatAsBlock = false;
+                } else if (isAfterReturnKeyword) {
+                    // Control flow after 'return' keyword (e.g., "return switch", "return foreach")
+                    // Treat as block statement - always push
+                    shouldPush = true;
+                    treatAsBlock = true;
+                } else {
+                    // Neither block nor inline, and not after return
+                    // This shouldn't normally happen, but treat as block to be safe
+                    shouldPush = true;
+                    treatAsBlock = true;
+                }
+                
+                if (shouldPush) {
+                    stack.push({
+                        type: type,
+                        line: i,
+                        isBlock: treatAsBlock,
+                        startColumn: match.index
+                    });
+                }
+            } else {
+                // This is an end keyword
+                // Inline if/switch may be open without their own endif/endswitch; pop them first
+                while (stack.length > 0 && !stack[stack.length - 1].isBlock && stack[stack.length - 1].type !== type) {
+                    stack.pop();
+                }
+                // Pop from stack if there's a matching start
+                if (stack.length === 0 || stack[stack.length - 1].type !== type) {
+                    // Unexpected end keyword
+                    const range = new vscode.Range(i, 0, i, fullLine.length);
+                    const diagnostic = new vscode.Diagnostic(
+                        range,
+                        `Unexpected ${match.keyword} without matching ${pair.start}`,
+                        vscode.DiagnosticSeverity.Error
+                    );
+                    errors.push({
+                        item: { type: type, line: i, isBlock: false },
+                        diagnostic: diagnostic
+                    });
+                } else {
+                    stack.pop();
+                }
+            }
+        }
+    }
+    
+    // Check for unclosed statements (only report block statements)
+    for (const item of stack) {
+        if (item.isBlock) {
+            const range = new vscode.Range(item.line, 0, item.line, lines[item.line].length);
+            const diagnostic = new vscode.Diagnostic(
+                range,
+                `Unclosed ${item.type} statement`,
+                vscode.DiagnosticSeverity.Error
+            );
+            errors.push({ item: item, diagnostic: diagnostic });
+        }
+    }
+    
+    return errors;
+}
diff --git a/plugin/src/definition.ts b/plugin/src/definition.ts
new file mode 100644
index 0000000..7f0bdb5
--- /dev/null
+++ b/plugin/src/definition.ts
@@ -0,0 +1,219 @@
+import * as vscode from 'vscode';
+import * as path from 'path';
+import * as fs from 'fs';
+
+export class IslDefinitionProvider implements vscode.DefinitionProvider {
+    
+    provideDefinition(
+        document: vscode.TextDocument,
+        position: vscode.Position,
+        token: vscode.CancellationToken
+    ): vscode.Definition | undefined {
+        const range = document.getWordRangeAtPosition(position);
+        if (!range) {
+            return undefined;
+        }
+
+        const word = document.getText(range);
+        const line = document.lineAt(position.line).text;
+        const beforeCursor = line.substring(0, position.character);
+
+        // Check if this is an imported function call: @.ModuleName.functionName()
+        // Look for the pattern before the cursor, allowing for the function name to extend to cursor
+        const importedFunctionMatch = beforeCursor.match(/@\.([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$/);
+        if (importedFunctionMatch) {
+            const moduleName = importedFunctionMatch[1];
+            const functionName = importedFunctionMatch[2];
+            
+            // If the word matches the function name, go to definition
+            if (functionName === word) {
+                return this.findImportedFunctionDefinition(moduleName, functionName, document);
+            }
+        }
+
+        // Also check the full line for @.ModuleName.word pattern (in case cursor is in middle of word)
+        const fullLineFunctionMatch = line.match(new RegExp(`@\\.([a-zA-Z_][a-zA-Z0-9_]*)\\.${word}\\s*\\(`));
+        if (fullLineFunctionMatch) {
+            const moduleName = fullLineFunctionMatch[1];
+            return this.findImportedFunctionDefinition(moduleName, word, document);
+        }
+
+        // Check if this is an imported modifier: | ModuleName.modifierName
+        const importedModifierMatch = beforeCursor.match(/\|\s*([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$/);
+        if (importedModifierMatch) {
+            const moduleName = importedModifierMatch[1];
+            const modifierName = importedModifierMatch[2];
+            
+            // If the word matches the modifier name, go to definition
+            if (modifierName === word) {
+                return this.findImportedModifierDefinition(moduleName, modifierName, document);
+            }
+        }
+
+        // Also check the full line for | ModuleName.word pattern (in case cursor is in middle of word)
+        const fullLineModifierMatch = line.match(new RegExp(`\\|\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\.${word}(?:\\s*\\(|\\s|$)`));
+        if (fullLineModifierMatch) {
+            const moduleName = fullLineModifierMatch[1];
+            return this.findImportedModifierDefinition(moduleName, word, document);
+        }
+
+        // Check if this is a local function call
+        if (line.includes('@.This.' + word)) {
+            return this.findFunctionDefinition(word, document);
+        }
+
+        // Check if this is a type reference
+        if (this.isTypeReference(word, line)) {
+            return this.findTypeDefinition(word, document);
+        }
+
+        // Check if this is an import reference (module name itself)
+        if (this.isImportReference(word, line)) {
+            return this.findImportedFile(word, document);
+        }
+
+        return undefined;
+    }
+
+    private findFunctionDefinition(functionName: string, document: vscode.TextDocument): vscode.Location | undefined {
+        const text = document.getText();
+        const lines = text.split('\n');
+
+        // Look for function declaration
+        const functionPattern = new RegExp(`^\\s*(fun|modifier)\\s+${functionName}\\s*\\(`);
+
+        for (let i = 0; i < lines.length; i++) {
+            if (functionPattern.test(lines[i])) {
+                const position = new vscode.Position(i, 0);
+                return new vscode.Location(document.uri, position);
+            }
+        }
+
+        return undefined;
+    }
+
+    private isTypeReference(word: string, line: string): boolean {
+        // Check if word appears after a colon (type annotation)
+        return line.includes(': ' + word) || line.includes(':' + word);
+    }
+
+    private findTypeDefinition(typeName: string, document: vscode.TextDocument): vscode.Location | undefined {
+        const text = document.getText();
+        const lines = text.split('\n');
+
+        // Look for type declaration
+        const typePattern = new RegExp(`^\\s*type\\s+${typeName}\\s+(as|from)`);
+
+        for (let i = 0; i < lines.length; i++) {
+            if (typePattern.test(lines[i])) {
+                const position = new vscode.Position(i, 0);
+                return new vscode.Location(document.uri, position);
+            }
+        }
+
+        return undefined;
+    }
+
+    private isImportReference(word: string, line: string): boolean {
+        // Check if word appears in import statement or is being used as imported module
+        return line.includes('import ' + word) || line.includes('@.' + word);
+    }
+
+    private findImportedFile(moduleName: string, document: vscode.TextDocument): vscode.Location | undefined {
+        const importedUri = this.resolveImportPath(document, moduleName);
+        if (importedUri) {
+            return new vscode.Location(importedUri, new vscode.Position(0, 0));
+        }
+        return undefined;
+    }
+
+    private findImportedFunctionDefinition(moduleName: string, functionName: string, document: vscode.TextDocument): vscode.Location | undefined {
+        const importedUri = this.resolveImportPath(document, moduleName);
+        if (!importedUri) {
+            return undefined;
+        }
+
+        try {
+            const importedText = fs.readFileSync(importedUri.fsPath, 'utf-8');
+            const lines = importedText.split('\n');
+
+            // Look for function declaration
+            const functionPattern = new RegExp(`^\\s*fun\\s+${functionName}\\s*\\(`);
+
+            for (let i = 0; i < lines.length; i++) {
+                if (functionPattern.test(lines[i])) {
+                    const position = new vscode.Position(i, 0);
+                    return new vscode.Location(importedUri, position);
+                }
+            }
+        } catch (error) {
+            console.warn(`Could not read imported file ${importedUri.fsPath}: ${error}`);
+        }
+
+        return undefined;
+    }
+
+    private findImportedModifierDefinition(moduleName: string, modifierName: string, document: vscode.TextDocument): vscode.Location | undefined {
+        const importedUri = this.resolveImportPath(document, moduleName);
+        if (!importedUri) {
+            return undefined;
+        }
+
+        try {
+            const importedText = fs.readFileSync(importedUri.fsPath, 'utf-8');
+            const lines = importedText.split('\n');
+
+            // Look for modifier declaration
+            const modifierPattern = new RegExp(`^\\s*modifier\\s+${modifierName}\\s*\\(`);
+
+            for (let i = 0; i < lines.length; i++) {
+                if (modifierPattern.test(lines[i])) {
+                    const position = new vscode.Position(i, 0);
+                    return new vscode.Location(importedUri, position);
+                }
+            }
+        } catch (error) {
+            console.warn(`Could not read imported file ${importedUri.fsPath}: ${error}`);
+        }
+
+        return undefined;
+    }
+
+    private resolveImportPath(document: vscode.TextDocument, moduleName: string): vscode.Uri | null {
+        const text = document.getText();
+        const lines = text.split('\n');
+
+        // Look for import statement
+        const importPattern = new RegExp(`import\\s+${moduleName}\\s+from\\s+['"]([^'"]+)['"]`);
+
+        for (let i = 0; i < lines.length; i++) {
+            const match = lines[i].match(importPattern);
+            if (match) {
+                const importPath = match[1];
+                const currentDir = path.dirname(document.uri.fsPath);
+                let resolvedPath: string;
+
+                if (path.isAbsolute(importPath)) {
+                    resolvedPath = importPath;
+                } else {
+                    resolvedPath = path.resolve(currentDir, importPath);
+                }
+
+                // Try with .isl extension if not present
+                if (!resolvedPath.endsWith('.isl')) {
+                    const withExtension = resolvedPath + '.isl';
+                    if (fs.existsSync(withExtension)) {
+                        resolvedPath = withExtension;
+                    }
+                }
+
+                if (fs.existsSync(resolvedPath)) {
+                    return vscode.Uri.file(resolvedPath);
+                }
+            }
+        }
+
+        return null;
+    }
+}
+
diff --git a/plugin/src/executor.ts b/plugin/src/executor.ts
new file mode 100644
index 0000000..75120f4
--- /dev/null
+++ b/plugin/src/executor.ts
@@ -0,0 +1,333 @@
+import * as vscode from 'vscode';
+import * as cp from 'child_process';
+import * as path from 'path';
+import * as fs from 'fs';
+
+const EMBEDDED_JAR_NAME = 'isl-cmd-all.jar';
+
+export class IslExecutor {
+    private outputChannel: vscode.OutputChannel;
+    private readonly extensionPath: string;
+
+    constructor(extensionPath: string) {
+        this.extensionPath = extensionPath;
+        this.outputChannel = vscode.window.createOutputChannel('ISL');
+    }
+
+    private getEmbeddedJarPath(): string | null {
+        const jarPath = path.join(this.extensionPath, 'lib', EMBEDDED_JAR_NAME);
+        return fs.existsSync(jarPath) ? jarPath : null;
+    }
+
+    private async findJava(): Promise {
+        const config = vscode.workspace.getConfiguration('isl.execution');
+        const javaHome = config.get('javaHome', '');
+        if (javaHome) {
+            const p = path.join(javaHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
+            if (fs.existsSync(p)) return p;
+        }
+        const envHome = process.env.JAVA_HOME;
+        if (envHome) {
+            const p = path.join(envHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
+            if (fs.existsSync(p)) return p;
+        }
+        return process.platform === 'win32' ? 'java.exe' : 'java';
+    }
+
+    public async run(document: vscode.TextDocument): Promise {
+        // Prompt for input data
+        const inputJson = await vscode.window.showInputBox({
+            prompt: 'Enter input JSON (or leave empty for empty object)',
+            placeHolder: '{"key": "value"}',
+            value: '{}'
+        });
+
+        if (inputJson === undefined) {
+            return; // User cancelled
+        }
+
+        await this.executeIsl(document, inputJson);
+    }
+
+    /**
+     * Compile (validate) the current ISL file using the embedded isl-cmd-all.jar or external CLI.
+     * Calls the CLI with "validate" and only the ISL file path (no input file).
+     */
+    public async compile(document: vscode.TextDocument): Promise {
+        if (document.uri.scheme !== 'file') {
+            vscode.window.showWarningMessage('Save the document to a file first to compile.');
+            return;
+        }
+        if (document.isDirty) {
+            await document.save();
+        }
+
+        this.outputChannel.clear();
+        this.outputChannel.show();
+        this.outputChannel.appendLine('=== ISL Compile (validate) ===');
+        this.outputChannel.appendLine(`File: ${document.fileName}`);
+        this.outputChannel.appendLine('');
+
+        let useEmbeddedJar = false;
+        try {
+            const config = await this.buildValidateConfig(document.fileName);
+            useEmbeddedJar = config.useEmbeddedJar;
+            this.outputChannel.appendLine(`Command: ${config.command} ${config.args.join(' ')}`);
+            this.outputChannel.appendLine('');
+
+            const result = await this.execPromise(config.command, config.args, {
+                env: config.env,
+                cwd: path.dirname(document.fileName)
+            });
+            this.outputChannel.appendLine(result.stdout);
+            if (result.stderr) {
+                this.outputChannel.appendLine(result.stderr);
+            }
+            vscode.window.showInformationMessage('ISL compile succeeded.');
+        } catch (error: any) {
+            this.outputChannel.appendLine('=== Compile Failed ===');
+            this.outputChannel.appendLine(error.message);
+            if (error.stdout) this.outputChannel.appendLine(error.stdout);
+            if (error.stderr) this.outputChannel.appendLine(error.stderr);
+            this.handleExecutionError(error, useEmbeddedJar);
+        }
+    }
+
+    public async runWithInput(document: vscode.TextDocument): Promise {
+        // Prompt for input file
+        const files = await vscode.window.showOpenDialog({
+            canSelectMany: false,
+            filters: { 'JSON Files': ['json'] },
+            openLabel: 'Select Input JSON File'
+        });
+
+        if (!files || files.length === 0) {
+            return; // User cancelled
+        }
+
+        const inputFile = files[0].fsPath;
+        
+        try {
+            const inputJson = fs.readFileSync(inputFile, 'utf8');
+            await this.executeIsl(document, inputJson);
+        } catch (error) {
+            vscode.window.showErrorMessage(`Failed to read input file: ${error}`);
+        }
+    }
+
+    /**
+     * Build command, args, and env for running ISL validate (compile only, no input file).
+     * Prefers embedded lib/isl-cmd-all.jar (java -jar ...); otherwise uses isl.execution.islCommand or workspace isl.bat/isl.sh.
+     */
+    private async buildValidateConfig(scriptPath: string): Promise<{ command: string; args: string[]; env: NodeJS.ProcessEnv; useEmbeddedJar: boolean }> {
+        const env = { ...process.env };
+        const config = vscode.workspace.getConfiguration('isl.execution');
+        const javaHome = config.get('javaHome', '');
+        if (javaHome) {
+            env.JAVA_HOME = javaHome;
+            env.PATH = `${path.join(javaHome, 'bin')}${path.delimiter}${env.PATH}`;
+        }
+
+        const jarPath = this.getEmbeddedJarPath();
+        if (jarPath) {
+            const javaPath = await this.findJava();
+            if (!javaPath) {
+                throw new Error('Java not found. Set isl.execution.javaHome or JAVA_HOME.');
+            }
+            return {
+                command: javaPath,
+                args: ['-jar', jarPath, 'validate', scriptPath],
+                env,
+                useEmbeddedJar: true
+            };
+        }
+
+        const islCommand = config.get('islCommand', '') || (() => {
+            const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
+            if (workspaceFolder) {
+                const islBat = path.join(workspaceFolder.uri.fsPath, 'isl.bat');
+                const islSh = path.join(workspaceFolder.uri.fsPath, 'isl.sh');
+                if (process.platform === 'win32' && fs.existsSync(islBat)) return islBat;
+                if (fs.existsSync(islSh)) return islSh;
+            }
+            return 'isl';
+        })();
+        return {
+            command: islCommand,
+            args: ['validate', scriptPath],
+            env,
+            useEmbeddedJar: false
+        };
+    }
+
+    /**
+     * Build command, args, and env for running ISL transform (run with input).
+     * Prefers embedded lib/isl-cmd-all.jar (java -jar ...); otherwise uses isl.execution.islCommand or workspace isl.bat/isl.sh.
+     */
+    private async buildRunConfig(scriptPath: string, inputFilePath: string): Promise<{ command: string; args: string[]; env: NodeJS.ProcessEnv; useEmbeddedJar: boolean }> {
+        const env = { ...process.env };
+        const config = vscode.workspace.getConfiguration('isl.execution');
+        const javaHome = config.get('javaHome', '');
+        if (javaHome) {
+            env.JAVA_HOME = javaHome;
+            env.PATH = `${path.join(javaHome, 'bin')}${path.delimiter}${env.PATH}`;
+        }
+
+        const jarPath = this.getEmbeddedJarPath();
+        if (jarPath) {
+            const javaPath = await this.findJava();
+            if (!javaPath) {
+                throw new Error('Java not found. Set isl.execution.javaHome or JAVA_HOME.');
+            }
+            return {
+                command: javaPath,
+                args: ['-jar', jarPath, 'transform', scriptPath, '-i', inputFilePath],
+                env,
+                useEmbeddedJar: true
+            };
+        }
+
+        const islCommand = config.get('islCommand', '') || (() => {
+            const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
+            if (workspaceFolder) {
+                const islBat = path.join(workspaceFolder.uri.fsPath, 'isl.bat');
+                const islSh = path.join(workspaceFolder.uri.fsPath, 'isl.sh');
+                if (process.platform === 'win32' && fs.existsSync(islBat)) return islBat;
+                if (fs.existsSync(islSh)) return islSh;
+            }
+            return 'isl';
+        })();
+        return {
+            command: islCommand,
+            args: ['transform', scriptPath, '-i', inputFilePath],
+            env,
+            useEmbeddedJar: false
+        };
+    }
+
+    private handleExecutionError(error: any, useEmbeddedJar: boolean): void {
+        const isNotFound = error.code === 'ENOENT' || (error.message && String(error.message).includes('ENOENT'));
+        if (isNotFound) {
+            this.outputChannel.appendLine('');
+            if (useEmbeddedJar) {
+                this.outputChannel.appendLine('Java was not found. Set isl.execution.javaHome to your Java installation, or ensure Java is on your PATH.');
+                vscode.window.showErrorMessage(
+                    'Java not found. Set isl.execution.javaHome or add Java to PATH. See Output for details.',
+                    'Open Settings'
+                ).then(choice => {
+                    if (choice === 'Open Settings') {
+                        vscode.commands.executeCommand('workbench.action.openSettings', 'isl.execution.javaHome');
+                    }
+                });
+            } else {
+                this.outputChannel.appendLine('Embedded CLI (lib/isl-cmd-all.jar) not found. Set isl.execution.islCommand to an external ISL executable (isl.bat or isl.sh), or reinstall the extension.');
+                vscode.window.showErrorMessage(
+                    'ISL CLI not found. Set isl.execution.islCommand to isl.bat/isl.sh, or reinstall the extension. See Output for details.',
+                    'Open Settings'
+                ).then(choice => {
+                    if (choice === 'Open Settings') {
+                        vscode.commands.executeCommand('workbench.action.openSettings', 'isl.execution.islCommand');
+                    }
+                });
+            }
+        } else {
+            vscode.window.showErrorMessage('ISL execution failed. Check output for details.');
+        }
+    }
+
+    private async executeIsl(document: vscode.TextDocument, inputJson: string): Promise {
+        if (document.isDirty) {
+            await document.save();
+        }
+
+        const tempDir = path.join(path.dirname(document.fileName), '.isl-temp');
+        if (!fs.existsSync(tempDir)) {
+            fs.mkdirSync(tempDir, { recursive: true });
+        }
+        const tempInputFile = path.join(tempDir, 'input.json');
+        fs.writeFileSync(tempInputFile, inputJson);
+
+        this.outputChannel.clear();
+        this.outputChannel.show();
+        this.outputChannel.appendLine('=== ISL Execution ===');
+        this.outputChannel.appendLine(`Script: ${document.fileName}`);
+        this.outputChannel.appendLine(`Input: ${inputJson}`);
+        this.outputChannel.appendLine('');
+
+        let useEmbeddedJar = false;
+        try {
+            const runConfig = await this.buildRunConfig(document.fileName, tempInputFile);
+            useEmbeddedJar = runConfig.useEmbeddedJar;
+            this.outputChannel.appendLine(`Command: ${runConfig.command} ${runConfig.args.join(' ')}`);
+            this.outputChannel.appendLine('');
+
+            const result = await this.execPromise(runConfig.command, runConfig.args, { env: runConfig.env, cwd: path.dirname(document.fileName) });
+
+            this.outputChannel.appendLine('=== Output ===');
+            this.outputChannel.appendLine(result.stdout);
+
+            if (result.stderr) {
+                this.outputChannel.appendLine('');
+                this.outputChannel.appendLine('=== Errors/Warnings ===');
+                this.outputChannel.appendLine(result.stderr);
+            }
+
+            // Try to parse and format output JSON
+            try {
+                const output = JSON.parse(result.stdout);
+                const formatted = JSON.stringify(output, null, 2);
+                
+                // Show formatted output in new editor
+                const doc = await vscode.workspace.openTextDocument({
+                    content: formatted,
+                    language: 'json'
+                });
+                await vscode.window.showTextDocument(doc, { preview: false, viewColumn: vscode.ViewColumn.Beside });
+            } catch (e) {
+                // Output is not valid JSON, just show it as is
+            }
+
+        } catch (error: any) {
+            this.outputChannel.appendLine('=== Execution Failed ===');
+            this.outputChannel.appendLine(error.message);
+            if (error.stdout) {
+                this.outputChannel.appendLine('');
+                this.outputChannel.appendLine('=== Stdout ===');
+                this.outputChannel.appendLine(error.stdout);
+            }
+            if (error.stderr) {
+                this.outputChannel.appendLine('');
+                this.outputChannel.appendLine('=== Stderr ===');
+                this.outputChannel.appendLine(error.stderr);
+            }
+            this.handleExecutionError(error, useEmbeddedJar);
+        } finally {
+            try {
+                fs.unlinkSync(tempInputFile);
+                fs.rmdirSync(tempDir);
+            } catch (e) {
+                // Ignore cleanup errors
+            }
+        }
+    }
+
+    private execPromise(command: string, args: string[], options: cp.ExecFileOptions): Promise<{ stdout: string, stderr: string }> {
+        return new Promise((resolve, reject) => {
+            cp.execFile(command, args, options, (error, stdout, stderr) => {
+                const stdoutStr = stdout ? stdout.toString() : '';
+                const stderrStr = stderr ? stderr.toString() : '';
+                if (error) {
+                    reject({
+                        message: error.message,
+                        code: (error as NodeJS.ErrnoException).code,
+                        stdout: stdoutStr,
+                        stderr: stderrStr
+                    });
+                } else {
+                    resolve({ stdout: stdoutStr, stderr: stderrStr });
+                }
+            });
+        });
+    }
+}
+
diff --git a/plugin/src/extension.ts b/plugin/src/extension.ts
new file mode 100644
index 0000000..c05a618
--- /dev/null
+++ b/plugin/src/extension.ts
@@ -0,0 +1,432 @@
+import * as vscode from 'vscode';
+import * as path from 'path';
+import { IslDocumentFormatter } from './formatter';
+import { IslValidator } from './validator';
+import { IslExecutor } from './executor';
+import { IslCompletionProvider } from './completion';
+import { IslHoverProvider } from './hover';
+import { IslDefinitionProvider } from './definition';
+import { IslCodeLensProvider, runIslFunction, showUsages, testFunction } from './codelens';
+import { IslSignatureHelpProvider } from './signature';
+import { IslInlayHintsProvider } from './inlayhints';
+import { IslCodeActionProvider, extractVariable, extractFunction, convertToTemplateString, useCoalesceOperator, useMathSum, formatChain, formatObject, renameDuplicateFunction } from './codeactions';
+import { IslDocumentHighlightProvider } from './highlights';
+import { IslExtensionsManager } from './extensions';
+import { initIslLanguage } from './language';
+import { IslTypeManager } from './types';
+import { IslTestController, isTestFile, yamlHasIslTests, addMockToFile, addMockToTestFile } from './testExplorer';
+import { IslYamlTestsCompletionProvider } from './islYamlTestsCompletion';
+import { IslPasteEditProvider } from './islPasteProvider';
+
+const outputChannelName = 'ISL Language Support';
+
+export function activate(context: vscode.ExtensionContext) {
+    const outputChannel = vscode.window.createOutputChannel(outputChannelName);
+    try {
+        initIslLanguage(context.extensionPath);
+        outputChannel.appendLine('[ISL Language Support] Extension is now active');
+
+    const documentSelector: vscode.DocumentSelector = [
+        { scheme: 'file', language: 'isl' },
+        { scheme: 'untitled', language: 'isl' }
+    ];
+
+    // Initialize extensions manager with output channel for extension logs
+    const extensionsManager = new IslExtensionsManager(outputChannel);
+    context.subscriptions.push(extensionsManager);
+
+    const typeManager = new IslTypeManager(extensionsManager, outputChannel);
+    context.subscriptions.push(typeManager);
+
+    // ISL Test Explorer - discovers @test/@setup in tests/**/*.isl and *.tests.yaml (islTests)
+    // Use a dedicated "ISL Tests" output channel for test run results
+    const islTestsOutputChannel = vscode.window.createOutputChannel('ISL Tests');
+    const testController = new IslTestController(islTestsOutputChannel, context.extensionPath);
+    context.subscriptions.push({ dispose: () => testController.dispose() });
+
+    // Update "Run all Tests in file" button visibility when active editor changes
+    const updateIsTestFileContext = (doc: vscode.TextDocument | undefined) => {
+        if (!doc || doc.uri.scheme !== 'file') {
+            vscode.commands.executeCommand('setContext', 'isl.isTestFile', false);
+            return;
+        }
+        const isIsl = doc.uri.fsPath.endsWith('.isl');
+        const isYamlTests = doc.uri.fsPath.endsWith('.tests.yaml');
+        if (isIsl) {
+            const folder = vscode.workspace.getWorkspaceFolder(doc.uri);
+            vscode.commands.executeCommand('setContext', 'isl.isTestFile', !!folder && isTestFile(doc.uri, folder));
+        } else if (isYamlTests) {
+            vscode.commands.executeCommand('setContext', 'isl.isTestFile', yamlHasIslTests(doc.getText()));
+        } else {
+            vscode.commands.executeCommand('setContext', 'isl.isTestFile', false);
+        }
+    };
+    context.subscriptions.push(
+        vscode.window.onDidChangeActiveTextEditor(e => updateIsTestFileContext(e?.document)),
+        vscode.workspace.onDidChangeTextDocument(e => {
+            if (vscode.window.activeTextEditor?.document === e.document) {
+                updateIsTestFileContext(e.document);
+            }
+        })
+    );
+    updateIsTestFileContext(vscode.window.activeTextEditor?.document);
+
+    // Preload extensions so first completion/validation has a warm cache
+    extensionsManager.preloadExtensions().catch(() => {});
+
+    // Register formatter
+    const formatter = new IslDocumentFormatter();
+    context.subscriptions.push(
+        vscode.languages.registerDocumentFormattingEditProvider(documentSelector, formatter),
+        vscode.languages.registerDocumentRangeFormattingEditProvider(documentSelector, formatter)
+    );
+
+    // Register validator (with output channel for validation logs and type manager for schema checks)
+    const validator = new IslValidator(extensionsManager, { outputChannel, typeManager });
+    context.subscriptions.push(validator);
+
+    // Validate on open, save, and change
+    context.subscriptions.push(
+        vscode.workspace.onDidOpenTextDocument(doc => {
+            if (doc.languageId === 'isl') {
+                validator.validate(doc);
+            }
+        }),
+        vscode.workspace.onDidSaveTextDocument(doc => {
+            if (doc.languageId === 'isl') {
+                validator.validate(doc);
+            }
+        }),
+        vscode.workspace.onDidChangeTextDocument(event => {
+            if (event.document.languageId === 'isl') {
+                validator.validateDebounced(event.document);
+            }
+        })
+    );
+
+    // Validate all open ISL documents on activation
+    vscode.workspace.textDocuments.forEach(doc => {
+        if (doc.languageId === 'isl') {
+            validator.validate(doc);
+        }
+    });
+
+    // Register completion provider (triggers: $ @ . | - not { or , so user can type { and Enter without popup)
+    const completionProvider = new IslCompletionProvider(extensionsManager, typeManager, outputChannel);
+    context.subscriptions.push(
+        vscode.languages.registerCompletionItemProvider(documentSelector, completionProvider, '$', '@', '.', '|')
+    );
+
+    // Autocomplete for ISL tests YAML (*.tests.yaml): root keys, setup, test entries, assertOptions, function names
+    const yamlTestsSelector: vscode.DocumentSelector = { language: 'yaml', pattern: '**/*.tests.yaml' };
+    const yamlTestsCompletionProvider = new IslYamlTestsCompletionProvider();
+    context.subscriptions.push(
+        vscode.languages.registerCompletionItemProvider(yamlTestsSelector, yamlTestsCompletionProvider, ':', '-', '\n')
+    );
+
+    // Register hover provider
+    const hoverProvider = new IslHoverProvider(extensionsManager, typeManager);
+    context.subscriptions.push(
+        vscode.languages.registerHoverProvider(documentSelector, hoverProvider)
+    );
+
+    // Watch for extensions file changes and revalidate
+    extensionsManager.onDidChange((uri) => {
+        // Revalidate all open ISL documents
+        vscode.workspace.textDocuments.forEach(doc => {
+            if (doc.languageId === 'isl') {
+                validator.validate(doc);
+            }
+        });
+        vscode.window.showInformationMessage('ISL extensions reloaded');
+    });
+
+    // Register definition provider
+    const definitionProvider = new IslDefinitionProvider();
+    context.subscriptions.push(
+        vscode.languages.registerDefinitionProvider(documentSelector, definitionProvider)
+    );
+
+    // Register signature help provider
+    const signatureHelpProvider = new IslSignatureHelpProvider();
+    context.subscriptions.push(
+        vscode.languages.registerSignatureHelpProvider(
+            documentSelector,
+            signatureHelpProvider,
+            '(', ',', ' '
+        )
+    );
+
+    // Register inlay hints provider
+    const inlayHintsProvider = new IslInlayHintsProvider();
+    context.subscriptions.push(
+        vscode.languages.registerInlayHintsProvider(documentSelector, inlayHintsProvider)
+    );
+
+    // Register document highlight provider for control flow keyword matching
+    const highlightProvider = new IslDocumentHighlightProvider();
+    context.subscriptions.push(
+        vscode.languages.registerDocumentHighlightProvider(documentSelector, highlightProvider)
+    );
+
+    // Register code action provider
+    const codeActionProvider = new IslCodeActionProvider();
+    context.subscriptions.push(
+        vscode.languages.registerCodeActionsProvider(
+            documentSelector,
+            codeActionProvider,
+            {
+                providedCodeActionKinds: IslCodeActionProvider.providedCodeActionKinds
+            }
+        )
+    );
+
+    // Register CodeLens provider for "Run" buttons
+    const codeLensProvider = new IslCodeLensProvider();
+    context.subscriptions.push(
+        vscode.languages.registerCodeLensProvider(documentSelector, codeLensProvider)
+    );
+
+    // Paste provider: insert clipboard text as-is to avoid VS Code adding indentation when pasting
+    // in the middle of a line (e.g. between quotes in $var.message = "").
+    context.subscriptions.push(
+        vscode.languages.registerDocumentPasteEditProvider(
+            documentSelector,
+            new IslPasteEditProvider(),
+            {
+                providedPasteEditKinds: [vscode.DocumentDropOrPasteEditKind.Text],
+                pasteMimeTypes: ['text/plain']
+            }
+        )
+    );
+    // Refresh CodeLens when ISL documents change (e.g. after rename, copy-paste) - debounced
+    let codeLensRefreshTimeout: NodeJS.Timeout | undefined;
+    context.subscriptions.push(
+        vscode.workspace.onDidChangeTextDocument(event => {
+            if (event.document.languageId === 'isl') {
+                if (codeLensRefreshTimeout) clearTimeout(codeLensRefreshTimeout);
+                codeLensRefreshTimeout = setTimeout(() => {
+                    codeLensRefreshTimeout = undefined;
+                    codeLensProvider.refresh();
+                }, 1500);
+            }
+        })
+    );
+
+    // Register executor
+    const executor = new IslExecutor(context.extensionPath);
+
+    // ISL terminal profile: shell with lib/ on PATH so user can run "isl" or "isl.bat" / "isl.sh"
+    const islLibPath = path.join(context.extensionPath, 'lib');
+    context.subscriptions.push(
+        vscode.window.registerTerminalProfileProvider('isl.terminal-profile', {
+            provideTerminalProfile(_token: vscode.CancellationToken): vscode.ProviderResult {
+                const pathEnv = process.env.PATH || process.env.Path || '';
+                const newPath = `${islLibPath}${path.delimiter}${pathEnv}`;
+                const shellPath = process.platform === 'win32'
+                    ? (process.env.COMSPEC || 'cmd.exe')
+                    : (process.env.SHELL || '/bin/bash');
+                const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri;
+                return new vscode.TerminalProfile({
+                    name: 'ISL',
+                    shellPath,
+                    cwd: workspaceRoot,
+                    env: { ...process.env, PATH: newPath }
+                });
+            }
+        })
+    );
+
+    // Register commands
+    context.subscriptions.push(
+        vscode.commands.registerCommand('isl.validate', async () => {
+            const editor = vscode.window.activeTextEditor;
+            if (editor && editor.document.languageId === 'isl') {
+                await validator.validate(editor.document);
+                vscode.window.showInformationMessage('ISL validation complete');
+            }
+        }),
+
+        vscode.commands.registerCommand('isl.run', async () => {
+            const editor = vscode.window.activeTextEditor;
+            if (editor && editor.document.languageId === 'isl') {
+                await executor.run(editor.document);
+            }
+        }),
+
+        vscode.commands.registerCommand('isl.runWithInput', async () => {
+            const editor = vscode.window.activeTextEditor;
+            if (editor && editor.document.languageId === 'isl') {
+                await executor.runWithInput(editor.document);
+            }
+        }),
+
+        vscode.commands.registerCommand('isl.compile', async () => {
+            const editor = vscode.window.activeTextEditor;
+            if (editor && editor.document.languageId === 'isl') {
+                await executor.compile(editor.document);
+            }
+        }),
+
+        vscode.commands.registerCommand('isl.format', async () => {
+            const editor = vscode.window.activeTextEditor;
+            if (editor && editor.document.languageId === 'isl') {
+                await vscode.commands.executeCommand('editor.action.formatDocument');
+            }
+        }),
+
+        vscode.commands.registerCommand('isl.showDocumentation', () => {
+            vscode.env.openExternal(vscode.Uri.parse('https://intuit.github.io/isl/'));
+        }),
+
+        vscode.commands.registerCommand('isl.runFunction', async (uri: vscode.Uri, functionName: string, params: string) => {
+            await runIslFunction(uri, functionName, params, context);
+        }),
+
+        vscode.commands.registerCommand('isl.showUsages', async (uri: vscode.Uri, functionName: string, functionType: string) => {
+            await showUsages(uri, functionName, functionType);
+        }),
+
+        vscode.commands.registerCommand('isl.testFunction', async (uri: vscode.Uri, functionName: string, params: string) => {
+            await testFunction(uri, functionName, params, context);
+        }),
+
+        vscode.commands.registerCommand('isl.runAllTestsInFile', async () => {
+            const editor = vscode.window.activeTextEditor;
+            if (editor) {
+                await testController.runTestsInFile(editor.document.uri);
+            }
+        }),
+
+        // Refactoring commands
+        vscode.commands.registerCommand('isl.refactor.extractVariable', extractVariable),
+        vscode.commands.registerCommand('isl.refactor.extractFunction', extractFunction),
+        vscode.commands.registerCommand('isl.refactor.toTemplateString', convertToTemplateString),
+
+        // Improvement commands
+        vscode.commands.registerCommand('isl.improvement.useCoalesceOperator', useCoalesceOperator),
+        vscode.commands.registerCommand('isl.improvement.useMathSum', useMathSum),
+        vscode.commands.registerCommand('isl.improvement.formatChain', formatChain),
+        vscode.commands.registerCommand('isl.improvement.formatObject', formatObject),
+
+        // Quick fix commands
+        vscode.commands.registerCommand('isl.quickFix.renameDuplicateFunction', (uri: vscode.Uri, lineNumber: number, functionName: string, kind: 'fun' | 'modifier') =>
+            renameDuplicateFunction(uri, lineNumber, functionName, kind)),
+
+        // Add mock from test failure (triggered by link in test message).
+        // VS Code may pass the JSON array as separate arguments (arg0, arg1, ...) or as a single JSON string.
+        vscode.commands.registerCommand('isl.addMockFromTestError', async (...allArgs: unknown[]) => {
+            let arr: unknown[];
+            if (allArgs.length >= 5) {
+                arr = allArgs;
+            } else if (allArgs.length === 1 && typeof allArgs[0] === 'string') {
+                try {
+                    let parsed = JSON.parse(allArgs[0]) as unknown;
+                    arr = (typeof parsed === 'string' ? JSON.parse(parsed) : parsed) as unknown[];
+                } catch {
+                    arr = allArgs;
+                }
+            } else {
+                arr = allArgs;
+            }
+            const [testFileUriStr, mockFileName, functionName, paramsJson, yamlSnippet, addToTestFile] = arr as [string?, string?, string?, string?, string?, boolean?];
+
+            if (!testFileUriStr || functionName == null || paramsJson == null || !yamlSnippet) {
+                vscode.window.showErrorMessage('ISL: Invalid add-mock arguments.');
+                return;
+            }
+            if (!addToTestFile && !mockFileName) {
+                vscode.window.showErrorMessage('ISL: Invalid add-mock arguments (missing mock file).');
+                return;
+            }
+            const testFileUri = vscode.Uri.parse(testFileUriStr);
+            try {
+                if (addToTestFile) {
+                    await addMockToTestFile(testFileUri, functionName, paramsJson, yamlSnippet, outputChannel);
+                    vscode.window.showInformationMessage(`Added mock for @.${functionName} to test file (setup)`);
+                } else {
+                    await addMockToFile(testFileUri, mockFileName!, functionName, paramsJson, yamlSnippet, outputChannel);
+                    vscode.window.showInformationMessage(`Added mock for @.${functionName} to ${mockFileName}`);
+                }
+            } catch (e) {
+                const msg = e instanceof Error ? e.message : String(e);
+                vscode.window.showErrorMessage(`ISL: Failed to add mock: ${msg}`);
+                outputChannel.appendLine(`[ISL] addMockFromTestError failed: ${msg}`);
+            }
+        })
+    );
+
+    // Enhanced status bar item
+    const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
+    context.subscriptions.push(statusBarItem);
+
+    // Update status bar
+    function updateStatusBar() {
+        const editor = vscode.window.activeTextEditor;
+        if (editor && editor.document.languageId === 'isl') {
+            const document = editor.document;
+            const text = document.getText();
+            
+            // Count functions and modifiers
+            const functionCount = (text.match(/^\s*(fun|modifier)\s+/gm) || []).length;
+            
+            // Get diagnostics count
+            const diagnostics = vscode.languages.getDiagnostics(document.uri);
+            const errorCount = diagnostics.filter(d => d.severity === vscode.DiagnosticSeverity.Error).length;
+            const warningCount = diagnostics.filter(d => d.severity === vscode.DiagnosticSeverity.Warning).length;
+            
+            // Build status text
+            let statusText = '$(check) ISL';
+            if (functionCount > 0) {
+                statusText += ` | ${functionCount} ${functionCount === 1 ? 'function' : 'functions'}`;
+            }
+            
+            if (errorCount > 0) {
+                statusText += ` | $(error) ${errorCount}`;
+            } else if (warningCount > 0) {
+                statusText += ` | $(warning) ${warningCount}`;
+            } else {
+                statusText += ` | $(pass) Valid`;
+            }
+            
+            statusBarItem.text = statusText;
+            statusBarItem.tooltip = `ISL Language Support\n${functionCount} functions\n${errorCount} errors, ${warningCount} warnings`;
+            statusBarItem.command = 'isl.showDocumentation';
+            statusBarItem.show();
+        } else {
+            statusBarItem.hide();
+        }
+    }
+
+    // Update status bar on various events
+    context.subscriptions.push(
+        vscode.window.onDidChangeActiveTextEditor(updateStatusBar),
+        vscode.workspace.onDidChangeTextDocument(e => {
+            if (e.document.languageId === 'isl') {
+                updateStatusBar();
+            }
+        }),
+        vscode.languages.onDidChangeDiagnostics(e => {
+            const editor = vscode.window.activeTextEditor;
+            if (editor && e.uris.some(uri => uri.toString() === editor.document.uri.toString())) {
+                updateStatusBar();
+            }
+        })
+    );
+
+    // Initial status bar update
+    updateStatusBar();
+    } catch (err) {
+        const msg = err instanceof Error ? err.message : String(err);
+        const stack = err instanceof Error ? err.stack : '';
+        outputChannel.appendLine(`[ISL Language Support] Activation failed: ${msg}`);
+        if (stack) outputChannel.appendLine(stack);
+        outputChannel.show(true);
+        vscode.window.showErrorMessage(`ISL Language Support failed to activate: ${msg}. See Output > ISL Language Support for details.`);
+    }
+}
+
+export function deactivate() {
+    console.log('ISL Language Support is now deactivated');
+}
+
diff --git a/plugin/src/extensions.ts b/plugin/src/extensions.ts
new file mode 100644
index 0000000..d551680
--- /dev/null
+++ b/plugin/src/extensions.ts
@@ -0,0 +1,797 @@
+import * as vscode from 'vscode';
+import * as path from 'path';
+import * as fs from 'fs';
+import * as https from 'https';
+import * as http from 'http';
+import * as os from 'os';
+import * as cp from 'child_process';
+import * as url from 'url';
+
+export interface IslParameter {
+    name: string;
+    type?: string;
+    description?: string;
+    optional?: boolean;
+    defaultValue?: string;
+}
+
+export interface IslFunctionDefinition {
+    name: string;
+    description?: string;
+    parameters: IslParameter[];
+    returns?: {
+        type?: string;
+        description?: string;
+    };
+    examples?: string[];
+}
+
+export interface IslModifierDefinition {
+    name: string;
+    description?: string;
+    parameters: IslParameter[];
+    returns?: {
+        type?: string;
+        description?: string;
+    };
+    examples?: string[];
+}
+
+export interface IslExtensions {
+    functions: Map;
+    modifiers: Map;
+}
+
+/** Case-insensitive lookup for extension function by name (e.g. sendEmail vs SendEmail). */
+export function getExtensionFunction(ext: IslExtensions, name: string): IslFunctionDefinition | undefined {
+    if (!name) return undefined;
+    if (ext.functions.has(name)) return ext.functions.get(name);
+    const lower = name.toLowerCase();
+    for (const [key, def] of ext.functions) {
+        if (key.toLowerCase() === lower) return def;
+    }
+    return undefined;
+}
+
+/** Case-insensitive lookup for extension modifier by name. */
+export function getExtensionModifier(ext: IslExtensions, name: string): IslModifierDefinition | undefined {
+    if (!name) return undefined;
+    if (ext.modifiers.has(name)) return ext.modifiers.get(name);
+    const lower = name.toLowerCase();
+    for (const [key, def] of ext.modifiers) {
+        if (key.toLowerCase() === lower) return def;
+    }
+    return undefined;
+}
+
+/**
+ * Manages loading and caching of custom ISL extensions from .islextensions files
+ */
+export class IslExtensionsManager {
+    private extensionsCache: Map = new Map();
+    private fileWatcher: vscode.FileSystemWatcher | undefined;
+    private onDidChangeEmitter = new vscode.EventEmitter();
+    private globalExtensionsCache: { content: string; timestamp: number } | null = null;
+    private configWatcher: vscode.Disposable | undefined;
+    private readonly outputChannel: vscode.OutputChannel | undefined;
+
+    /**
+     * Event fired when an .islextensions file changes
+     */
+    public readonly onDidChange = this.onDidChangeEmitter.event;
+
+    constructor(outputChannel?: vscode.OutputChannel) {
+        this.outputChannel = outputChannel;
+        this.setupFileWatcher();
+        this.setupConfigWatcher();
+    }
+
+    /**
+     * Writes a message to the extension output channel (and console when debugging)
+     */
+    private log(message: string, level: 'log' | 'warn' | 'error' = 'log'): void {
+        if (this.outputChannel) {
+            this.outputChannel.appendLine(message);
+        }
+        if (level === 'log') {
+            console.log(message);
+        } else if (level === 'warn') {
+            console.warn(message);
+        } else {
+            console.error(message);
+        }
+    }
+
+    /**
+     * Sets up file watcher for .islextensions files
+     */
+    private setupFileWatcher() {
+        // Watch for .islextensions files in the workspace
+        this.fileWatcher = vscode.workspace.createFileSystemWatcher('**/.islextensions');
+        
+        this.fileWatcher.onDidCreate(uri => {
+            this.log(`[ISL Extensions] File created: ${uri.fsPath}`);
+            this.invalidateCache(uri);
+            this.onDidChangeEmitter.fire(uri);
+        });
+        
+        this.fileWatcher.onDidChange(uri => {
+            this.log(`[ISL Extensions] File changed: ${uri.fsPath}`);
+            this.invalidateCache(uri);
+            this.onDidChangeEmitter.fire(uri);
+        });
+        
+        this.fileWatcher.onDidDelete(uri => {
+            this.log(`[ISL Extensions] File deleted: ${uri.fsPath}`);
+            this.invalidateCache(uri);
+            this.onDidChangeEmitter.fire(uri);
+        });
+    }
+
+    /**
+     * Sets up configuration watcher for global extensions source
+     */
+    private setupConfigWatcher() {
+        this.configWatcher = vscode.workspace.onDidChangeConfiguration(event => {
+            if (event.affectsConfiguration('isl.extensions.source') || 
+                event.affectsConfiguration('isl.extensions.cacheTTL')) {
+                const source = vscode.workspace.getConfiguration('isl').get('extensions.source', '');
+                this.log(`[ISL Extensions] Configuration changed - source: ${source || '(none)'}`);
+                // Clear global cache when config changes
+                this.globalExtensionsCache = null;
+                // Invalidate all workspace caches to force reload
+                this.extensionsCache.clear();
+                this.log('[ISL Extensions] Cleared all extension caches due to configuration change');
+                // Notify all workspaces to reload
+                vscode.workspace.workspaceFolders?.forEach(folder => {
+                    this.onDidChangeEmitter.fire(vscode.Uri.file(folder.uri.fsPath));
+                });
+            }
+        });
+    }
+
+    /**
+     * Invalidates the cache for a specific extensions file
+     */
+    private invalidateCache(uri: vscode.Uri) {
+        const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
+        if (workspaceFolder) {
+            this.extensionsCache.delete(workspaceFolder.uri.fsPath);
+        }
+    }
+
+    /** Cache key used when document has no workspace folder (e.g. untitled or single-file) */
+    private static readonly NO_WORKSPACE_CACHE_KEY = '__global_extensions__';
+
+    /**
+     * Gets extensions for a given document's workspace (or global source if no workspace)
+     */
+    public async getExtensionsForDocument(document: vscode.TextDocument): Promise {
+        const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri);
+
+        if (workspaceFolder) {
+            // Check cache first
+            const cached = this.extensionsCache.get(workspaceFolder.uri.fsPath);
+            if (cached) {
+                return cached;
+            }
+            const extensions = await this.loadExtensionsForWorkspace(workspaceFolder);
+            this.extensionsCache.set(workspaceFolder.uri.fsPath, extensions);
+            return extensions;
+        }
+
+        // No workspace folder (untitled, or file outside workspace) – use global source only
+        const cached = this.extensionsCache.get(IslExtensionsManager.NO_WORKSPACE_CACHE_KEY);
+        if (cached) {
+            return cached;
+        }
+        const extensions = await this.loadExtensionsFromGlobalSourceOnly();
+        this.extensionsCache.set(IslExtensionsManager.NO_WORKSPACE_CACHE_KEY, extensions);
+        return extensions;
+    }
+
+    /**
+     * Loads extensions from global source only (used when document has no workspace folder)
+     */
+    private async loadExtensionsFromGlobalSourceOnly(): Promise {
+        const globalSource = vscode.workspace.getConfiguration('isl').get('extensions.source', '');
+        if (!globalSource) {
+            return this.createEmptyExtensions();
+        }
+        try {
+            this.log(`[ISL Extensions] Loading global extensions (no workspace): ${globalSource}`);
+            const content = await this.loadGlobalExtensions(globalSource);
+            if (content) {
+                const extensions = this.parseExtensionsFile(content, globalSource);
+                this.log(`[ISL Extensions] Loaded ${extensions.functions.size} functions and ${extensions.modifiers.size} modifiers from global source`);
+                return extensions;
+            }
+        } catch (error) {
+            this.log(`[ISL Extensions] Error loading global extensions: ${error instanceof Error ? error.message : String(error)}`, 'error');
+        }
+        return this.createEmptyExtensions();
+    }
+
+    /**
+     * Loads extensions from .islextensions file in the workspace
+     * Priority: workspace-local file > global source (URL/file) > empty
+     */
+    private async loadExtensionsForWorkspace(workspaceFolder: vscode.WorkspaceFolder): Promise {
+        // First, try workspace-local file (highest priority)
+        const extensionsPath = path.join(workspaceFolder.uri.fsPath, '.islextensions');
+        
+        try {
+            if (fs.existsSync(extensionsPath)) {
+                this.log(`[ISL Extensions] Loading workspace-local extensions from: ${extensionsPath}`);
+                const content = fs.readFileSync(extensionsPath, 'utf-8');
+                const extensions = this.parseExtensionsFile(content, extensionsPath);
+                this.log(`[ISL Extensions] Loaded ${extensions.functions.size} functions and ${extensions.modifiers.size} modifiers from workspace-local file`);
+                return extensions;
+            } else {
+                this.log(`[ISL Extensions] No workspace-local .islextensions file found at: ${extensionsPath}`);
+            }
+        } catch (error) {
+            this.log(`[ISL Extensions] Error loading workspace-local .islextensions file: ${error instanceof Error ? error.message : String(error)}`, 'error');
+            vscode.window.showWarningMessage(`Failed to load workspace .islextensions file: ${error instanceof Error ? error.message : String(error)}`);
+        }
+
+        // If no workspace-local file, try global source
+        const globalSource = vscode.workspace.getConfiguration('isl').get('extensions.source', '');
+        if (!globalSource) {
+            return this.createEmptyExtensions();
+        }
+
+        try {
+            this.log(`[ISL Extensions] Loading global extensions from: ${globalSource}`);
+            const content = await this.loadGlobalExtensions(globalSource);
+            if (content) {
+                const extensions = this.parseExtensionsFile(content, globalSource);
+                this.log(`[ISL Extensions] Loaded ${extensions.functions.size} functions and ${extensions.modifiers.size} modifiers from global source`);
+                return extensions;
+            } else {
+                this.log(`[ISL Extensions] No content loaded from global source: ${globalSource}`);
+            }
+        } catch (error) {
+            this.log(`[ISL Extensions] Error loading global ISL extensions: ${error instanceof Error ? error.message : String(error)}`, 'error');
+            // Don't show warning for global source failures - it's optional
+        }
+
+        return this.createEmptyExtensions();
+    }
+
+    /**
+     * Loads extensions from global source (URL or file path)
+     */
+    private async loadGlobalExtensions(source: string): Promise {
+        // Check if it's a URL
+        if (source.startsWith('http://') || source.startsWith('https://')) {
+            return await this.loadFromUrl(source);
+        }
+
+        // Otherwise, treat as file path
+        return await this.loadFromFile(source);
+    }
+
+    /**
+     * Loads extensions from a URL with caching
+     */
+    private async loadFromUrl(urlString: string): Promise {
+        const cacheTTL = vscode.workspace.getConfiguration('isl').get('extensions.cacheTTL', 3600);
+        const now = Date.now();
+        const cacheAge = this.globalExtensionsCache 
+            ? Math.floor((now - this.globalExtensionsCache.timestamp) / 1000)
+            : null;
+
+        // Check cache
+        if (this.globalExtensionsCache && 
+            (now - this.globalExtensionsCache.timestamp) < (cacheTTL * 1000)) {
+            this.log(`[ISL Extensions] Using cached extensions from URL (age: ${cacheAge}s, TTL: ${cacheTTL}s)`);
+            return this.globalExtensionsCache.content;
+        }
+
+        // Check if it's a GitHub URL
+        const githubInfo = this.parseGitHubUrl(urlString);
+        if (githubInfo) {
+            this.log(`[ISL Extensions] Detected GitHub URL: ${urlString}`);
+            this.log(`[ISL Extensions] Parsed GitHub info: ${githubInfo.host}/${githubInfo.owner}/${githubInfo.repo}@${githubInfo.ref}:${githubInfo.path}`);
+            try {
+                const content = await this.downloadFromGitHub(githubInfo);
+                this.log(`[ISL Extensions] Successfully downloaded from GitHub (${content.length} bytes)`);
+                
+                // Update cache
+                this.globalExtensionsCache = {
+                    content: content,
+                    timestamp: now
+                };
+                this.log(`[ISL Extensions] Cached extensions (will expire in ${cacheTTL}s)`);
+                return content;
+            } catch (error) {
+                this.log(`[ISL Extensions] Failed to download from GitHub: ${error instanceof Error ? error.message : String(error)}`, 'error');
+                // Return cached content even if expired, as fallback
+                if (this.globalExtensionsCache) {
+                    this.log(`[ISL Extensions] Using expired cache as fallback (age: ${cacheAge}s)`);
+                    return this.globalExtensionsCache.content;
+                }
+                return null;
+            }
+        }
+
+        // Regular URL download
+        try {
+            this.log(`[ISL Extensions] Downloading extensions from URL: ${urlString}`);
+            const content = await this.downloadUrl(urlString);
+            this.log(`[ISL Extensions] Successfully downloaded ${content.length} bytes`);
+            
+            // Update cache
+            this.globalExtensionsCache = {
+                content: content,
+                timestamp: now
+            };
+            this.log(`[ISL Extensions] Cached extensions (will expire in ${cacheTTL}s)`);
+            return content;
+        } catch (error) {
+            this.log(`[ISL Extensions] Failed to download extensions from URL ${urlString}: ${error instanceof Error ? error.message : String(error)}`, 'error');
+            // Return cached content even if expired, as fallback
+            if (this.globalExtensionsCache) {
+                this.log(`[ISL Extensions] Using expired cache as fallback (age: ${cacheAge}s)`);
+                return this.globalExtensionsCache.content;
+            }
+            return null;
+        }
+    }
+
+    /**
+     * Parses a GitHub URL to extract repository information
+     */
+    private parseGitHubUrl(urlString: string): { host: string; owner: string; repo: string; ref: string; path: string } | null {
+        try {
+            const parsed = new url.URL(urlString);
+            const host = parsed.hostname;
+            
+            // Check if it's a GitHub host (github.com or github.*.com)
+            if (!host.includes('github')) {
+                return null;
+            }
+
+            // Handle raw.githubusercontent.com URLs
+            if (host === 'raw.githubusercontent.com') {
+                const parts = parsed.pathname.split('/').filter(p => p);
+                if (parts.length >= 4) {
+                    return {
+                        host: 'github.com',
+                        owner: parts[0],
+                        repo: parts[1],
+                        ref: parts[2],
+                        path: parts.slice(3).join('/')
+                    };
+                }
+            }
+
+            // Handle regular GitHub URLs (github.com or github.*.com)
+            // Pattern: https://github.com/owner/repo/blob/branch/path/to/file
+            const pathMatch = parsed.pathname.match(/^\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/(.+)$/);
+            if (pathMatch) {
+                return {
+                    host: host,
+                    owner: pathMatch[1],
+                    repo: pathMatch[2],
+                    ref: pathMatch[3],
+                    path: pathMatch[4]
+                };
+            }
+
+            return null;
+        } catch (error) {
+            return null;
+        }
+    }
+
+    /**
+     * Downloads content from GitHub using git/gh CLI with proper authentication
+     */
+    private async downloadFromGitHub(githubInfo: { host: string; owner: string; repo: string; ref: string; path: string }): Promise {
+        // Try GitHub CLI (gh) first
+        try {
+            this.log(`[ISL Extensions] Attempting to download via GitHub CLI (gh)`);
+            const content = await this.downloadViaGhCli(githubInfo);
+            if (content) {
+                return content;
+            }
+        } catch (error) {
+            this.log(`[ISL Extensions] GitHub CLI failed: ${error instanceof Error ? error.message : String(error)}`);
+        }
+
+        // Fall back to git checkout
+        try {
+            this.log(`[ISL Extensions] Attempting to download via git checkout`);
+            const content = await this.downloadViaGit(githubInfo);
+            if (content) {
+                return content;
+            }
+        } catch (error) {
+            this.log(`[ISL Extensions] Git checkout failed: ${error instanceof Error ? error.message : String(error)}`);
+        }
+
+        // Final fallback: direct download from raw URL
+        this.log(`[ISL Extensions] Falling back to direct download from raw URL`);
+        const rawUrl =
+            githubInfo.host === 'github.com'
+                ? `https://raw.githubusercontent.com/${githubInfo.owner}/${githubInfo.repo}/${githubInfo.ref}/${githubInfo.path}`
+                : `https://${githubInfo.host}/${githubInfo.owner}/${githubInfo.repo}/raw/${githubInfo.ref}/${githubInfo.path}`;
+        return await this.downloadUrl(rawUrl);
+    }
+
+    /**
+     * Downloads file using GitHub CLI (gh)
+     */
+    private downloadViaGhCli(githubInfo: { host: string; owner: string; repo: string; ref: string; path: string }): Promise {
+        return new Promise((resolve, reject) => {
+            const repo = `${githubInfo.owner}/${githubInfo.repo}`;
+            const command = 'gh';
+            const args = ['api', `repos/${repo}/contents/${githubInfo.path}`, '--jq', '.content', '--field', `ref=${githubInfo.ref}`];
+
+            // If it's not github.com, set the host
+            if (githubInfo.host !== 'github.com') {
+                args.push('--hostname', githubInfo.host);
+            }
+
+            this.log(`[ISL Extensions] Executing: gh ${args.join(' ')}`);
+
+            cp.execFile(command, args, { timeout: 30000 }, (error, stdout, stderr) => {
+                if (error) {
+                    reject(new Error(`gh CLI failed: ${error.message}`));
+                    return;
+                }
+
+                if (stderr && !stdout) {
+                    reject(new Error(`gh CLI error: ${stderr}`));
+                    return;
+                }
+
+                try {
+                    // GitHub API returns base64-encoded content
+                    const base64Content = stdout.trim().replace(/"/g, '');
+                    const content = Buffer.from(base64Content, 'base64').toString('utf-8');
+                    resolve(content);
+                } catch (parseError) {
+                    reject(new Error(`Failed to decode GitHub API response: ${parseError instanceof Error ? parseError.message : String(parseError)}`));
+                }
+            });
+        });
+    }
+
+    /** Cache dir for cloned repos: ~/.isl/cache/repos/. Reused across fetches with TTL. */
+    private getRepoCacheDir(): string {
+        return path.join(os.homedir(), '.isl', 'cache', 'repos');
+    }
+
+    private getRepoCacheKey(githubInfo: { host: string; owner: string; repo: string; ref: string }): string {
+        return `${githubInfo.host}-${githubInfo.owner}-${githubInfo.repo}-${githubInfo.ref}`.replace(/[/:]/g, '-');
+    }
+
+    /**
+     * Downloads file using git checkout. Reuses cloned repos in ~/.isl/cache/repos with TTL
+     * to avoid hitting GitHub on every autocomplete.
+     */
+    private downloadViaGit(githubInfo: { host: string; owner: string; repo: string; ref: string; path: string }): Promise {
+        const cacheTTL = vscode.workspace.getConfiguration('isl').get('extensions.cacheTTL', 3600) * 1000;
+        const repoKey = this.getRepoCacheKey(githubInfo);
+        const cacheDir = path.join(this.getRepoCacheDir(), repoKey);
+        const filePath = path.join(cacheDir, githubInfo.path);
+
+        const tryReadFromCache = (): string | null => {
+            try {
+                if (!fs.existsSync(filePath)) return null;
+                const content = fs.readFileSync(filePath, 'utf-8');
+                this.log(`[ISL Extensions] Repo cache hit: ${githubInfo.path} from ${cacheDir}`);
+                return content;
+            } catch {
+                return null;
+            }
+        };
+
+        const checkCacheFresh = (): boolean => {
+            try {
+                const gitDir = path.join(cacheDir, '.git');
+                if (!fs.existsSync(gitDir)) return false;
+                const stat = fs.statSync(gitDir);
+                return Date.now() - stat.mtimeMs < cacheTTL;
+            } catch {
+                return false;
+            }
+        };
+
+        const content = tryReadFromCache();
+        if (content !== null && checkCacheFresh()) {
+            this.log(`[ISL Extensions] Using cached repo (TTL ${cacheTTL / 1000}s): ${repoKey}`);
+            return Promise.resolve(content);
+        }
+
+        if (content !== null && !checkCacheFresh()) {
+            this.log(`[ISL Extensions] Repo cache expired, re-cloning: ${repoKey}`);
+        }
+
+        return new Promise((resolve, reject) => {
+            const repoUrl = `https://${githubInfo.host}/${githubInfo.owner}/${githubInfo.repo}.git`;
+
+            const doClone = (targetDir: string) => {
+                this.log(`[ISL Extensions] Cloning ${repoUrl} to ${targetDir} (with submodules, will cache)`);
+                cp.exec(`git clone --depth 1 --recurse-submodules --branch ${githubInfo.ref} ${repoUrl} ${targetDir}`, { timeout: 120000 }, (cloneError) => {
+                    if (cloneError) {
+                        this.log(`[ISL Extensions] Clone with branch failed, trying without branch`);
+                        cp.exec(`git clone --depth 1 --recurse-submodules ${repoUrl} ${targetDir}`, { timeout: 120000 }, (cloneError2) => {
+                            if (cloneError2) {
+                                reject(new Error(`Git clone failed: ${cloneError2.message}`));
+                                return;
+                            }
+                            cp.exec(`git checkout ${githubInfo.ref}`, { cwd: targetDir, timeout: 30000 }, (checkoutError) => {
+                                if (checkoutError) {
+                                    reject(new Error(`Git checkout failed: ${checkoutError.message}`));
+                                    return;
+                                }
+                                cp.exec(`git submodule update --init --recursive`, { cwd: targetDir, timeout: 60000 }, (submoduleError) => {
+                                    if (submoduleError) this.log(`[ISL Extensions] Submodule update failed (continuing): ${submoduleError.message}`, 'warn');
+                                    this.readFromRepo(targetDir, path.join(targetDir, githubInfo.path), resolve, reject);
+                                });
+                            });
+                        });
+                        return;
+                    }
+                    cp.exec(`git submodule update --init --recursive`, { cwd: targetDir, timeout: 60000 }, (submoduleError) => {
+                        if (submoduleError) this.log(`[ISL Extensions] Submodule update failed (continuing): ${submoduleError.message}`, 'warn');
+                        this.readFromRepo(targetDir, path.join(targetDir, githubInfo.path), resolve, reject);
+                    });
+                });
+            };
+
+            if (fs.existsSync(cacheDir)) {
+                try {
+                    fs.rmSync(cacheDir, { recursive: true, force: true });
+                } catch (e) {
+                    this.log(`[ISL Extensions] Failed to clear stale cache dir: ${e}`, 'warn');
+                }
+            }
+            fs.mkdirSync(path.dirname(cacheDir), { recursive: true });
+            doClone(cacheDir);
+        });
+    }
+
+    /** Read file from repo (no cleanup - repo is cached). */
+    private readFromRepo(repoDir: string, filePath: string, resolve: (value: string) => void, reject: (error: Error) => void) {
+        try {
+            if (!fs.existsSync(filePath)) {
+                reject(new Error(`File not found in repository: ${filePath}`));
+                return;
+            }
+            const content = fs.readFileSync(filePath, 'utf-8');
+            this.log(`[ISL Extensions] Read ${filePath} from cached repo`);
+            resolve(content);
+        } catch (readError) {
+            reject(new Error(`Failed to read file: ${readError instanceof Error ? readError.message : String(readError)}`));
+        }
+    }
+
+    /**
+     * Fetches content from a URL. For GitHub URLs (github.com or github.*.com), uses
+     * gh CLI or git checkout like global extensions, then falls back to raw URL.
+     * Use this for schema imports (type X from 'url') so GitHub URLs are retrieved consistently.
+     */
+    public async fetchContentFromUrl(urlString: string): Promise {
+        if (!urlString.startsWith('http://') && !urlString.startsWith('https://')) {
+            throw new Error('URL must start with http:// or https://');
+        }
+        const githubInfo = this.parseGitHubUrl(urlString);
+        if (githubInfo) {
+            this.log(`[ISL Extensions] Fetching schema from GitHub: ${urlString}`);
+            return await this.downloadFromGitHub(githubInfo);
+        }
+        this.log(`[ISL Extensions] Fetching from URL: ${urlString}`);
+        return await this.downloadUrl(urlString);
+    }
+
+    /**
+     * Downloads content from a URL
+     */
+    private downloadUrl(urlString: string): Promise {
+        return new Promise((resolve, reject) => {
+            const client = urlString.startsWith('https://') ? https : http;
+            
+            client.get(urlString, (res) => {
+                if (res.statusCode !== 200) {
+                    reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
+                    return;
+                }
+
+                let data = '';
+                res.on('data', (chunk) => {
+                    data += chunk;
+                });
+                res.on('end', () => {
+                    resolve(data);
+                });
+            }).on('error', (err) => {
+                reject(err);
+            });
+        });
+    }
+
+    /**
+     * Loads extensions from a file path
+     */
+    private async loadFromFile(filePath: string): Promise {
+        try {
+            // Resolve ~ to home directory
+            const resolvedPath = filePath.startsWith('~') 
+                ? path.join(os.homedir(), filePath.slice(1))
+                : filePath;
+
+            // Resolve relative paths from home directory if not absolute
+            const absolutePath = path.isAbsolute(resolvedPath) 
+                ? resolvedPath 
+                : path.join(os.homedir(), resolvedPath);
+
+            if (!fs.existsSync(absolutePath)) {
+                this.log(`[ISL Extensions] Global extensions file not found: ${absolutePath}`, 'warn');
+                return null;
+            }
+
+            this.log(`[ISL Extensions] Loading extensions from file: ${absolutePath}`);
+            const content = fs.readFileSync(absolutePath, 'utf-8');
+            this.log(`[ISL Extensions] Loaded ${content.length} bytes from file`);
+            return content;
+        } catch (error) {
+            this.log(`[ISL Extensions] Failed to load extensions from file ${filePath}: ${error instanceof Error ? error.message : String(error)}`, 'error');
+            return null;
+        }
+    }
+
+    /**
+     * Parses the .islextensions file content
+     */
+    private parseExtensionsFile(content: string, filePath: string): IslExtensions {
+        const extensions = this.createEmptyExtensions();
+
+        try {
+            const data = JSON.parse(content);
+
+            // Parse functions
+            if (data.functions && Array.isArray(data.functions)) {
+                for (const func of data.functions) {
+                    if (!func.name || typeof func.name !== 'string') {
+                        this.log('[ISL Extensions] Skipping function without name in .islextensions', 'warn');
+                        continue;
+                    }
+
+                    const functionDef: IslFunctionDefinition = {
+                        name: func.name,
+                        description: func.description || func.desc || '',
+                        parameters: this.parseParameters(func.parameters || func.params || []),
+                        returns: func.returns ? {
+                            type: func.returns.type,
+                            description: func.returns.description || func.returns.desc
+                        } : undefined,
+                        examples: Array.isArray(func.examples) ? func.examples : undefined
+                    };
+
+                    extensions.functions.set(func.name, functionDef);
+                }
+            }
+
+            // Parse modifiers
+            if (data.modifiers && Array.isArray(data.modifiers)) {
+                for (const mod of data.modifiers) {
+                    if (!mod.name || typeof mod.name !== 'string') {
+                        this.log('[ISL Extensions] Skipping modifier without name in .islextensions', 'warn');
+                        continue;
+                    }
+
+                    const modifierDef: IslModifierDefinition = {
+                        name: mod.name,
+                        description: mod.description || mod.desc || '',
+                        parameters: this.parseParameters(mod.parameters || mod.params || []),
+                        returns: mod.returns ? {
+                            type: mod.returns.type,
+                            description: mod.returns.description || mod.returns.desc
+                        } : undefined,
+                        examples: Array.isArray(mod.examples) ? mod.examples : undefined
+                    };
+
+                    extensions.modifiers.set(mod.name, modifierDef);
+                }
+            }
+
+            this.log(`[ISL Extensions] Parsed ${extensions.functions.size} custom functions and ${extensions.modifiers.size} custom modifiers from ${filePath}`);
+        } catch (error) {
+            this.log(`[ISL Extensions] Error parsing .islextensions file: ${error instanceof Error ? error.message : String(error)}`, 'error');
+            throw new Error(`Failed to parse .islextensions: ${error instanceof Error ? error.message : String(error)}`);
+        }
+
+        return extensions;
+    }
+
+    /**
+     * Parses parameter definitions
+     */
+    private parseParameters(params: any[]): IslParameter[] {
+        if (!Array.isArray(params)) {
+            return [];
+        }
+
+        return params.map(param => {
+            // Support both object and string formats
+            if (typeof param === 'string') {
+                return { name: param };
+            }
+
+            return {
+                name: param.name || '',
+                type: param.type,
+                description: param.description || param.desc,
+                optional: param.optional === true,
+                defaultValue: param.default || param.defaultValue
+            };
+        });
+    }
+
+    /**
+     * Creates an empty extensions object
+     */
+    private createEmptyExtensions(): IslExtensions {
+        return {
+            functions: new Map(),
+            modifiers: new Map()
+        };
+    }
+
+    /**
+     * Preloads extensions for all workspace folders and global-only cache.
+     * Call on activation so the first completion/validation has a warm cache.
+     */
+    public async preloadExtensions(): Promise {
+        const folders = vscode.workspace.workspaceFolders;
+        if (folders?.length) {
+            for (const folder of folders) {
+                if (!this.extensionsCache.has(folder.uri.fsPath)) {
+                    const ext = await this.loadExtensionsForWorkspace(folder);
+                    this.extensionsCache.set(folder.uri.fsPath, ext);
+                }
+            }
+        }
+        const globalSource = vscode.workspace.getConfiguration('isl').get('extensions.source', '');
+        if (globalSource && !this.extensionsCache.has(IslExtensionsManager.NO_WORKSPACE_CACHE_KEY)) {
+            const ext = await this.loadExtensionsFromGlobalSourceOnly();
+            this.extensionsCache.set(IslExtensionsManager.NO_WORKSPACE_CACHE_KEY, ext);
+        }
+    }
+
+    /**
+     * Reloads all cached extensions
+     */
+    public async reloadAll(): Promise {
+        this.log('[ISL Extensions] Reloading all extensions (clearing cache)');
+        this.extensionsCache.clear();
+        this.globalExtensionsCache = null;
+        
+        // Reload for all open ISL documents
+        const islDocuments = vscode.workspace.textDocuments.filter(doc => doc.languageId === 'isl');
+        this.log(`[ISL Extensions] Reloading extensions for ${islDocuments.length} open ISL document(s)`);
+        
+        for (const document of islDocuments) {
+            await this.getExtensionsForDocument(document);
+        }
+        
+        this.log('[ISL Extensions] Finished reloading all extensions');
+    }
+
+    /**
+     * Disposes the manager and cleans up resources
+     */
+    public dispose() {
+        this.fileWatcher?.dispose();
+        this.configWatcher?.dispose();
+        this.onDidChangeEmitter.dispose();
+        this.extensionsCache.clear();
+        this.globalExtensionsCache = null;
+    }
+}
+
diff --git a/plugin/src/formatter.ts b/plugin/src/formatter.ts
new file mode 100644
index 0000000..c8fbc47
--- /dev/null
+++ b/plugin/src/formatter.ts
@@ -0,0 +1,956 @@
+import * as vscode from 'vscode';
+
+export class IslDocumentFormatter implements vscode.DocumentFormattingEditProvider, vscode.DocumentRangeFormattingEditProvider {
+    
+    provideDocumentFormattingEdits(
+        document: vscode.TextDocument,
+        options: vscode.FormattingOptions,
+        token: vscode.CancellationToken
+    ): vscode.TextEdit[] {
+        const config = vscode.workspace.getConfiguration('isl.formatting');
+        if (!config.get('enabled', true)) {
+            return [];
+        }
+
+        const fullRange = new vscode.Range(
+            document.positionAt(0),
+            document.positionAt(document.getText().length)
+        );
+        
+        const formatted = this.formatIslCode(document.getText(), options);
+        return [vscode.TextEdit.replace(fullRange, formatted)];
+    }
+
+    provideDocumentRangeFormattingEdits(
+        document: vscode.TextDocument,
+        range: vscode.Range,
+        options: vscode.FormattingOptions,
+        token: vscode.CancellationToken
+    ): vscode.TextEdit[] {
+        const config = vscode.workspace.getConfiguration('isl.formatting');
+        if (!config.get('enabled', true)) {
+            return [];
+        }
+
+        const selectionText = document.getText(range);
+        // When pasting in the middle of a line (e.g. between quotes), format-on-paste runs on that
+        // range. Using context indent would add leading spaces and make the text "jump right".
+        // Use indent 0 for single-line ranges that don't start at column 0 so inline paste stays put.
+        const isInlineRange = range.start.line === range.end.line && range.start.character > 0;
+        const contextRange = new vscode.Range(
+            document.positionAt(0),
+            range.start
+        );
+        const contextText = document.getText(contextRange);
+        const indentState = isInlineRange
+            ? { indentLevel: 0, openInlineControlFlow: [] as string[] }
+            : this.computeIndentState(contextText);
+        const formatted = this.formatIslCode(selectionText, options, indentState);
+        return [vscode.TextEdit.replace(range, formatted)];
+    }
+
+    /**
+     * Computes the indent level and open control flow state at the end of the given code.
+     * Used for range formatting to preserve context-aware indentation.
+     */
+    private computeIndentState(code: string): { indentLevel: number; openInlineControlFlow: string[] } {
+        let indentLevel = 0;
+        let openInlineControlFlow: string[] = [];
+        const lines = code.split('\n');
+
+        const isInsideMultiLineString: boolean[] = new Array(lines.length).fill(false);
+        let inMultiLineString = false;
+        let backtickCount = 0;
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            let tempBacktickCount = 0;
+            for (let j = 0; j < line.length; j++) {
+                if (line[j] === '`' && (j === 0 || line[j - 1] !== '\\')) tempBacktickCount++;
+            }
+            if (!inMultiLineString && tempBacktickCount % 2 === 1) {
+                inMultiLineString = true;
+                backtickCount = tempBacktickCount;
+            } else if (inMultiLineString) {
+                isInsideMultiLineString[i] = true;
+                backtickCount += tempBacktickCount;
+                if (backtickCount % 2 === 0) {
+                    inMultiLineString = false;
+                    backtickCount = 0;
+                }
+            }
+        }
+
+        const processedLines: string[] = [];
+        for (let i = 0; i < lines.length; i++) {
+            if (isInsideMultiLineString[i]) {
+                processedLines.push(lines[i].trimEnd());
+                continue;
+            }
+            let line = lines[i].trim();
+            line = this.normalizePipeSpacing(line);
+            line = this.normalizeFunctionParameters(line);
+            line = this.normalizeControlFlowParameters(line);
+            line = this.collapseEmptyBrackets(line);
+            line = this.normalizePropertyAssignments(line);
+            line = this.normalizeVariableAssignments(line);
+            line = this.normalizeFunctionObjectParameters(line);
+            processedLines.push(line);
+        }
+
+        for (let i = 0; i < processedLines.length; i++) {
+            const trimmedLine = processedLines[i];
+            if (isInsideMultiLineString[i]) continue;
+            if (trimmedLine.match(/^(fun|modifier)\s/)) indentLevel = 0;
+            if (trimmedLine.startsWith('//') || trimmedLine.startsWith('#')) continue;
+            if (trimmedLine === '') continue;
+            if (trimmedLine.match(/^[\}\]\)]/) && indentLevel > 0) indentLevel--;
+            else if (trimmedLine.match(/\}[;,]?\s*$/) && indentLevel > 0) indentLevel--;
+            if ((trimmedLine.match(/^[\}\]].*;$/) || trimmedLine.match(/^[\}\]],$/)) && openInlineControlFlow.length > 0) {
+                const top = openInlineControlFlow[openInlineControlFlow.length - 1];
+                if (top !== 'switch' && indentLevel > 0) {
+                    indentLevel--;
+                    openInlineControlFlow.pop();
+                }
+            }
+            if (openInlineControlFlow.length > 0 && trimmedLine.match(/\s+else\s+[^,;]+[,;]$/)) {
+                if (indentLevel > 0) {
+                    indentLevel--;
+                    openInlineControlFlow.pop();
+                }
+            }
+            if (trimmedLine.match(/^(endif|endfor|endwhile|endswitch)/)) {
+                if (indentLevel > 0) indentLevel--;
+                if (openInlineControlFlow.length > 0) openInlineControlFlow.pop();
+            }
+            if (openInlineControlFlow.length > 0 && /^\$[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*\s*[=:]/.test(trimmedLine)) {
+                if (indentLevel > 0) indentLevel--;
+                openInlineControlFlow.pop();
+            }
+            if (trimmedLine === 'else' && indentLevel > 0) {
+                const inSwitch = openInlineControlFlow.length > 0 && openInlineControlFlow[openInlineControlFlow.length - 1] === 'switch';
+                if (!inSwitch) {
+                    indentLevel--;
+                    indentLevel++;
+                }
+                continue;
+            }
+            if (trimmedLine.match(/[\{\[\(]$/)) indentLevel++;
+            else if (trimmedLine.match(/[=:]\s*\{/) || trimmedLine.match(/[=:]\s*\[/)) indentLevel++;
+            const blockControlFlow = trimmedLine.match(/^(if|foreach|while|switch|parallel)[\s(]/);
+            const inlineControlFlow = trimmedLine.match(/[=:>]\s*(if|foreach|while|switch)[\s(]/);
+            const returnSwitchBlock = !blockControlFlow && !inlineControlFlow &&
+                trimmedLine.match(/\bswitch\s*\(/) &&
+                !trimmedLine.endsWith(';') && !trimmedLine.endsWith(',') && !trimmedLine.endsWith('{');
+            if (blockControlFlow || inlineControlFlow) {
+                if (!trimmedLine.includes('endif') && !trimmedLine.includes('endfor') &&
+                    !trimmedLine.includes('endwhile') && !trimmedLine.includes('endswitch')) {
+                    const endsWithBrace = trimmedLine.endsWith('{');
+                    const endsWithTerminator = trimmedLine.endsWith(';') || trimmedLine.endsWith(',');
+                    const isCompleteLine = inlineControlFlow && endsWithTerminator;
+                    if (!endsWithBrace && !isCompleteLine) {
+                        indentLevel++;
+                        if (inlineControlFlow) {
+                            const controlFlowType = inlineControlFlow[0].match(/(if|foreach|while|switch)/)?.[1] || 'unknown';
+                            openInlineControlFlow.push(controlFlowType);
+                        }
+                    }
+                }
+            } else if (returnSwitchBlock) {
+                indentLevel++;
+                openInlineControlFlow.push('switch');
+            }
+        }
+        return { indentLevel, openInlineControlFlow };
+    }
+
+    private formatIslCode(code: string, options: vscode.FormattingOptions, initialIndentState?: { indentLevel: number; openInlineControlFlow: string[] }): string {
+        const config = vscode.workspace.getConfiguration('isl.formatting');
+        const indentSize = config.get('indentSize', 4);
+        const useTabs = config.get('useTabs', false);
+        const indentChar = useTabs ? '\t' : ' '.repeat(indentSize);
+        const alignProperties = config.get('alignProperties', false);
+
+        let formatted = '';
+        let indentLevel = initialIndentState?.indentLevel ?? 0;
+        let openInlineControlFlow: string[] = initialIndentState ? [...initialIndentState.openInlineControlFlow] : [];
+
+        const lines = code.split('\n');
+        
+        // First pass: identify lines that are inside multi-line backtick strings
+        const isInsideMultiLineString: boolean[] = new Array(lines.length).fill(false);
+        let inMultiLineString = false;
+        let backtickCount = 0;
+        
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            
+            // Count backticks in this line (excluding escaped ones)
+            let tempBacktickCount = 0;
+            for (let j = 0; j < line.length; j++) {
+                if (line[j] === '`' && (j === 0 || line[j - 1] !== '\\')) {
+                    tempBacktickCount++;
+                }
+            }
+            
+            // Track if we're entering or exiting a multi-line string
+            if (!inMultiLineString && tempBacktickCount % 2 === 1) {
+                // Started a string on this line, check if it closes on same line
+                inMultiLineString = true;
+                backtickCount = tempBacktickCount;
+            } else if (inMultiLineString) {
+                // We're inside a multi-line string
+                isInsideMultiLineString[i] = true;
+                backtickCount += tempBacktickCount;
+                
+                // If we now have an even number of backticks, the string is closed
+                if (backtickCount % 2 === 0) {
+                    inMultiLineString = false;
+                    backtickCount = 0;
+                }
+            }
+        }
+        
+        // Process lines: normalize pipe spacing but don't split chains
+        const processedLines: string[] = [];
+        for (let i = 0; i < lines.length; i++) {
+            // If this line is inside a multi-line string, preserve it as-is (only trim trailing spaces)
+            if (isInsideMultiLineString[i]) {
+                processedLines.push(lines[i].trimEnd());
+                continue;
+            }
+            
+            let line = lines[i].trim();
+            
+            // Normalize pipe spacing (ensure space after pipe)
+            line = this.normalizePipeSpacing(line);
+            
+            // Normalize function/modifier declaration parameter spacing
+            line = this.normalizeFunctionParameters(line);
+            
+            // Normalize control flow statement parameter spacing
+            line = this.normalizeControlFlowParameters(line);
+            
+            // Collapse empty objects and arrays
+            line = this.collapseEmptyBrackets(line);
+            
+            // Normalize property assignments (key: value)
+            line = this.normalizePropertyAssignments(line);
+            
+            // Normalize variable assignments (= with spaces)
+            line = this.normalizeVariableAssignments(line);
+            
+            // Normalize function calls with object parameters
+            line = this.normalizeFunctionObjectParameters(line);
+            
+            processedLines.push(line);
+        }
+
+        // Indent and format
+        let prevLineIndent = '';
+        let prevLineEndsWithColon = false;
+        for (let i = 0; i < processedLines.length; i++) {
+            const trimmedLine = processedLines[i];
+
+            // If this line is inside a multi-line string, preserve it as-is
+            if (isInsideMultiLineString[i]) {
+                formatted += trimmedLine + '\n';
+                continue;
+            }
+
+            // Reset indent to 0 for function/modifier declarations (always start at column zero)
+            if (trimmedLine.match(/^(fun|modifier)\s/)) {
+                indentLevel = 0;
+            }
+
+            // Handle line comments
+            if (trimmedLine.startsWith('//') || trimmedLine.startsWith('#')) {
+                prevLineIndent = indentChar.repeat(indentLevel);
+                prevLineEndsWithColon = false;
+                formatted += prevLineIndent + trimmedLine + '\n';
+                continue;
+            }
+
+            // Skip empty lines but preserve them
+            if (trimmedLine === '') {
+                prevLineIndent = '';
+                prevLineEndsWithColon = false;
+                formatted += '\n';
+                continue;
+            }
+
+            // Adjust indent for closing braces/brackets (line starts with } ] ) or ends with } }; },
+            if (trimmedLine.match(/^[\}\]\)]/) && indentLevel > 0) {
+                indentLevel--;
+            } else if (trimmedLine.match(/\}[;,]?\s*$/) && indentLevel > 0) {
+                // Line ends with } or }; or }, but doesn't start with } (fallback so we still close the block)
+                indentLevel--;
+            }
+            
+            // If line ends with }; or }], and we have open inline control flow,
+            // this closes the inline expression - decrease indent
+            // BUT: for switch statements, }; only closes a case, not the switch itself
+            if ((trimmedLine.match(/^[\}\]].*;$/) || trimmedLine.match(/^[\}\]],$/)) && openInlineControlFlow.length > 0) {
+                const topControlFlow = openInlineControlFlow[openInlineControlFlow.length - 1];
+                // Only close if it's NOT a switch statement
+                if (topControlFlow !== 'switch') {
+                    if (indentLevel > 0) {
+                        indentLevel--;
+                        openInlineControlFlow.pop();
+                    }
+                }
+            }
+            
+            // Check if this line completes an inline if/switch with 'else' but no explicit end keyword
+            // Pattern: ] else value, or ) else value, etc. (ends with terminator)
+            // This closes the inline control flow
+            if (openInlineControlFlow.length > 0 && trimmedLine.match(/\s+else\s+[^,;]+[,;]$/)) {
+                if (indentLevel > 0) {
+                    indentLevel--;
+                    openInlineControlFlow.pop();
+                }
+            }
+
+            // Special handling for control flow endings
+            if (trimmedLine.match(/^(endif|endfor|endwhile|endswitch)/)) {
+                if (indentLevel > 0) indentLevel--;
+                if (openInlineControlFlow.length > 0) openInlineControlFlow.pop();
+            }
+
+            // Inline if/switch used as conditional modifier don't require endif/endswitch.
+            // A new statement (e.g. $var = ... or $var: ... or endfor) closes the previous inline expression.
+            if (openInlineControlFlow.length > 0 && /^\$[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*\s*[=:]/.test(trimmedLine)) {
+                if (indentLevel > 0) indentLevel--;
+                openInlineControlFlow.pop();
+            }
+
+            // Handle 'else' - decrease then increase indent (align else with if). Skip inside switch where else is a case.
+            if (trimmedLine === 'else' && indentLevel > 0) {
+                const inSwitch = openInlineControlFlow.length > 0 && openInlineControlFlow[openInlineControlFlow.length - 1] === 'switch';
+                if (!inSwitch) {
+                    indentLevel--;
+                    prevLineIndent = indentChar.repeat(indentLevel);
+                    prevLineEndsWithColon = false;
+                    formatted += prevLineIndent + trimmedLine + '\n';
+                    indentLevel++;
+                    continue;
+                }
+            }
+
+            // Check if this line is a continuation of a modifier chain (starts with |)
+            const isModifierContinuation = trimmedLine.startsWith('|');
+            const extraIndent = isModifierContinuation ? 1 : 0;
+
+            // For standalone '{' after a property key (line ending with :), use same indent as key
+            // to prevent drift when repeatedly formatting (e.g. format-on-Enter)
+            let lineIndent = indentChar.repeat(indentLevel + extraIndent);
+            if (trimmedLine === '{' && prevLineEndsWithColon && prevLineIndent !== '') {
+                lineIndent = prevLineIndent;
+            }
+
+            // Add indentation (with extra indent for modifier continuations)
+            formatted += lineIndent + trimmedLine;
+
+            // Track for next iteration (property key with value on next line)
+            prevLineIndent = lineIndent;
+            prevLineEndsWithColon = trimmedLine.endsWith(':') && !trimmedLine.includes('?');
+
+            // Adjust indent for opening braces/brackets (at end of line or assignment-style in middle, e.g. $var: { key: value )
+            if (trimmedLine.match(/[\{\[\(]$/)) {
+                indentLevel++;
+            } else if (trimmedLine.match(/[=:]\s*\{/) || trimmedLine.match(/[=:]\s*\[/)) {
+                // Assignment/colon then { or [ on same line -> block content continues on next lines
+                indentLevel++;
+            }
+
+            // Special handling for control flow beginnings (but not fun/modifier)
+            // Check for both block statements (start of line) and inline statements (after : or = or ->)
+            // Match either space or opening paren after keyword: if( or if (
+            const blockControlFlow = trimmedLine.match(/^(if|foreach|while|switch|parallel)[\s(]/);
+            const inlineControlFlow = trimmedLine.match(/[=:>]\s*(if|foreach|while|switch)[\s(]/);
+            // "return switch ( ... )" or other leading token + switch: body and endswitch get indented
+            const returnSwitchBlock = !blockControlFlow && !inlineControlFlow &&
+                trimmedLine.match(/\bswitch\s*\(/) &&
+                !trimmedLine.endsWith(';') && !trimmedLine.endsWith(',') && !trimmedLine.endsWith('{');
+
+            if (blockControlFlow || inlineControlFlow) {
+                if (!trimmedLine.includes('endif') && !trimmedLine.includes('endfor') &&
+                    !trimmedLine.includes('endwhile') && !trimmedLine.includes('endswitch')) {
+                    // Don't increase indent if line ends with { (brace handles it)
+                    // Don't increase indent if inline expression is complete on one line (; or , at end)
+                    const endsWithBrace = trimmedLine.endsWith('{');
+                    const endsWithTerminator = trimmedLine.endsWith(';') || trimmedLine.endsWith(',');
+                    
+                    // For inline expressions: if they end with a terminator, they're complete
+                    // For block expressions: they never end with terminators, so this won't affect them
+                    const isCompleteLine = inlineControlFlow && endsWithTerminator;
+                    
+                    if (!endsWithBrace && !isCompleteLine) {
+                        indentLevel++;
+                        // Track open inline control flow type (will be closed by }; or endif)
+                        if (inlineControlFlow) {
+                            // Extract the control flow type from the match
+                            const controlFlowType = inlineControlFlow[0].match(/(if|foreach|while|switch)/)?.[1] || 'unknown';
+                            openInlineControlFlow.push(controlFlowType);
+                        }
+                    }
+                }
+            } else if (returnSwitchBlock) {
+                indentLevel++;
+                openInlineControlFlow.push('switch');
+            }
+            
+            // Note: fun/modifier declarations don't increase indent themselves
+            // The opening { (whether on same line or next line) handles the indentation
+
+            // Add newline
+            if (i < processedLines.length - 1) {
+                formatted += '\n';
+            }
+        }
+
+        // Align properties if enabled
+        if (alignProperties) {
+            formatted = this.alignObjectProperties(formatted);
+        }
+
+        return formatted;
+    }
+    
+    private normalizePipeSpacing(line: string): string {
+        // Skip comments and strings
+        if (line.trim().startsWith('//') || line.trim().startsWith('#')) {
+            return line;
+        }
+        
+        // Replace | with | (with space) but be careful with strings
+        // This regex finds pipes that are not inside strings
+        let result = '';
+        let inString = false;
+        let stringChar = '';
+        let i = 0;
+        
+        while (i < line.length) {
+            const char = line[i];
+            const nextChar = line[i + 1] || '';
+            
+            // Track string boundaries
+            if ((char === '"' || char === "'" || char === '`') && (i === 0 || line[i - 1] !== '\\')) {
+                if (!inString) {
+                    inString = true;
+                    stringChar = char;
+                } else if (char === stringChar) {
+                    inString = false;
+                    stringChar = '';
+                }
+                result += char;
+                i++;
+                continue;
+            }
+            
+            // If we find a pipe outside of strings
+            if (char === '|' && !inString) {
+                // Add pipe with space after
+                result += '| ';
+                // Skip any existing spaces after the pipe
+                while (i + 1 < line.length && line[i + 1] === ' ') {
+                    i++;
+                }
+                i++;
+                continue;
+            }
+            
+            result += char;
+            i++;
+        }
+        
+        return result;
+    }
+
+    private normalizeFunctionParameters(line: string): string {
+        // Skip comments
+        if (line.trim().startsWith('//') || line.trim().startsWith('#')) {
+            return line;
+        }
+
+        // Match function/modifier declarations: fun name(...) or modifier name(...)
+        const functionMatch = line.match(/^(fun|modifier)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\)/);
+        if (!functionMatch) {
+            return line;
+        }
+
+        const keyword = functionMatch[1];     // 'fun' or 'modifier'
+        const name = functionMatch[2];        // function name
+        const params = functionMatch[3];      // parameters inside ()
+        const afterParams = line.substring(functionMatch[0].length); // everything after )
+
+        // Trim the parameters
+        const trimmedParams = params.trim();
+
+        // If no parameters, no spaces
+        if (trimmedParams === '') {
+            return `${keyword} ${name}()${afterParams}`;
+        }
+
+        // If there are parameters, add spaces after ( and before )
+        // Also normalize spaces around commas
+        const normalizedParams = trimmedParams
+            .split(',')
+            .map(p => p.trim())
+            .join(', ');
+
+        return `${keyword} ${name}( ${normalizedParams} )${afterParams}`;
+    }
+
+    private normalizeControlFlowParameters(line: string): string {
+        // Skip comments
+        if (line.trim().startsWith('//') || line.trim().startsWith('#')) {
+            return line;
+        }
+
+        // Normalize if, switch, while, foreach statements
+        // Pattern: keyword( condition ) or keyword ( condition )
+        const keywords = ['if', 'switch', 'while'];
+        
+        for (const keyword of keywords) {
+            // Match keyword followed by parentheses
+            const regex = new RegExp(`\\b(${keyword})\\s*\\(([^)]*)\\)`, 'g');
+            line = line.replace(regex, (match, kw, content) => {
+                const trimmedContent = content.trim();
+                // If no content, no spaces: if()
+                if (trimmedContent === '') {
+                    return `${kw}()`;
+                }
+                // Otherwise add spaces: if ( condition )
+                return `${kw} ( ${trimmedContent} )`;
+            });
+        }
+
+        return line;
+    }
+    
+    private collapseEmptyBrackets(line: string): string {
+        // Skip comments
+        if (line.trim().startsWith('//') || line.trim().startsWith('#')) {
+            return line;
+        }
+        
+        let result = '';
+        let inString = false;
+        let stringChar = '';
+        let i = 0;
+        
+        while (i < line.length) {
+            const char = line[i];
+            
+            // Track string boundaries
+            if ((char === '"' || char === "'" || char === '`') && (i === 0 || line[i - 1] !== '\\')) {
+                if (!inString) {
+                    inString = true;
+                    stringChar = char;
+                } else if (char === stringChar) {
+                    inString = false;
+                    stringChar = '';
+                }
+                result += char;
+                i++;
+                continue;
+            }
+            
+            // If we're in a string, just copy the character
+            if (inString) {
+                result += char;
+                i++;
+                continue;
+            }
+            
+            // Check for empty objects or arrays
+            if (char === '{' || char === '[') {
+                const closingChar = char === '{' ? '}' : ']';
+                let j = i + 1;
+                
+                // Skip whitespace
+                while (j < line.length && (line[j] === ' ' || line[j] === '\t')) {
+                    j++;
+                }
+                
+                // Check if next non-whitespace char is the closing bracket
+                if (j < line.length && line[j] === closingChar) {
+                    // Collapse: output {}, []
+                    result += char + closingChar;
+                    i = j + 1;
+                    continue;
+                }
+            }
+            
+            result += char;
+            i++;
+        }
+        
+        return result;
+    }
+    
+    private alignObjectProperties(code: string): string {
+        const lines = code.split('\n');
+        const result: string[] = [];
+        let inObject = false;
+        let objectLines: { indent: string, key: string, value: string, fullLine: string }[] = [];
+        
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            const trimmed = line.trim();
+            
+            // Detect object start
+            if (trimmed === '{' || trimmed.endsWith('{')) {
+                if (objectLines.length > 0) {
+                    // Flush previous object
+                    result.push(...this.alignLines(objectLines));
+                    objectLines = [];
+                }
+                result.push(line);
+                inObject = true;
+                continue;
+            }
+            
+            // Detect object end
+            if (trimmed === '}' || trimmed.startsWith('}')) {
+                if (objectLines.length > 0) {
+                    result.push(...this.alignLines(objectLines));
+                    objectLines = [];
+                }
+                result.push(line);
+                inObject = false;
+                continue;
+            }
+            
+            // Collect object properties
+            if (inObject && trimmed.match(/^[a-zA-Z_`\$"][^:]*:\s*.+/)) {
+                const match = line.match(/^(\s*)([a-zA-Z_`\$"][^:]*):\s*(.+)/);
+                if (match) {
+                    objectLines.push({
+                        indent: match[1],
+                        key: match[2],
+                        value: match[3],
+                        fullLine: line
+                    });
+                    continue;
+                }
+            }
+            
+            // Not an object property - flush and add
+            if (objectLines.length > 0) {
+                result.push(...this.alignLines(objectLines));
+                objectLines = [];
+            }
+            result.push(line);
+        }
+        
+        // Flush remaining
+        if (objectLines.length > 0) {
+            result.push(...this.alignLines(objectLines));
+        }
+        
+        return result.join('\n');
+    }
+    
+    private alignLines(lines: { indent: string, key: string, value: string }[]): string[] {
+        if (lines.length === 0) {
+            return [];
+        }
+        
+        // Find max key length
+        const maxKeyLength = Math.max(...lines.map(l => l.key.length));
+        
+        // Align all lines
+        return lines.map(l => {
+            const padding = ' '.repeat(maxKeyLength - l.key.length);
+            return `${l.indent}${l.key}:${padding} ${l.value}`;
+        });
+    }
+    
+    /**
+     * Rule 1: For property assignments using :, colon should be next to property with one space before value
+     * Example: name: "value" (not name : "value" or name:"value")
+     */
+    private normalizePropertyAssignments(line: string): string {
+        // Skip comments
+        if (line.trim().startsWith('//') || line.trim().startsWith('#')) {
+            return line;
+        }
+        
+        let result = '';
+        let inString = false;
+        let stringChar = '';
+        let i = 0;
+        
+        while (i < line.length) {
+            const char = line[i];
+            
+            // Track string boundaries
+            if ((char === '"' || char === "'" || char === '`') && (i === 0 || line[i - 1] !== '\\')) {
+                if (!inString) {
+                    inString = true;
+                    stringChar = char;
+                } else if (char === stringChar) {
+                    inString = false;
+                    stringChar = '';
+                }
+                result += char;
+                i++;
+                continue;
+            }
+            
+            // If we're in a string, just copy the character
+            if (inString) {
+                result += char;
+                i++;
+                continue;
+            }
+            
+            // Look for : used in property assignments
+            if (char === ':') {
+                // Check if this looks like a property assignment (not a ternary operator)
+                // Property assignments have an identifier before the colon
+                let j = result.length - 1;
+                
+                // Skip trailing spaces before colon
+                while (j >= 0 && result[j] === ' ') {
+                    result = result.substring(0, j);
+                    j--;
+                }
+                
+                // Check if there's an identifier before (property name)
+                const beforeColon = result.match(/[a-zA-Z_$][a-zA-Z0-9_.]*$/);
+                if (beforeColon) {
+                    const trimmedResult = result.trimEnd();
+                    // Type name context: "type idx" / "type ns.idx" or "$var : idx" / "$var: idx" - colon is inside type name (e.g. idx:name)
+                    // Do not add space so we keep type names as one word: idx:name not idx: name
+                    const isTypeNameContext = /^type\s+[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmedResult) ||
+                        /^\$[a-zA-Z_][a-zA-Z0-9_]*\s*:\s*[a-zA-Z_][a-zA-Z0-9_.:]*$/.test(trimmedResult);
+                    if (isTypeNameContext) {
+                        result += ':';
+                        i++;
+                        // Skip any spaces after colon, then copy the rest of the type name (e.g. name or ns:name)
+                        while (i < line.length && line[i] === ' ') {
+                            i++;
+                        }
+                        while (i < line.length) {
+                            const c = line[i];
+                            if (/[a-zA-Z0-9_]/.test(c) || c === '.' || (c === ':' && result.length > 0 && result[result.length - 1] !== ':')) {
+                                result += c;
+                                i++;
+                            } else {
+                                break;
+                            }
+                        }
+                        continue;
+                    }
+                    // This looks like a property assignment
+                    // Add colon without space, then ensure one space after
+                    result += ': ';
+                    i++;
+                    
+                    // Skip any existing spaces after the colon
+                    while (i < line.length && line[i] === ' ') {
+                        i++;
+                    }
+                    continue;
+                }
+            }
+            
+            result += char;
+            i++;
+        }
+        
+        return result;
+    }
+    
+    /**
+     * Rule 2: For variable assignments using =, ensure spaces on both sides
+     * Example: $var = value (not $var=value or $var =value)
+     */
+    private normalizeVariableAssignments(line: string): string {
+        // Skip comments
+        if (line.trim().startsWith('//') || line.trim().startsWith('#')) {
+            return line;
+        }
+        
+        let result = '';
+        let inString = false;
+        let stringChar = '';
+        let i = 0;
+        
+        while (i < line.length) {
+            const char = line[i];
+            const nextChar = line[i + 1] || '';
+            
+            // Track string boundaries
+            if ((char === '"' || char === "'" || char === '`') && (i === 0 || line[i - 1] !== '\\')) {
+                if (!inString) {
+                    inString = true;
+                    stringChar = char;
+                } else if (char === stringChar) {
+                    inString = false;
+                    stringChar = '';
+                }
+                result += char;
+                i++;
+                continue;
+            }
+            
+            // If we're in a string, just copy the character
+            if (inString) {
+                result += char;
+                i++;
+                continue;
+            }
+            
+            // Look for = (but not ==, !=, >=, <=, =>)
+            if (char === '=' && nextChar !== '=' && (i === 0 || line[i - 1] !== '!' && line[i - 1] !== '>' && line[i - 1] !== '<' && line[i - 1] !== '=')) {
+                // Not an arrow function (=>)
+                if (nextChar !== '>') {
+                    // Remove trailing spaces before =
+                    while (result.length > 0 && result[result.length - 1] === ' ') {
+                        result = result.substring(0, result.length - 1);
+                    }
+                    
+                    // Add = with spaces on both sides
+                    result += ' = ';
+                    i++;
+                    
+                    // Skip any existing spaces after =
+                    while (i < line.length && line[i] === ' ') {
+                        i++;
+                    }
+                    continue;
+                }
+            }
+            
+            result += char;
+            i++;
+        }
+        
+        return result;
+    }
+    
+    /**
+     * Rule 3: For function calls with object {} as first parameter, keep braces next to parentheses
+     * Example: @.source.name({ key: value }) (not @.source.name( { key: value } ))
+     */
+    private normalizeFunctionObjectParameters(line: string): string {
+        // Skip comments
+        if (line.trim().startsWith('//') || line.trim().startsWith('#')) {
+            return line;
+        }
+        
+        let result = '';
+        let inString = false;
+        let stringChar = '';
+        let i = 0;
+        
+        while (i < line.length) {
+            const char = line[i];
+            
+            // Track string boundaries
+            if ((char === '"' || char === "'" || char === '`') && (i === 0 || line[i - 1] !== '\\')) {
+                if (!inString) {
+                    inString = true;
+                    stringChar = char;
+                } else if (char === stringChar) {
+                    inString = false;
+                    stringChar = '';
+                }
+                result += char;
+                i++;
+                continue;
+            }
+            
+            // If we're in a string, just copy the character
+            if (inString) {
+                result += char;
+                i++;
+                continue;
+            }
+            
+            // Look for opening parenthesis that's part of a function call
+            if (char === '(' && i > 0) {
+                // Check if this is preceded by an identifier (function name)
+                let j = i - 1;
+                while (j >= 0 && line[j] === ' ') {
+                    j--;
+                }
+                
+                const beforeParen = line.substring(0, j + 1);
+                const isAfterIdentifier = /[a-zA-Z_$\.]$/.test(beforeParen);
+                
+                if (isAfterIdentifier) {
+                    result += '(';
+                    i++;
+                    
+                    // Temporarily skip spaces to check what's next
+                    let tempI = i;
+                    while (tempI < line.length && line[tempI] === ' ') {
+                        tempI++;
+                    }
+                    
+                    // Check if the first parameter is an object {
+                    if (tempI < line.length && line[tempI] === '{') {
+                        // Skip the spaces (move i forward)
+                        i = tempI;
+                        // Find matching closing }
+                        let depth = 1;
+                        let k = i + 1;
+                        let tempInString = false;
+                        let tempStringChar = '';
+                        
+                        while (k < line.length && depth > 0) {
+                            const c = line[k];
+                            
+                            // Track strings
+                            if ((c === '"' || c === "'" || c === '`') && (k === 0 || line[k - 1] !== '\\')) {
+                                if (!tempInString) {
+                                    tempInString = true;
+                                    tempStringChar = c;
+                                } else if (c === tempStringChar) {
+                                    tempInString = false;
+                                    tempStringChar = '';
+                                }
+                            }
+                            
+                            if (!tempInString) {
+                                if (c === '{') depth++;
+                                if (c === '}') depth--;
+                            }
+                            k++;
+                        }
+                        
+                        if (depth === 0) {
+                            // Extract the object content
+                            const objectContent = line.substring(i, k);
+                            result += objectContent;
+                            i = k;
+                            
+                            // Skip spaces after }
+                            while (i < line.length && line[i] === ' ') {
+                                i++;
+                            }
+                            
+                            // If next char is ), add it without space
+                            if (i < line.length && line[i] === ')') {
+                                result += ')';
+                                i++;
+                            }
+                            continue;
+                        }
+                    }
+                    continue;
+                }
+            }
+            
+            result += char;
+            i++;
+        }
+        
+        return result;
+    }
+}
+
diff --git a/plugin/src/highlights.ts b/plugin/src/highlights.ts
new file mode 100644
index 0000000..6494c7a
--- /dev/null
+++ b/plugin/src/highlights.ts
@@ -0,0 +1,224 @@
+import * as vscode from 'vscode';
+import { 
+    findMatchingControlFlowKeyword, 
+    CONTROL_FLOW_PAIRS,
+    stripComments,
+    findAllControlFlowKeywordsInLine
+} from './controlFlowMatcher';
+
+export class IslDocumentHighlightProvider implements vscode.DocumentHighlightProvider {
+    
+    provideDocumentHighlights(
+        document: vscode.TextDocument,
+        position: vscode.Position,
+        token: vscode.CancellationToken
+    ): vscode.DocumentHighlight[] | undefined {
+        const wordRange = document.getWordRangeAtPosition(position);
+        if (!wordRange) {
+            return undefined;
+        }
+
+        const word = document.getText(wordRange);
+
+        // Check if the word is a control flow keyword
+        const controlFlowPairs: { [key: string]: { match: string, isOpening: boolean } } = {
+            'if': { match: 'endif', isOpening: true },
+            'endif': { match: 'if', isOpening: false },
+            'else': { match: 'if', isOpening: false }, // else matches with if
+            'foreach': { match: 'endfor', isOpening: true },
+            'endfor': { match: 'foreach', isOpening: false },
+            'while': { match: 'endwhile', isOpening: true },
+            'endwhile': { match: 'while', isOpening: false },
+            'switch': { match: 'endswitch', isOpening: true },
+            'endswitch': { match: 'switch', isOpening: false }
+        };
+
+        if (!controlFlowPairs[word]) {
+            return undefined;
+        }
+
+        const { match: matchingKeyword, isOpening } = controlFlowPairs[word];
+        
+        // Special handling for if/else/endif - highlight all three
+        if (word === 'if' || word === 'else' || word === 'endif') {
+            return this.findIfElseEndifHighlights(document, position.line, word);
+        }
+        
+        // Find the matching keyword for other control flow
+        const matchPosition = this.findMatchingKeyword(
+            document,
+            position.line,
+            word,
+            matchingKeyword,
+            isOpening
+        );
+
+        if (!matchPosition) {
+            return undefined;
+        }
+
+        // Return highlights for both keywords
+        return [
+            new vscode.DocumentHighlight(wordRange, vscode.DocumentHighlightKind.Text),
+            new vscode.DocumentHighlight(
+                new vscode.Range(
+                    matchPosition,
+                    new vscode.Position(matchPosition.line, matchPosition.character + matchingKeyword.length)
+                ),
+                vscode.DocumentHighlightKind.Text
+            )
+        ];
+    }
+
+    private findMatchingKeyword(
+        document: vscode.TextDocument,
+        startLine: number,
+        keyword: string,
+        matchKeyword: string,
+        searchForward: boolean
+    ): vscode.Position | undefined {
+        // Get the current position of the keyword on the start line
+        const startLineText = document.lineAt(startLine).text;
+        const codeLine = stripComments(startLineText);
+        const keywordRegex = new RegExp(`\\b${keyword}\\b`, 'g');
+        let match;
+        let startColumn = -1;
+        
+        // Find the keyword position on the start line
+        while ((match = keywordRegex.exec(codeLine)) !== null) {
+            startColumn = match.index;
+            break; // Use the first match
+        }
+        
+        if (startColumn === -1) {
+            return undefined;
+        }
+        
+        // Use the shared utility to find the matching keyword
+        // This properly handles multi-line statements and nested structures
+        return findMatchingControlFlowKeyword(document, startLine, startColumn, keyword);
+    }
+
+    private findIfElseEndifHighlights(
+        document: vscode.TextDocument,
+        startLine: number,
+        word: string
+    ): vscode.DocumentHighlight[] | undefined {
+        const text = document.getText();
+        const lines = text.split('\n');
+        const highlights: vscode.DocumentHighlight[] = [];
+        let ifPosition: vscode.Position | undefined;
+
+        // Find the 'if' position
+        if (word === 'else' || word === 'endif') {
+            // Search backwards to find the matching 'if'
+            const startLineText = document.lineAt(startLine).text;
+            const codeLine = stripComments(startLineText);
+            
+            // Get the position of 'else' or 'endif' on the current line
+            let currentColumn = -1;
+            if (word === 'else') {
+                const elseMatch = codeLine.match(/\belse\b/);
+                if (elseMatch) {
+                    currentColumn = elseMatch.index!;
+                }
+            } else {
+                const endifMatch = codeLine.match(/\bendif\b/);
+                if (endifMatch) {
+                    currentColumn = endifMatch.index!;
+                }
+            }
+            
+            if (currentColumn === -1) {
+                return undefined;
+            }
+            
+            // Use the shared utility to find the matching 'if'
+            ifPosition = findMatchingControlFlowKeyword(document, startLine, currentColumn, word);
+        } else {
+            // We're on 'if', use current position
+            const currentLine = lines[startLine];
+            const ifMatch = currentLine.match(/\bif[\s(]/);
+            if (ifMatch) {
+                ifPosition = new vscode.Position(startLine, ifMatch.index!);
+            } else {
+                return undefined;
+            }
+        }
+
+        if (!ifPosition) {
+            return undefined;
+        }
+
+        // Now search forward from the 'if' position to find 'else' and 'endif'
+        // Use depth tracking to handle nested if statements
+        let depth = 0;
+        let elsePosition: vscode.Position | undefined;
+        let endifPosition: vscode.Position | undefined;
+        const pair = CONTROL_FLOW_PAIRS.if;
+
+        for (let i = ifPosition.line; i < lines.length; i++) {
+            const line = lines[i];
+            const codeLine = stripComments(line);
+
+            // Find all if keywords (increase depth)
+            const ifMatches = Array.from(codeLine.matchAll(pair.startPattern));
+            for (const match of ifMatches) {
+                // Skip the initial 'if' position
+                if (i === ifPosition.line && match.index === ifPosition.character) {
+                    continue;
+                }
+                depth++;
+            }
+
+            // Look for 'else' at depth 0 (same level as the initial if)
+            if (depth === 0 && !elsePosition) {
+                const elseMatch = codeLine.match(/\belse\b/);
+                if (elseMatch && i !== ifPosition.line) {
+                    elsePosition = new vscode.Position(i, elseMatch.index!);
+                }
+            }
+
+            // Count endif keywords (decrease depth)
+            const endifMatch = codeLine.match(pair.endPattern);
+            if (endifMatch) {
+                if (depth === 0) {
+                    endifPosition = new vscode.Position(i, endifMatch.index!);
+                    break;
+                }
+                depth--;
+            }
+        }
+
+        // Add highlights
+        if (ifPosition) {
+            highlights.push(
+                new vscode.DocumentHighlight(
+                    new vscode.Range(ifPosition, new vscode.Position(ifPosition.line, ifPosition.character + 2)),
+                    vscode.DocumentHighlightKind.Text
+                )
+            );
+        }
+
+        if (elsePosition) {
+            highlights.push(
+                new vscode.DocumentHighlight(
+                    new vscode.Range(elsePosition, new vscode.Position(elsePosition.line, elsePosition.character + 4)),
+                    vscode.DocumentHighlightKind.Text
+                )
+            );
+        }
+
+        if (endifPosition) {
+            highlights.push(
+                new vscode.DocumentHighlight(
+                    new vscode.Range(endifPosition, new vscode.Position(endifPosition.line, endifPosition.character + 5)),
+                    vscode.DocumentHighlightKind.Text
+                )
+            );
+        }
+
+        return highlights.length > 0 ? highlights : undefined;
+    }
+}
+
diff --git a/plugin/src/hover.ts b/plugin/src/hover.ts
new file mode 100644
index 0000000..1a3154f
--- /dev/null
+++ b/plugin/src/hover.ts
@@ -0,0 +1,424 @@
+import * as vscode from 'vscode';
+import { IslExtensionsManager, getExtensionFunction, getExtensionModifier } from './extensions';
+import { getModifiersMap, getServicesMap, type BuiltInModifier } from './language';
+import { IslTypeManager } from './types';
+import type { SchemaInfo, SchemaProperty, TypeDeclaration } from './types';
+
+export class IslHoverProvider implements vscode.HoverProvider {
+
+    constructor(
+        private extensionsManager: IslExtensionsManager,
+        private typeManager?: IslTypeManager
+    ) {}
+    
+    async provideHover(
+        document: vscode.TextDocument,
+        position: vscode.Position,
+        token: vscode.CancellationToken
+    ): Promise {
+        const range = document.getWordRangeAtPosition(position);
+        if (!range) {
+            return undefined;
+        }
+
+        let word = document.getText(range);
+        const line = document.lineAt(position.line).text;
+        const offset = document.offsetAt(position);
+
+        // Load custom extensions
+        const extensions = await this.extensionsManager.getExtensionsForDocument(document);
+
+        // If we're in an @.identifier context, resolve the exact identifier at cursor (more reliable than word).
+        // Use dotted pattern so @.Call.Api() resolves to "Call.Api" when hovering over Call or Api.
+        const lineStartOffset = document.offsetAt(new vscode.Position(position.line, 0));
+        const col = offset - lineStartOffset;
+        const atDotIdentifierPattern = /@\.\s*([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)/g;
+        for (const m of line.matchAll(atDotIdentifierPattern)) {
+            const start = m.index! + m[0].indexOf(m[1]);
+            const end = start + m[1].length;
+            if (col >= start && col <= end) {
+                word = m[1];
+                break;
+            }
+        }
+
+        // Check what kind of token we're hovering over
+        if (this.isKeyword(word)) {
+            return this.getKeywordHover(word);
+        }
+        // @.word or @.Module.func – built-in service (Date, Math) or global extension (sendEmail, Call.Api, …)
+        if (line.includes('@.' + word) || line.includes('@. ' + word)) {
+            const extFunc = getExtensionFunction(extensions, word);
+            if (extFunc) {
+                return this.getCustomFunctionHover(extFunc);
+            }
+            // For compound names (e.g. Date.Now), show service hover only for the first part
+            const serviceWord = word.includes('.') ? word.split('.')[0] : word;
+            return this.getServiceHover(serviceWord);
+        }
+        if (this.isModifier(word, line)) {
+            const extMod = getExtensionModifier(extensions, word);
+            if (extMod) {
+                return this.getCustomModifierHover(extMod);
+            }
+            return this.getModifierHover(word, line);
+        }
+        if (line.includes('$' + word)) {
+            return this.getVariableHover(word, document);
+        }
+        // @.This.word – same-file function
+        if (line.includes('@.This.' + word)) {
+            const extFunc = getExtensionFunction(extensions, word);
+            if (extFunc) {
+                return this.getCustomFunctionHover(extFunc);
+            }
+        }
+
+        // Type name (e.g. idx:name in "type idx:name from '...'" or "$var: idx:name = { ... }")
+        const typeAtPos = this.getTypeNameAtPosition(line, col);
+        if (typeAtPos && this.typeManager) {
+            const decl = this.typeManager.getDeclarationForType(document, typeAtPos.typeName);
+            if (decl) {
+                const schema = await this.typeManager.getSchemaForType(document, typeAtPos.typeName);
+                const hoverRange = new vscode.Range(
+                    position.line, typeAtPos.start,
+                    position.line, typeAtPos.end
+                );
+                return this.getTypeHover(decl, schema, typeAtPos.typeName, hoverRange);
+            }
+        }
+
+        // Property name inside typed object literal ($var : Type = { propName: ... })
+        if (this.typeManager && range) {
+            const schemaAt = await this.typeManager.getSchemaForObjectAt(document, position);
+            if (schemaAt) {
+                const prop = schemaAt.schema.properties[word];
+                if (prop) {
+                    return this.getPropertyHover(word, prop, schemaAt.schema, range);
+                }
+            }
+        }
+
+        return undefined;
+    }
+
+    /**
+     * Finds the full type name at the given column (e.g. idx:name) if the cursor is inside one.
+     * Type names appear after "type " or after ": " in variable declarations.
+     */
+    private getTypeNameAtPosition(line: string, col: number): { typeName: string; start: number; end: number } | null {
+        // After "type " (declaration)
+        const typeDeclRegex = /\btype\s+([a-zA-Z_][a-zA-Z0-9_.:]*)\s+(?:from|as)\b/g;
+        let m: RegExpExecArray | null;
+        while ((m = typeDeclRegex.exec(line)) !== null) {
+            const start = m.index + m[0].indexOf(m[1]);
+            const end = start + m[1].length;
+            if (col >= start && col <= end) {
+                return { typeName: m[1], start, end };
+            }
+        }
+        // After ": " (type annotation, e.g. $var: idx:name = or $var: idx:name)
+        const typeAnnotRegex = /:\s*([a-zA-Z_][a-zA-Z0-9_.:]*)(?:\s*[={]|\s*$)/g;
+        while ((m = typeAnnotRegex.exec(line)) !== null) {
+            const start = m.index + m[0].indexOf(m[1]);
+            const end = start + m[1].length;
+            if (col >= start && col <= end) {
+                return { typeName: m[1], start, end };
+            }
+        }
+        return null;
+    }
+
+    private getPropertyHover(propName: string, prop: SchemaProperty, schema: SchemaInfo, range: vscode.Range): vscode.Hover {
+        const md = new vscode.MarkdownString();
+        md.isTrusted = true;
+        const typeStr = prop.type ?? 'any';
+        const required = schema.required?.includes(propName) ? ' *(required)*' : '';
+        md.appendMarkdown(`**\`${propName}\`** \`${typeStr}\`${required}\n\n`);
+
+        if (prop.description) {
+            md.appendMarkdown(`${prop.description}\n\n`);
+        }
+
+        if (prop.enum && prop.enum.length > 0) {
+            md.appendMarkdown(`**Allowed values:** \`${prop.enum.join('`, `')}\`\n\n`);
+        }
+
+        const examples: string[] = [];
+        if (prop.example) examples.push(prop.example);
+        if (prop.examples) examples.push(...prop.examples);
+        if (examples.length > 0) {
+            md.appendMarkdown('**Examples:**\n');
+            for (const ex of examples) {
+                md.appendMarkdown('```\n' + ex + '\n```\n');
+            }
+            md.appendMarkdown('\n');
+        }
+
+        if (prop.extensions && Object.keys(prop.extensions).length > 0) {
+            md.appendMarkdown('**Extensions:**\n');
+            for (const [k, v] of Object.entries(prop.extensions)) {
+                const valStr = typeof v === 'object' && v !== null
+                    ? JSON.stringify(v, null, 2)
+                    : String(v);
+                if (valStr.includes('\n')) {
+                    md.appendMarkdown(`- \`${k}\`:\n\`\`\`\n${valStr}\n\`\`\`\n`);
+                } else {
+                    md.appendMarkdown(`- \`${k}\`: ${valStr}\n`);
+                }
+            }
+        }
+
+        return new vscode.Hover(md, range);
+    }
+
+    private getTypeHover(decl: TypeDeclaration, schema: SchemaInfo | null, typeName: string, range: vscode.Range): vscode.Hover {
+        const md = new vscode.MarkdownString();
+        md.isTrusted = true;
+        md.appendMarkdown(`**\`${typeName}\`** *(type)*\n\n`);
+        if (decl.source === 'url' && decl.url) {
+            md.appendMarkdown(`**From:** [${decl.url}](${decl.url})\n\n`);
+        } else {
+            md.appendMarkdown('**From:** inline definition\n\n');
+        }
+        if (schema && Object.keys(schema.properties).length > 0) {
+            md.appendMarkdown('**Properties:**\n');
+            for (const [propName, prop] of Object.entries(schema.properties)) {
+                const typeStr = prop.type ?? 'any';
+                const required = schema.required?.includes(propName) ? ' *(required)*' : '';
+                md.appendMarkdown(`- \`${propName}\`: \`${typeStr}\`${required}\n`);
+            }
+        } else if (schema) {
+            md.appendMarkdown('*No properties.*\n');
+        }
+        return new vscode.Hover(md, range);
+    }
+
+    private getCustomFunctionHover(func: import('./extensions').IslFunctionDefinition): vscode.Hover {
+        const md = new vscode.MarkdownString();
+        md.isTrusted = true;
+        
+        // Build signature
+        const params = func.parameters.map(p => {
+            let result = `${p.name}`;
+            if (p.type) {
+                result += `: ${p.type}`;
+            }
+            if (p.optional) {
+                result += '?';
+            }
+            return result;
+        }).join(', ');
+        
+        const returnType = func.returns?.type ? `: ${func.returns.type}` : '';
+        md.appendMarkdown(`**\`${func.name}(${params})${returnType}\`** *(custom function)*\n\n`);
+        
+        if (func.description) {
+            md.appendMarkdown(`${func.description}\n\n`);
+        }
+        
+        if (func.parameters.length > 0) {
+            md.appendMarkdown('**Parameters:**\n');
+            for (const param of func.parameters) {
+                const optional = param.optional ? ' (optional)' : '';
+                const type = param.type ? `: ${param.type}` : '';
+                const desc = param.description ? ` - ${param.description}` : '';
+                md.appendMarkdown(`- \`${param.name}${type}\`${optional}${desc}\n`);
+            }
+            md.appendMarkdown('\n');
+        }
+        
+        if (func.returns) {
+            md.appendMarkdown('**Returns:**');
+            if (func.returns.type) {
+                md.appendMarkdown(` \`${func.returns.type}\``);
+            }
+            if (func.returns.description) {
+                md.appendMarkdown(` - ${func.returns.description}`);
+            }
+            md.appendMarkdown('\n\n');
+        }
+        
+        if (func.examples && func.examples.length > 0) {
+            md.appendMarkdown('**Examples:**\n');
+            for (const example of func.examples) {
+                md.appendMarkdown('```isl\n' + example + '\n```\n');
+            }
+        }
+        
+        md.appendMarkdown('\n---\n*Defined in .islextensions*');
+        
+        return new vscode.Hover(md);
+    }
+
+    private getCustomModifierHover(mod: import('./extensions').IslModifierDefinition): vscode.Hover {
+        const md = new vscode.MarkdownString();
+        md.isTrusted = true;
+        
+        // Build signature
+        if (mod.parameters.length > 0) {
+            const params = mod.parameters.map(p => {
+                let result = `${p.name}`;
+                if (p.type) {
+                    result += `: ${p.type}`;
+                }
+                if (p.optional) {
+                    result += '?';
+                }
+                return result;
+            }).join(', ');
+            
+            const returnType = mod.returns?.type ? `: ${mod.returns.type}` : '';
+            md.appendMarkdown(`**\`${mod.name}(${params})${returnType}\`** *(custom modifier)*\n\n`);
+        } else {
+            md.appendMarkdown(`**\`${mod.name}\`** *(custom modifier)*\n\n`);
+        }
+        
+        if (mod.description) {
+            md.appendMarkdown(`${mod.description}\n\n`);
+        }
+        
+        if (mod.parameters.length > 0) {
+            md.appendMarkdown('**Parameters:**\n');
+            for (const param of mod.parameters) {
+                const optional = param.optional ? ' (optional)' : '';
+                const type = param.type ? `: ${param.type}` : '';
+                const desc = param.description ? ` - ${param.description}` : '';
+                const defaultVal = param.defaultValue ? ` (default: ${param.defaultValue})` : '';
+                md.appendMarkdown(`- \`${param.name}${type}\`${optional}${defaultVal}${desc}\n`);
+            }
+            md.appendMarkdown('\n');
+        }
+        
+        if (mod.returns) {
+            md.appendMarkdown('**Returns:**');
+            if (mod.returns.type) {
+                md.appendMarkdown(` \`${mod.returns.type}\``);
+            }
+            if (mod.returns.description) {
+                md.appendMarkdown(` - ${mod.returns.description}`);
+            }
+            md.appendMarkdown('\n\n');
+        }
+        
+        if (mod.examples && mod.examples.length > 0) {
+            md.appendMarkdown('**Examples:**\n');
+            for (const example of mod.examples) {
+                md.appendMarkdown('```isl\n' + example + '\n```\n');
+            }
+        }
+        
+        md.appendMarkdown('\n---\n*Defined in .islextensions*');
+        
+        return new vscode.Hover(md);
+    }
+
+    private isKeyword(word: string): boolean {
+        const keywords = ['fun', 'modifier', 'if', 'else', 'endif', 'foreach', 'endfor', 'while', 'endwhile', 
+                         'switch', 'endswitch', 'return', 'import', 'type', 'as', 'from', 'in', 'cache', 'parallel',
+                         'filter', 'map', 'and', 'or', 'not', 'contains', 'startsWith', 'endsWith', 'matches', 'is'];
+        return keywords.includes(word);
+    }
+
+    private isModifier(word: string, line: string): boolean {
+        return line.includes('|') && (line.includes(word + '(') || line.includes(word + ' ') || 
+               line.includes('| ' + word) || line.includes('|' + word) || 
+               new RegExp(`\\|\\s*${word}\\b`).test(line));
+    }
+
+    private getKeywordHover(word: string): vscode.Hover {
+        const docs: { [key: string]: string } = {
+            'fun': '**Function Declaration**\n\nDefines a function that can be called within ISL.\n\n```isl\nfun myFunction($param) {\n    return $param | upperCase\n}\n\n// Call it:\n$result: @.This.myFunction($value);\n```',
+            'modifier': '**Modifier Function**\n\nDefines a custom modifier that can be used with the pipe operator.\n\n```isl\nmodifier double($value) {\n    return {{ $value * 2 }}\n}\n\n// Use it:\n$result: $input | double;\n```',
+            'if': '**If Statement**\n\nConditional execution based on a boolean expression.\n\n**Block form** (requires `endif`):\n```isl\nif ($value > 10)\n    result: "high"\nelse\n    result: "low"\nendif\n```\n\n**Inline / conditional modifier** (optional `endif` when used in assignment or property):\n```isl\n$status: if ($active) "active" else "inactive";\n$val: if ($ok) $a else $b;\n{ prop: if ($x) "yes" else "no" }\n```',
+            'foreach': '**ForEach Loop**\n\nIterates over an array and transforms each element.\n\n```isl\nforeach $item in $array\n    { id: $item.id, name: $item.name }\nendfor\n```\n\n**With filtering:**\n```isl\nforeach $item in $array | filter($item.active)\n    $item.name | upperCase\nendfor\n```',
+            'while': '**While Loop**\n\nRepeats a block while a condition is true. Max 50 iterations by default.\n\n```isl\n$counter = 0;\nwhile ($counter < 10)\n    $counter = {{ $counter + 1 }}\nendwhile\n```\n\n**With options:**\n```isl\nwhile ($condition, {maxLoops: 100})\n    // statements\nendwhile\n```',
+            'switch': '**Switch Statement**\n\nMatches a value against multiple cases.\n\n```isl\nswitch ($status)\n    "active" -> "Active";\n    "pending" -> "Pending";\n    /^temp.*/ -> "Temporary";\n    < 100 -> "Low";\n    else -> "Unknown";\nendswitch\n```',
+            'return': '**Return Statement**\n\nReturns a value from a function. Functions must always return a value.\n\n```isl\nfun calculate($x) {\n    return {{ $x * 2 }}\n}\n```\n\n**Note:** Use `return {};` to return empty object, not `return;`',
+            'import': '**Import Statement**\n\nImports functions and types from another ISL file.\n\n```isl\nimport Common from \'common.isl\';\nimport Utils from \'../utils.isl\';\n\n$result: @.Common.someFunction();\n$value: $input | Common.someModifier;\n```',
+            'type': '**Type Declaration**\n\nDefines a custom type for validation.\n\n```isl\ntype Address as { \n    street: String, \n    city: String,\n    zip: String \n};\n\ntype User as {\n    name: String,\n    address: Address\n};\n```',
+            'parallel': '**Parallel Execution**\n\nExecutes a foreach loop in parallel for better performance.\n\n```isl\nparallel foreach $item in $array\n    @.Service.process($item)\nendfor\n\nparallel {threads: 10} foreach $item in $largeArray\n    // processing\nendfor\n```',
+            'cache': '**Cache Decorator**\n\nCaches the result of a function call based on parameters.\n\n```isl\ncache fun expensiveOperation($param) {\n    // result is cached\n    return @.Service.slowCall($param)\n}\n```',
+            'filter': '**Filter Modifier**\n\nFilters an array based on a condition.\n\n```isl\n$active: $items | filter($item.status == "active");\n$highValue: $orders | filter($order.total > 1000);\n```',
+            'map': '**Map Modifier**\n\nTransforms each element of an array.\n\n```isl\n$names: $users | map($user.name);\n$totals: $orders | map({{ $order.qty * $order.price }});\n```',
+        };
+
+        const markdown = new vscode.MarkdownString(docs[word] || `**${word}**\n\nISL keyword`);
+        markdown.isTrusted = true;
+        return new vscode.Hover(markdown);
+    }
+
+    private getServiceHover(word: string): vscode.Hover {
+        const servicesMap = getServicesMap();
+        const info = servicesMap.get(word);
+        const markdown = new vscode.MarkdownString(
+            info?.documentation ? `**@.${word}**\n\n${info.documentation}` : `**@.${word}**\n\nISL service`
+        );
+        markdown.isTrusted = true;
+        return new vscode.Hover(markdown);
+    }
+
+    /** Resolve full modifier name from line (e.g. "fromEpochSeconds" in "| date.fromEpochSeconds" -> "date.fromEpochSeconds"). */
+    private resolveFullModifierName(line: string, word: string): string {
+        const modifiersMap = getModifiersMap();
+        if (modifiersMap.has(word)) return word;
+        const modifierPattern = /\|\s*([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*(?:\(|$|\|)/g;
+        let match: RegExpExecArray | null;
+        while ((match = modifierPattern.exec(line)) !== null) {
+            const fullName = match[1];
+            if (fullName === word || fullName.endsWith('.' + word)) {
+                return fullName;
+            }
+        }
+        return word;
+    }
+
+    private getModifierHover(word: string, line: string): vscode.Hover {
+        const modifiersMap = getModifiersMap();
+        const fullName = this.resolveFullModifierName(line, word);
+        const mod = modifiersMap.get(fullName);
+        if (mod && mod.hover) {
+            const info = mod.hover;
+            let markdown = `**\`${fullName}\`** modifier\n\n${info.description}`;
+            if (info.signature) {
+                markdown += `\n\n**Signature:** \`${info.signature}\``;
+            }
+            if (mod.returns) {
+                markdown += `\n\n**Returns:**`;
+                if (mod.returns.type) markdown += ` \`${mod.returns.type}\``;
+                if (mod.returns.description) markdown += ` - ${mod.returns.description}`;
+                markdown += '\n';
+            }
+            if (info.example) {
+                markdown += `\n\n**Example:**\n\`\`\`isl\n${info.example}\n\`\`\``;
+            }
+            const md = new vscode.MarkdownString(markdown);
+            md.isTrusted = true;
+            return new vscode.Hover(md);
+        }
+        if (mod) {
+            let markdown = `**\`${fullName}\`** modifier`;
+            if (mod.detail) markdown += `\n\n${mod.detail}`;
+            if (mod.returns) {
+                markdown += `\n\n**Returns:**`;
+                if (mod.returns.type) markdown += ` \`${mod.returns.type}\``;
+                if (mod.returns.description) markdown += ` - ${mod.returns.description}`;
+                markdown += '\n';
+            }
+            if (mod.documentation) markdown += `\n\n${mod.documentation}`;
+            const md = new vscode.MarkdownString(markdown);
+            md.isTrusted = true;
+            return new vscode.Hover(md);
+        }
+
+        const markdown = new vscode.MarkdownString(`**\`${word}\`** modifier`);
+        markdown.isTrusted = true;
+        return new vscode.Hover(markdown);
+    }
+
+    private getVariableHover(word: string, document: vscode.TextDocument): vscode.Hover {
+        const markdown = new vscode.MarkdownString(`**$${word}**\n\nVariable`);
+        markdown.isTrusted = true;
+        return new vscode.Hover(markdown);
+    }
+}
diff --git a/plugin/src/inlayhints.ts b/plugin/src/inlayhints.ts
new file mode 100644
index 0000000..a18ea81
--- /dev/null
+++ b/plugin/src/inlayhints.ts
@@ -0,0 +1,207 @@
+import * as vscode from 'vscode';
+
+export class IslInlayHintsProvider implements vscode.InlayHintsProvider {
+    
+    provideInlayHints(
+        document: vscode.TextDocument,
+        range: vscode.Range,
+        token: vscode.CancellationToken
+    ): vscode.InlayHint[] {
+        const hints: vscode.InlayHint[] = [];
+        const text = document.getText(range);
+        const lines = text.split('\n');
+        const startLine = range.start.line;
+        
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            const lineNumber = startLine + i;
+            
+            // Add type hints for variable declarations
+            this.addVariableTypeHints(line, lineNumber, hints, document);
+            
+            // Add parameter name hints for function calls
+            this.addParameterNameHints(line, lineNumber, hints, document);
+            
+            // Add array operation hints
+            this.addArrayOperationHints(line, lineNumber, hints, document);
+        }
+        
+        return hints;
+    }
+    
+    private addVariableTypeHints(line: string, lineNumber: number, hints: vscode.InlayHint[], document: vscode.TextDocument) {
+        // Detect variable declarations with modifiers that indicate type
+        const patterns = [
+            // to.string suggests string
+            { pattern: /(\$[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*[^;|]*\|\s*to\.string/g, type: 'string' },
+            // to.number suggests number
+            { pattern: /(\$[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*[^;|]*\|\s*to\.number/g, type: 'number' },
+            // to.decimal suggests decimal
+            { pattern: /(\$[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*[^;|]*\|\s*to\.decimal/g, type: 'decimal' },
+            // to.boolean suggests boolean
+            { pattern: /(\$[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*[^;|]*\|\s*to\.boolean/g, type: 'boolean' },
+            // to.array or map suggests array
+            { pattern: /(\$[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*[^;|]*\|\s*(to\.array|map|filter)/g, type: 'array' },
+            // date.parse suggests date
+            { pattern: /(\$[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*[^;|]*\|\s*date\.parse/g, type: 'date' },
+            // @.Date suggests date
+            { pattern: /(\$[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*@\.Date\./g, type: 'date' },
+            // Math operations suggest number
+            { pattern: /(\$[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*\{\{[^}]+\}\}/g, type: 'number' },
+            // keys suggests string array
+            { pattern: /(\$[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*[^;|]*\|\s*keys/g, type: 'string[]' },
+            // Object literal suggests object (but not {{ for math expressions)
+            { pattern: /(\$[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*\{(?!\{)/g, type: 'object' },
+            // Array literal suggests array
+            { pattern: /(\$[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*\[/g, type: 'array' },
+            // String literal
+            { pattern: /(\$[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*["'`]/g, type: 'string' },
+            // Number literal
+            { pattern: /(\$[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*-?\d+(\.\d+)?(?!\w)/g, type: 'number' },
+            // Boolean literal
+            { pattern: /(\$[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(true|false)\b/g, type: 'boolean' },
+        ];
+        
+        for (const { pattern, type } of patterns) {
+            let match;
+            pattern.lastIndex = 0; // Reset regex
+            
+            while ((match = pattern.exec(line)) !== null) {
+                const varName = match[1];
+                const position = new vscode.Position(lineNumber, match.index + varName.length);
+                
+                const hint = new vscode.InlayHint(
+                    position,
+                    `: ${type}`,
+                    vscode.InlayHintKind.Type
+                );
+                hint.paddingLeft = false;
+                hint.paddingRight = false;
+                
+                hints.push(hint);
+            }
+        }
+    }
+    
+    private addParameterNameHints(line: string, lineNumber: number, hints: vscode.InlayHint[], document: vscode.TextDocument) {
+        // Add parameter name hints for common functions with multiple parameters
+        const functionCalls = [
+            // truncate(length, suffix)
+            { 
+                pattern: /\|\s*truncate\s*\(\s*(\d+)\s*,\s*("[^"]*")/g, 
+                params: ['maxLength:', 'suffix:']
+            },
+            // padStart(length, padString)
+            { 
+                pattern: /\|\s*padStart\s*\(\s*(\d+)\s*,\s*("[^"]*")/g, 
+                params: ['length:', 'padString:']
+            },
+            // padEnd(length, padString)
+            { 
+                pattern: /\|\s*padEnd\s*\(\s*(\d+)\s*,\s*("[^"]*")/g, 
+                params: ['length:', 'padString:']
+            },
+            // Math.clamp(min, max)
+            { 
+                pattern: /\|\s*Math\.clamp\s*\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)/g, 
+                params: ['min:', 'max:']
+            },
+            // date.add(amount, unit)
+            { 
+                pattern: /\|\s*date\.add\s*\(\s*(-?\d+)\s*,\s*"([^"]*)"/g, 
+                params: ['amount:', 'unit:']
+            },
+            // substring(start, end)
+            { 
+                pattern: /\|\s*substring\s*\(\s*(\d+)\s*,\s*(\d+)/g, 
+                params: ['start:', 'end:']
+            },
+            // replace(find, replaceWith)
+            { 
+                pattern: /\|\s*replace\s*\(\s*("[^"]*")\s*,\s*("[^"]*")/g, 
+                params: ['find:', 'replaceWith:']
+            },
+        ];
+        
+        for (const { pattern, params } of functionCalls) {
+            let match;
+            pattern.lastIndex = 0;
+            
+            while ((match = pattern.exec(line)) !== null) {
+                // Add hint for each parameter
+                for (let i = 1; i < match.length && i - 1 < params.length; i++) {
+                    const paramValue = match[i];
+                    const paramStart = match.index + match[0].indexOf(paramValue);
+                    const position = new vscode.Position(lineNumber, paramStart);
+                    
+                    const hint = new vscode.InlayHint(
+                        position,
+                        params[i - 1],
+                        vscode.InlayHintKind.Parameter
+                    );
+                    hint.paddingLeft = false;
+                    hint.paddingRight = true;
+                    
+                    hints.push(hint);
+                }
+            }
+        }
+    }
+    
+    private addArrayOperationHints(line: string, lineNumber: number, hints: vscode.InlayHint[], document: vscode.TextDocument) {
+        // Add hints for array operations that produce specific types
+        const arrayOps = [
+            // map produces array
+            { pattern: /(\|)\s*(map\s*\([^)]+\))/g, hint: ' → array' },
+            // filter produces array
+            { pattern: /(\|)\s*(filter\s*\([^)]+\))/g, hint: ' → array' },
+            // unique produces array
+            { pattern: /(\|)\s*(unique)/g, hint: ' → array' },
+            // sort produces array
+            { pattern: /(\|)\s*(sort)/g, hint: ' → array' },
+            // reverse produces array
+            { pattern: /(\|)\s*(reverse)/g, hint: ' → array' },
+            // flatten produces array
+            { pattern: /(\|)\s*(flatten)/g, hint: ' → array' },
+            // Math.sum produces number
+            { pattern: /(\|)\s*(Math\.sum\([^)]*\))/g, hint: ' → number' },
+            // Math.average produces number
+            { pattern: /(\|)\s*(Math\.average)/g, hint: ' → number' },
+            // length produces number
+            { pattern: /(\|)\s*(length)/g, hint: ' → number' },
+            // first produces single item
+            { pattern: /(\|)\s*(first)/g, hint: ' → item' },
+            // last produces single item
+            { pattern: /(\|)\s*(last)/g, hint: ' → item' },
+            // keys produces string array
+            { pattern: /(\|)\s*(keys)/g, hint: ' → string[]' },
+            // to.boolean produces boolean
+            { pattern: /(\|)\s*(to\.boolean)/g, hint: ' → boolean' },
+            // isEmpty produces boolean
+            { pattern: /(\|)\s*(isEmpty)/g, hint: ' → boolean' },
+            // isNotEmpty produces boolean
+            { pattern: /(\|)\s*(isNotEmpty)/g, hint: ' → boolean' },
+        ];
+        
+        for (const { pattern, hint: hintText } of arrayOps) {
+            let match;
+            pattern.lastIndex = 0;
+            
+            while ((match = pattern.exec(line)) !== null) {
+                const opEnd = match.index + match[0].length;
+                const position = new vscode.Position(lineNumber, opEnd);
+                
+                const hint = new vscode.InlayHint(
+                    position,
+                    hintText,
+                    vscode.InlayHintKind.Type
+                );
+                hint.paddingLeft = true;
+                hint.paddingRight = false;
+                
+                hints.push(hint);
+            }
+        }
+    }
+}
+
diff --git a/plugin/src/islPasteProvider.ts b/plugin/src/islPasteProvider.ts
new file mode 100644
index 0000000..3d27f93
--- /dev/null
+++ b/plugin/src/islPasteProvider.ts
@@ -0,0 +1,23 @@
+import * as vscode from 'vscode';
+
+/**
+ * Paste edit provider for ISL that inserts clipboard text as-is.
+ * VS Code's default paste applies indentation based on the current line, which often
+ * adds unwanted leading spaces when pasting in the middle of a line (e.g. between quotes).
+ * This provider bypasses that by providing a plain-text paste edit with no extra indentation.
+ */
+export class IslPasteEditProvider implements vscode.DocumentPasteEditProvider {
+    async provideDocumentPasteEdits(
+        _document: vscode.TextDocument,
+        _ranges: readonly vscode.Range[],
+        dataTransfer: vscode.DataTransfer,
+        _context: vscode.DocumentPasteEditContext,
+        token: vscode.CancellationToken
+    ): Promise {
+        const item = dataTransfer.get('text/plain');
+        if (!item) return undefined;
+        const text = await item.asString();
+        if (token.isCancellationRequested) return undefined;
+        return [new vscode.DocumentPasteEdit(text, 'Plain text', vscode.DocumentDropOrPasteEditKind.Text)];
+    }
+}
diff --git a/plugin/src/islYamlTestsCompletion.ts b/plugin/src/islYamlTestsCompletion.ts
new file mode 100644
index 0000000..dad17d7
--- /dev/null
+++ b/plugin/src/islYamlTestsCompletion.ts
@@ -0,0 +1,286 @@
+import * as vscode from 'vscode';
+import * as path from 'path';
+
+type YamlContext = 'root' | 'setup' | 'tests';
+
+interface YamlStructure {
+    rootIndent: number;
+    hasCategory: boolean;
+    hasSetup: boolean;
+    hasAssertOptions: boolean;
+    hasTests: boolean;
+    setupIndent: number;
+    testsIndent: number;
+    context: YamlContext;
+    /** True when cursor is inside assertOptions block (suite or per-test). */
+    inAssertOptions: boolean;
+}
+
+const ASSERT_OPTION_NAMES = [
+    'nullSameAsMissing',
+    'nullSameAsEmptyArray',
+    'missingSameAsEmptyArray',
+    'ignoreExtraFieldsInActual',
+    'numbersEqualIgnoreFormat'
+];
+
+/** Completions for *.tests.yaml ISL test suite files: schema keys and function names. */
+export class IslYamlTestsCompletionProvider implements vscode.CompletionItemProvider {
+    private readonly functionNameCache = new Map();
+    private static readonly CACHE_TTL_MS = 10000;
+
+    async provideCompletionItems(
+        document: vscode.TextDocument,
+        position: vscode.Position,
+        _token: vscode.CancellationToken,
+        _context: vscode.CompletionContext
+    ): Promise {
+        if (!document.uri.fsPath.endsWith('.tests.yaml')) {
+            return null;
+        }
+
+        const lineText = document.lineAt(position).text;
+        const linePrefix = lineText.substring(0, position.character);
+        const indent = lineText.match(/^\s*/)?.[0] ?? '';
+        const indentLen = indent.length;
+        const content = document.getText();
+        const lines = content.split(/\r?\n/);
+        const currentLineIndex = position.line;
+        const structure = this.getYamlStructure(lines, currentLineIndex, indentLen);
+
+        // ---- Root level: category, setup, assertOptions, tests, islTests (not when inside assertOptions block) ----
+        if (structure.context === 'root' && !structure.inAssertOptions && indentLen <= structure.rootIndent + 2) {
+            const rootKeyMatch = linePrefix.match(/^(\s*)(\w*)$/);
+            const keyPrefix = rootKeyMatch ? rootKeyMatch[2] : '';
+            const items = this.getRootKeyCompletions(structure, keyPrefix);
+            if (items.length > 0) return items;
+        }
+
+        // ---- Inside setup: islSource, mockSource, mocks ----
+        if (structure.context === 'setup') {
+            const setupKeyMatch = linePrefix.match(/^(\s*)(\w*)$/);
+            if (setupKeyMatch) {
+                const keyPrefix = setupKeyMatch[2];
+                const items = this.getSetupKeyCompletions(keyPrefix);
+                if (items.length > 0) return items;
+            }
+        }
+
+        // ---- Inside tests/islTests array: list item keys ----
+        const insideTestsArray = structure.context === 'tests' && indentLen > structure.testsIndent;
+        const listItemKeyMatch = linePrefix.match(/^(\s*)-\s*(\w*)$/);
+        if (insideTestsArray && listItemKeyMatch) {
+            const keyPrefix = listItemKeyMatch[2];
+            const items = this.getTestEntryKeyCompletions(keyPrefix, listItemKeyMatch[1].length + 2);
+            if (items.length > 0) return items;
+        }
+
+        // ---- functionName value: suggest ISL function names ----
+        const afterFunctionNameKey = linePrefix.match(/^\s*functionName\s*:\s*["']?(\w*)$/);
+        if (insideTestsArray && afterFunctionNameKey) {
+            const valuePrefix = afterFunctionNameKey[1];
+            const names = await this.getIslFunctionNames(document);
+            const items = names
+                .filter(n => !valuePrefix || n.toLowerCase().startsWith(valuePrefix.toLowerCase()))
+                .map(n => {
+                    const item = new vscode.CompletionItem(n, vscode.CompletionItemKind.Function);
+                    item.detail = 'ISL function or modifier';
+                    item.insertText = n;
+                    return item;
+                });
+            if (items.length > 0) return items;
+        }
+
+        // ---- assertOptions keys (suite or per-test): option names ----
+        const assertOptionKeyMatch = linePrefix.match(/^(\s*)(\w*)$/);
+        if (structure.inAssertOptions && assertOptionKeyMatch) {
+            const keyPrefix = assertOptionKeyMatch[2];
+            const items = ASSERT_OPTION_NAMES
+                .filter(n => !keyPrefix || n.toLowerCase().startsWith(keyPrefix.toLowerCase()))
+                .map(n => {
+                    const item = new vscode.CompletionItem(n, vscode.CompletionItemKind.Property);
+                    item.detail = 'Assert option';
+                    item.insertText = new vscode.SnippetString(`${n}: \${1:true}`);
+                    return item;
+                });
+            if (items.length > 0) return items;
+        }
+
+        return null;
+    }
+
+    private getYamlStructure(lines: string[], currentLineIndex: number, currentIndent: number): YamlStructure {
+        let rootIndent = 0;
+        let hasCategory = false;
+        let hasSetup = false;
+        let hasAssertOptions = false;
+        let hasTests = false;
+        let setupIndent = -1;
+        let testsIndent = -1;
+        let context: YamlContext = 'root';
+
+        const keyRe = /^(\s*)(\w+)\s*:/;
+        for (let i = 0; i <= currentLineIndex && i < lines.length; i++) {
+            const line = lines[i];
+            const m = line.match(keyRe);
+            if (m) {
+                const ind = m[1].length;
+                const key = m[2];
+                if (i === 0 || ind <= rootIndent) {
+                    rootIndent = ind;
+                }
+                if (ind === rootIndent) {
+                    hasCategory = hasCategory || key === 'category';
+                    hasSetup = hasSetup || key === 'setup';
+                    hasAssertOptions = hasAssertOptions || key === 'assertOptions';
+                    hasTests = hasTests || key === 'tests' || key === 'islTests';
+                    if (key === 'setup') setupIndent = ind;
+                    if (key === 'tests' || key === 'islTests') testsIndent = ind;
+                    context = 'root';
+                } else if (setupIndent >= 0 && ind > setupIndent && (testsIndent < 0 || ind <= testsIndent)) {
+                    context = 'setup';
+                } else if (testsIndent >= 0 && ind > testsIndent) {
+                    context = 'tests';
+                }
+            }
+        }
+
+        // InAssertOptions: scan backward; only stop when we find a key with strictly less indent (parent). If that parent is assertOptions, we're inside it.
+        let inAssertOptions = false;
+        for (let i = currentLineIndex; i >= 0; i--) {
+            const line = lines[i];
+            const m = line.match(keyRe);
+            if (m) {
+                const ind = m[1].length;
+                if (ind < currentIndent) {
+                    if (m[2] === 'assertOptions') inAssertOptions = true;
+                    break;
+                }
+            }
+        }
+
+        return {
+            rootIndent,
+            hasCategory,
+            hasSetup,
+            hasAssertOptions,
+            hasTests,
+            setupIndent: setupIndent >= 0 ? setupIndent : 0,
+            testsIndent: testsIndent >= 0 ? testsIndent : 0,
+            context,
+            inAssertOptions
+        };
+    }
+
+    private getRootKeyCompletions(structure: YamlStructure, keyPrefix: string): vscode.CompletionItem[] {
+        const items: vscode.CompletionItem[] = [];
+
+        const add = (key: string, detail: string, doc: string, insert: vscode.SnippetString | string) => {
+            if (keyPrefix && !key.toLowerCase().startsWith(keyPrefix.toLowerCase())) return;
+            const item = new vscode.CompletionItem(key, vscode.CompletionItemKind.Property);
+            item.detail = detail;
+            item.documentation = new vscode.MarkdownString(doc);
+            item.insertText = typeof insert === 'string' ? insert : insert;
+            items.push(item);
+        };
+
+        if (!structure.hasCategory) {
+            add('category', 'Test group label in output', 'Optional label for the suite in results.', 'category: ${1:my-group-name}');
+        }
+        if (!structure.hasSetup) {
+            add('setup', 'ISL source and mocks', 'Required. Contains **islSource** and optionally **mockSource**, **mocks**.', new vscode.SnippetString('setup:\n  islSource: ${1:module.isl}'));
+        }
+        if (!structure.hasAssertOptions) {
+            add('assertOptions', 'Comparison options for expected vs actual', 'e.g. nullSameAsMissing, ignoreExtraFieldsInActual.', new vscode.SnippetString('assertOptions:\n  nullSameAsMissing: ${1:true}'));
+        }
+        if (!structure.hasTests) {
+            add('tests', 'List of test entries', 'Same as islTests. Each entry: name, functionName, input, expected.', new vscode.SnippetString('tests:\n  - name: "${1:Test name}"\n    functionName: ${2:functionName}'));
+            add('islTests', 'List of test entries', 'Same as tests. Each entry: name, functionName, input, expected.', new vscode.SnippetString('islTests:\n  - name: "${1:Test name}"\n    functionName: ${2:functionName}'));
+        }
+
+        return items;
+    }
+
+    private getSetupKeyCompletions(keyPrefix: string): vscode.CompletionItem[] {
+        const items: vscode.CompletionItem[] = [];
+
+        const add = (key: string, detail: string, insert: string | vscode.SnippetString) => {
+            if (keyPrefix && !key.toLowerCase().startsWith(keyPrefix.toLowerCase())) return;
+            const item = new vscode.CompletionItem(key, vscode.CompletionItemKind.Property);
+            item.detail = detail;
+            item.insertText = typeof insert === 'string' ? insert : insert;
+            items.push(item);
+        };
+
+        add('islSource', 'ISL file to test (required)', 'islSource: ${1:module.isl}');
+        add('mockSource', 'Mock definitions file(s)', 'mockSource: ${1:optional.yaml}');
+        add('mocks', 'Inline mocks (func / annotation arrays)', new vscode.SnippetString('mocks:\n  func:\n    - name: "${1:FuncName}"\n      return: ${2:null}'));
+
+        return items;
+    }
+
+    private getTestEntryKeyCompletions(keyPrefix: string, itemIndent: number): vscode.CompletionItem[] {
+        const items: vscode.CompletionItem[] = [];
+        const pad = ' '.repeat(itemIndent);
+
+        const add = (key: string, detail: string, insert: string | vscode.SnippetString) => {
+            if (keyPrefix && !key.toLowerCase().startsWith(keyPrefix.toLowerCase())) return;
+            const item = new vscode.CompletionItem(key, vscode.CompletionItemKind.Property);
+            item.detail = detail;
+            item.insertText = typeof insert === 'string' ? insert : insert;
+            items.push(item);
+        };
+
+        add('name', 'Display name for the test', new vscode.SnippetString('name: "${1:Test name}"'));
+        add('functionName', 'ISL function to call', new vscode.SnippetString('functionName: ${1:functionName}'));
+        add('input', 'Input (scalar or object with param names)', new vscode.SnippetString('input: ${1:null}'));
+        add('expected', 'Expected return value', new vscode.SnippetString('expected: ${1:null}'));
+        add('byPassAnnotations', 'Bypass annotation processing', 'byPassAnnotations: ${1:false}');
+        add('assertOptions', 'Per-test assert options', new vscode.SnippetString('assertOptions:\n  ${1:nullSameAsMissing}: true'));
+
+        if (!keyPrefix) {
+            const fullItem = new vscode.CompletionItem('Test entry (name + functionName + input/expected)', vscode.CompletionItemKind.Snippet);
+            fullItem.detail = 'Insert full test entry';
+            fullItem.insertText = new vscode.SnippetString(`name: "\${1:Test name}"\n${pad}functionName: \${2:functionName}\n${pad}input: \${3:null}\n${pad}expected: \${4:null}`);
+            items.push(fullItem);
+        }
+
+        return items;
+    }
+
+    /** Collect fun/modifier names from workspace .isl files (same dir, tests/, and root). */
+    private async getIslFunctionNames(document: vscode.TextDocument): Promise {
+        const folder = vscode.workspace.getWorkspaceFolder(document.uri);
+        if (!folder) return [];
+
+        const dir = path.dirname(document.uri.fsPath);
+        const cacheKey = `${folder.uri.fsPath}:${dir}`;
+        const now = Date.now();
+        const cached = this.functionNameCache.get(cacheKey);
+        if (cached && (now - cached.mtime) < IslYamlTestsCompletionProvider.CACHE_TTL_MS) {
+            return cached.names;
+        }
+
+        const names = new Set();
+        const pattern = new vscode.RelativePattern(folder, '**/*.isl');
+        const uris = await vscode.workspace.findFiles(pattern);
+        const funcPattern = /^\s*(fun|modifier)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/;
+
+        for (const uri of uris) {
+            try {
+                const doc = await vscode.workspace.openTextDocument(uri);
+                const lines = doc.getText().split(/\r?\n/);
+                for (const line of lines) {
+                    const m = line.match(funcPattern);
+                    if (m) names.add(m[2]);
+                }
+            } catch {
+                // skip unreadable files
+            }
+        }
+
+        const list = Array.from(names).sort();
+        this.functionNameCache.set(cacheKey, { mtime: now, names: list });
+        return list;
+    }
+}
diff --git a/plugin/src/language.ts b/plugin/src/language.ts
new file mode 100644
index 0000000..1f61326
--- /dev/null
+++ b/plugin/src/language.ts
@@ -0,0 +1,208 @@
+import * as path from 'path';
+import * as fs from 'fs';
+
+export interface ModifierSignatureParam {
+    label: string;
+    documentation?: string;
+}
+
+export interface ModifierSignature {
+    label: string;
+    parameters?: ModifierSignatureParam[];
+    documentation?: string;
+}
+
+export interface ModifierHover {
+    description: string;
+    signature?: string;
+    example?: string;
+}
+
+export interface IslReturns {
+    type?: string;
+    description?: string;
+}
+
+export interface BuiltInModifier {
+    name: string;
+    detail?: string;
+    insertText: string;
+    documentation?: string;
+    returns?: IslReturns;
+    signature?: ModifierSignature;
+    hover?: ModifierHover;
+}
+
+export interface FunctionSignatureParam {
+    label: string;
+    documentation?: string;
+}
+
+export interface FunctionSignature {
+    label: string;
+    parameters?: FunctionSignatureParam[];
+    documentation?: string;
+}
+
+export interface BuiltInFunction {
+    namespace: string;
+    name: string;
+    detail?: string;
+    params?: string;
+    documentation?: string;
+    returns?: IslReturns;
+    signature?: FunctionSignature;
+}
+
+export interface ServiceInfo {
+    name: string;
+    detail?: string;
+    documentation?: string;
+}
+
+export interface AnnotationInfo {
+    name: string;
+    detail?: string;
+    insertText?: string;
+    documentation?: string;
+}
+
+export interface IslLanguageData {
+    modifierValidationPatterns: string[];
+    modifiers: BuiltInModifier[];
+    functions: BuiltInFunction[];
+    services: ServiceInfo[];
+    annotations?: AnnotationInfo[];
+}
+
+let cachedData: IslLanguageData | null = null;
+let extensionPath: string | null = null;
+
+/**
+ * Initialize the language data from the extension path. Call this from extension.ts on activate.
+ */
+export function initIslLanguage(extPath: string): void {
+    extensionPath = extPath;
+    cachedData = null;
+}
+
+function getDataPath(): string {
+    if (extensionPath) {
+        return path.join(extensionPath, 'isl-language.json');
+    }
+    return path.join(__dirname, '..', 'isl-language.json');
+}
+
+function loadData(): IslLanguageData {
+    if (cachedData) {
+        return cachedData;
+    }
+    const dataPath = getDataPath();
+    try {
+        const raw = fs.readFileSync(dataPath, 'utf-8');
+        cachedData = JSON.parse(raw) as IslLanguageData;
+        if (!cachedData.modifiers) cachedData.modifiers = [];
+        if (!cachedData.functions) cachedData.functions = [];
+        if (!cachedData.services) cachedData.services = [];
+        if (!cachedData.annotations) cachedData.annotations = [];
+        if (!cachedData.modifierValidationPatterns) cachedData.modifierValidationPatterns = [];
+        return cachedData;
+    } catch (e) {
+        console.error('[ISL] Failed to load isl-language.json:', e);
+        cachedData = {
+            modifierValidationPatterns: [],
+            modifiers: [],
+            functions: [],
+            services: []
+        };
+        return cachedData;
+    }
+}
+
+/**
+ * Get the full language data (modifiers, functions, services, validation patterns).
+ */
+export function getLanguageData(): IslLanguageData {
+    return loadData();
+}
+
+/**
+ * Set of built-in modifier names + wildcard patterns for validation (e.g. "trim", "to.*").
+ */
+export function getBuiltInModifiersSet(): Set {
+    const data = loadData();
+    const set = new Set(data.modifiers.map(m => m.name));
+    data.modifierValidationPatterns.forEach(p => set.add(p));
+    return set;
+}
+
+/**
+ * Set of built-in function keys "Namespace.name" for validation (e.g. "Date.now", "Math.sum").
+ */
+export function getBuiltInFunctionsSet(): Set {
+    const data = loadData();
+    return new Set(data.functions.map(f => `${f.namespace}.${f.name}`));
+}
+
+/**
+ * Set of built-in namespace names (Date, Math, String, etc.) for validation.
+ */
+export function getBuiltInNamespacesSet(): Set {
+    const data = loadData();
+    const names = new Set(data.functions.map(f => f.namespace));
+    names.add('This');
+    names.add('Pagination');
+    return names;
+}
+
+/**
+ * Map of modifier name -> definition for completion, hover, signature.
+ */
+export function getModifiersMap(): Map {
+    const data = loadData();
+    const map = new Map();
+    data.modifiers.forEach(m => map.set(m.name, m));
+    return map;
+}
+
+/**
+ * Map of "Namespace.name" -> function definition for completion, hover, signature.
+ */
+export function getFunctionsMap(): Map {
+    const data = loadData();
+    const map = new Map();
+    data.functions.forEach(f => map.set(`${f.namespace}.${f.name}`, f));
+    return map;
+}
+
+/**
+ * Map of namespace -> function definitions for service method completion/signature.
+ */
+export function getFunctionsByNamespace(): Map {
+    const data = loadData();
+    const map = new Map();
+    data.functions.forEach(f => {
+        const list = map.get(f.namespace) || [];
+        list.push(f);
+        map.set(f.namespace, list);
+    });
+    return map;
+}
+
+/**
+ * Map of service name -> ServiceInfo for service completion and hover.
+ */
+export function getServicesMap(): Map {
+    const data = loadData();
+    const map = new Map();
+    data.services.forEach(s => map.set(s.name, s));
+    return map;
+}
+
+/**
+ * Get annotations (e.g. @test, @setup) for completion and hover.
+ */
+export function getAnnotations(): AnnotationInfo[] {
+    const data = loadData();
+    return data.annotations ?? [];
+}
diff --git a/plugin/src/signature.ts b/plugin/src/signature.ts
new file mode 100644
index 0000000..79f9566
--- /dev/null
+++ b/plugin/src/signature.ts
@@ -0,0 +1,131 @@
+import * as vscode from 'vscode';
+import { getModifiersMap, getFunctionsByNamespace } from './language';
+
+export class IslSignatureHelpProvider implements vscode.SignatureHelpProvider {
+    
+    provideSignatureHelp(
+        document: vscode.TextDocument,
+        position: vscode.Position,
+        token: vscode.CancellationToken,
+        context: vscode.SignatureHelpContext
+    ): vscode.SignatureHelp | undefined {
+        const line = document.lineAt(position).text;
+        const beforeCursor = line.substring(0, position.character);
+        
+        // Check if we're inside a modifier call
+        const modifierMatch = beforeCursor.match(/\|\s*([a-zA-Z_][a-zA-Z0-9_.]*)(?:\(([^)]*))?$/);
+        if (modifierMatch) {
+            return this.getModifierSignature(modifierMatch[1], beforeCursor);
+        }
+        
+        // Check if we're inside a service call
+        const serviceMatch = beforeCursor.match(/@\.([A-Z][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)(?:\(([^)]*))?$/);
+        if (serviceMatch) {
+            return this.getServiceSignature(serviceMatch[1], serviceMatch[2], beforeCursor);
+        }
+        
+        // Check if we're inside a custom function call (@.This.)
+        const thisMatch = beforeCursor.match(/@\.This\.([a-zA-Z_][a-zA-Z0-9_]*)(?:\(([^)]*))?$/);
+        if (thisMatch) {
+            return this.getCustomFunctionSignature(document, thisMatch[1], beforeCursor);
+        }
+        
+        return undefined;
+    }
+    
+    private getModifierSignature(modifier: string, beforeCursor: string): vscode.SignatureHelp | undefined {
+        const modifiersMap = getModifiersMap();
+        const mod = modifiersMap.get(modifier);
+        if (!mod?.signature) {
+            return undefined;
+        }
+        const sig = mod.signature;
+        const signatureHelp = new vscode.SignatureHelp();
+        const signature = new vscode.SignatureInformation(
+            sig.label,
+            new vscode.MarkdownString(sig.documentation ?? '')
+        );
+        (sig.parameters ?? []).forEach(param => {
+            signature.parameters.push(
+                new vscode.ParameterInformation(param.label, new vscode.MarkdownString(param.documentation ?? ''))
+            );
+        });
+        signatureHelp.signatures = [signature];
+        signatureHelp.activeSignature = 0;
+        const paramSection = beforeCursor.match(/\(([^)]*)?$/)?.[1] || '';
+        const commaCount = (paramSection.match(/,/g) || []).length;
+        signatureHelp.activeParameter = Math.min(commaCount, (sig.parameters ?? []).length - 1);
+        return signatureHelp;
+    }
+    
+    private getServiceSignature(service: string, method: string, beforeCursor: string): vscode.SignatureHelp | undefined {
+        const byNamespace = getFunctionsByNamespace();
+        const methods = byNamespace.get(service);
+        if (!methods) return undefined;
+        const func = methods.find(f => f.name.toLowerCase() === method.toLowerCase());
+        if (!func?.signature) return undefined;
+        const sig = func.signature;
+        const signatureHelp = new vscode.SignatureHelp();
+        const signature = new vscode.SignatureInformation(
+            sig.label,
+            new vscode.MarkdownString(sig.documentation ?? '')
+        );
+        (sig.parameters ?? []).forEach(param => {
+            signature.parameters.push(
+                new vscode.ParameterInformation(param.label, new vscode.MarkdownString(param.documentation ?? ''))
+            );
+        });
+        signatureHelp.signatures = [signature];
+        signatureHelp.activeSignature = 0;
+        const paramSection = beforeCursor.match(/\(([^)]*)?$/)?.[1] || '';
+        const commaCount = (paramSection.match(/,/g) || []).length;
+        signatureHelp.activeParameter = Math.min(commaCount, (sig.parameters ?? []).length - 1);
+        return signatureHelp;
+    }
+    
+    private getCustomFunctionSignature(document: vscode.TextDocument, functionName: string, beforeCursor: string): vscode.SignatureHelp | undefined {
+        const text = document.getText();
+        const lines = text.split('\n');
+        
+        // Find function declaration
+        const functionPattern = new RegExp(`^\\s*(fun|modifier)\\s+${functionName}\\s*\\(([^)]*)\\)`);
+        
+        for (let i = 0; i < lines.length; i++) {
+            const match = lines[i].match(functionPattern);
+            if (match) {
+                const funcType = match[1];
+                const params = match[2].trim();
+                
+                if (!params) {
+                    return undefined; // No parameters
+                }
+                
+                const signatureHelp = new vscode.SignatureHelp();
+                const signature = new vscode.SignatureInformation(
+                    `${funcType} ${functionName}(${params})`,
+                    new vscode.MarkdownString(`Custom ${funcType} defined in this file`)
+                );
+                
+                // Parse parameters
+                const paramList = params.split(',').map(p => p.trim()).filter(p => p.length > 0);
+                paramList.forEach(param => {
+                    const paramName = param.split(':')[0].trim();
+                    signature.parameters.push(new vscode.ParameterInformation(paramName));
+                });
+                
+                signatureHelp.signatures = [signature];
+                signatureHelp.activeSignature = 0;
+                
+                // Determine active parameter
+                const paramSection = beforeCursor.match(/\(([^)]*)?$/)?.[1] || '';
+                const commaCount = (paramSection.match(/,/g) || []).length;
+                signatureHelp.activeParameter = Math.min(commaCount, paramList.length - 1);
+                
+                return signatureHelp;
+            }
+        }
+        
+        return undefined;
+    }
+}
+
diff --git a/plugin/src/testExplorer.ts b/plugin/src/testExplorer.ts
new file mode 100644
index 0000000..7de091c
--- /dev/null
+++ b/plugin/src/testExplorer.ts
@@ -0,0 +1,1193 @@
+import * as vscode from 'vscode';
+import * as path from 'path';
+import * as fs from 'fs';
+import * as os from 'os';
+import * as cp from 'child_process';
+import * as yaml from 'js-yaml';
+
+/** Test file pattern: tests folder anywhere in workspace; applies to files from that folder down (e.g. tests/, src/tests/, a/b/tests/) */
+const TEST_FILE_GLOB = '**/tests/**/*.isl';
+
+/** YAML test suite files: [name].tests.yaml that contain islTests property */
+const YAML_TEST_FILE_GLOB = '**/*.tests.yaml';
+
+/** Parsed test info from an ISL test file */
+export interface IslTestInfo {
+    id: string;
+    label: string;
+    group?: string;
+    functionName: string;
+    range: vscode.Range;
+    uri: vscode.Uri;
+}
+
+/** Parsed setup info */
+export interface IslSetupInfo {
+    functionName: string;
+    range: vscode.Range;
+}
+
+/** Parsed test entry from a *.tests.yaml file (islTests array) */
+export interface YamlTestEntryInfo {
+    id: string;
+    label: string;
+    functionName: string;
+    range: vscode.Range;
+    uri: vscode.Uri;
+}
+
+/**
+ * Check if YAML content looks like an ISL test suite (has top-level islTests array).
+ */
+export function yamlHasIslTests(content: string): boolean {
+    try {
+        const doc = yaml.load(content) as Record | null;
+        return !!doc && Array.isArray(doc.islTests);
+    } catch {
+        return false;
+    }
+}
+
+/**
+ * Parse islTests from a *.tests.yaml file. Returns test entries with id, label, functionName, range.
+ * Uses simple line scan to approximate range for each entry (line of "- name:" or "name:").
+ */
+export function parseYamlIslTests(uri: vscode.Uri, content: string): YamlTestEntryInfo[] {
+    const entries: YamlTestEntryInfo[] = [];
+    try {
+        const doc = yaml.load(content) as { islTests?: Array<{ name?: string; functionName?: string }> } | null;
+        const list = doc?.islTests;
+        if (!Array.isArray(list)) return entries;
+
+        const lines = content.split(/\r?\n/);
+        let listStartLine = -1;
+        for (let i = 0; i < lines.length; i++) {
+            const trimmed = lines[i].trim();
+            if (/^islTests\s*:/.test(trimmed)) {
+                listStartLine = i;
+                break;
+            }
+        }
+        if (listStartLine < 0) listStartLine = 0;
+
+        for (let idx = 0; idx < list.length; idx++) {
+            const entry = list[idx];
+            if (!entry || typeof entry !== 'object') continue;
+            const name = (entry.name != null && entry.name !== '') ? String(entry.name) : `Test ${idx + 1}`;
+            const functionName = (entry.functionName != null && entry.functionName !== '') ? String(entry.functionName) : name;
+            const id = `${uri.toString()}#${functionName}#${idx}`;
+            const line = listStartLine + 1 + idx;
+            const range = new vscode.Range(line, 0, line, 999);
+            entries.push({ id, label: name, functionName, range, uri });
+        }
+    } catch {
+        // ignore parse errors
+    }
+    return entries;
+}
+
+/**
+ * Parse @test and @setup annotations from ISL test file content.
+ * Supports:
+ *   @setup
+ *   fun setup() { ... }
+ *
+ *   @test or @test(optional name) or @test({ name: "test name", group: "test group" })
+ *   fun testABCD(...) { ... }
+ */
+export function parseIslTests(uri: vscode.Uri, content: string): { tests: IslTestInfo[]; setup?: IslSetupInfo } {
+    const tests: IslTestInfo[] = [];
+    let setup: IslSetupInfo | undefined;
+    const lines = content.split(/\r?\n/);
+
+    for (let i = 0; i < lines.length; i++) {
+        const line = lines[i];
+        const trimmed = line.trim();
+
+        // @setup - must be followed by fun setup()
+        const setupMatch = trimmed.match(/^@setup\s*$/);
+        if (setupMatch) {
+            const nextLine = lines[i + 1]?.trim() ?? '';
+            const funMatch = nextLine.match(/^\s*fun\s+(setup)\s*\(\s*\)/);
+            if (funMatch) {
+                const start = new vscode.Position(i, line.indexOf('@setup'));
+                const end = new vscode.Position(i, line.length);
+                setup = {
+                    functionName: 'setup',
+                    range: new vscode.Range(start, end)
+                };
+            }
+            continue;
+        }
+
+        // @test - various forms: @test, @test(), @test(name), @test({ name, group })
+        const testAnnMatch = trimmed.match(/^@test\s*(?:\(\s*(.*?)\s*\))?\s*$/);
+        if (testAnnMatch) {
+            const nextLine = lines[i + 1]?.trim() ?? '';
+            const funMatch = nextLine.match(/^\s*fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
+            if (funMatch) {
+                const functionName = funMatch[1];
+                const argPart = testAnnMatch[1]?.trim();
+
+                // When no name in annotation: use function name, strip test_ prefix if present
+                let label = functionName.startsWith('test_')
+                    ? functionName.slice(6)
+                    : functionName;
+                let group: string | undefined;
+
+                if (argPart) {
+                    // @test({ name: "x", group: "y" }) or @test("name") or @test(name)
+                    const objMatch = argPart.match(/\{\s*name\s*:\s*["']?([^"',}]+)["']?\s*(?:,\s*group\s*:\s*["']?([^"',}]+)["']?)?\s*\}/);
+                    if (objMatch) {
+                        label = objMatch[1].trim();
+                        group = objMatch[2]?.trim();
+                    } else {
+                        // Simple string: @test("My Test") or @test(My Test)
+                        const strMatch = argPart.match(/^["'](.+)["']$/);
+                        label = strMatch ? strMatch[1] : argPart;
+                    }
+                }
+
+                // Include line number in id so copy-pasted functions (same name) get unique ids
+                const id = `${uri.toString()}#${functionName}#${i}`;
+                const start = new vscode.Position(i, line.indexOf('@test'));
+                const end = new vscode.Position(i, line.length);
+
+                tests.push({
+                    id,
+                    label,
+                    group,
+                    functionName,
+                    range: new vscode.Range(start, end),
+                    uri
+                });
+            }
+        }
+    }
+
+    return { tests, setup };
+}
+
+/** JSON output from isl-cmd test -o output.json */
+interface IslTestResultJson {
+    passed: number;
+    failed: number;
+    total: number;
+    success: boolean;
+    results: Array<{
+        testFile: string;
+        functionName: string;
+        testName: string;
+        testGroup: string | null;
+        success: boolean;
+        message: string | null;
+        errorPosition: {
+            file: string;
+            line: number;
+            column: number;
+            endLine?: number | null;
+            endColumn?: number | null;
+        } | null;
+    }>;
+}
+
+/** Parsed assertion difference from "[ISL Assert] Difference(s):" message (path, expected, actual). */
+export interface AssertDiff {
+    path: string;
+    expected: string;
+    actual: string;
+    /** Leaf key to find in YAML expected section (e.g. initialLoanDate). */
+    key: string;
+}
+
+/**
+ * Parse "[ISL Assert] Difference(s):" message into path/expected/actual pairs.
+ * Format:
+ *   Expected: $path = value
+ *   Actual:   $path = value
+ */
+export function parseAssertDiffsFromMessage(message: string | null): AssertDiff[] {
+    const diffs: AssertDiff[] = [];
+    if (!message || !message.includes('Difference(s):') || !message.includes('Expected:')) return diffs;
+    const lines = message.split(/\r?\n/);
+    for (let i = 0; i < lines.length - 1; i++) {
+        const expMatch = lines[i].match(/^\s*Expected:\s*(\$[^\s=]+)\s*=\s*(.*)$/);
+        if (!expMatch) continue;
+        const actMatch = lines[i + 1].match(/^\s*Actual:\s*(\$[^\s=]+)\s*=\s*(.*)$/);
+        if (!actMatch || expMatch[1] !== actMatch[1]) continue;
+        const pathStr = expMatch[1];
+        const expectedVal = expMatch[2].trim();
+        const actualVal = actMatch[2].trim();
+        const key = getLeafKeyFromPath(pathStr);
+        diffs.push({ path: pathStr, expected: expectedVal, actual: actualVal, key });
+        i++;
+    }
+    return diffs;
+}
+
+/** Extract leaf key from JSON path like $providerResponses.accounts.[0].initialLoanDate -> initialLoanDate */
+function getLeafKeyFromPath(pathStr: string): string {
+    const trimmed = pathStr.replace(/^\$\.?/, '').trim();
+    if (!trimmed) return '';
+    const segments = trimmed.split(/\./).filter(s => s.length > 0);
+    for (let i = segments.length - 1; i >= 0; i--) {
+        const s = segments[i];
+        if (!/^\[\d+\]$/.test(s)) return s;
+    }
+    return segments[segments.length - 1] ?? '';
+}
+
+/**
+ * Find ranges in YAML content where the given key appears inside the "expected" section of the test
+ * that matches functionName. Returns one range per occurrence (for squiggles).
+ */
+export function findExpectedKeyRangesInYaml(
+    content: string,
+    functionName: string,
+    key: string
+): vscode.Range[] {
+    if (!key) return [];
+    const lines = content.split(/\r?\n/);
+    const ranges: vscode.Range[] = [];
+
+    let islTestsIndent = -1;
+    let entryStart = -1;
+    let matchingTest = false;
+    let expectedStart = -1;
+    let expectedEnd = -1;
+
+    for (let i = 0; i < lines.length; i++) {
+        const line = lines[i];
+        const trimmed = line.trim();
+        const indent = line.search(/\S/);
+        if (indent < 0) continue;
+
+        if (/^islTests\s*:/.test(trimmed)) {
+            islTestsIndent = indent;
+            continue;
+        }
+        if (islTestsIndent < 0) continue;
+
+        const isListItem = /^\s*-\s+/.test(line) && indent <= islTestsIndent + 2;
+        if (isListItem) {
+            if (expectedStart >= 0 && matchingTest) {
+                expectedEnd = i;
+                for (let j = expectedStart; j < expectedEnd; j++) {
+                    pushKeyRanges(lines[j], j, key, ranges);
+                }
+            }
+            entryStart = i;
+            matchingTest = false;
+            expectedStart = -1;
+        }
+
+        if (entryStart >= 0) {
+            const fnMatch = trimmed.match(/^(?:functionName|name)\s*:\s*["']?([^"'\s]+)["']?/);
+            if (fnMatch && fnMatch[1].trim() === functionName) matchingTest = true;
+            if (/^expected\s*:/.test(trimmed) && matchingTest) {
+                expectedStart = i;
+            }
+        }
+    }
+
+    if (expectedStart >= 0 && matchingTest && expectedEnd < 0) {
+        expectedEnd = lines.length;
+        for (let j = expectedStart; j < expectedEnd; j++) {
+            const l = lines[j];
+            const lineIndent = l.search(/\S/);
+            const expIndent = lines[expectedStart].search(/\S/);
+            if (j > expectedStart && l.trim() && lineIndent <= expIndent) {
+                expectedEnd = j;
+                break;
+            }
+            pushKeyRanges(l, j, key, ranges);
+        }
+    }
+
+    return ranges;
+}
+
+function pushKeyRanges(line: string, lineIndex: number, key: string, ranges: vscode.Range[]): void {
+    let col = line.indexOf(key + ':');
+    if (col === -1) {
+        const jsonMatch = line.match(new RegExp(`"${escapeRegex(key)}"\\s*:`));
+        if (jsonMatch && jsonMatch.index !== undefined) col = jsonMatch.index;
+    }
+    if (col === -1 && line.includes(key)) {
+        const quoted = line.indexOf('"' + key + '"');
+        if (quoted >= 0) col = quoted + 1;
+        else col = line.indexOf(key);
+    }
+    if (col >= 0) {
+        const endCol = Math.min(line.length, col + key.length);
+        ranges.push(new vscode.Range(lineIndex, col, lineIndex, endCol));
+    }
+}
+
+function escapeRegex(s: string): string {
+    return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+/** Parsed "No mock matched" / "Unmocked function" error from the test runner (suggested mock to add). */
+export interface MockSuggestion {
+    functionName: string;
+    params: string; // JSON array string, e.g. '["start_date"]'
+    mockFileName: string;
+    yamlSnippet: string; // lines to add under func: (e.g. "- name: ...\n  params: ...\n  result: ...")
+    /** When true, add to the test file (setup.mocks) instead of an imported mock file. */
+    addToTestFile?: boolean;
+}
+
+/**
+ * Parse backend "No mock matched" / "Unmocked function" error message to extract mock suggestion.
+ * Returns null if the message does not match that format.
+ */
+export function parseMockSuggestionFromError(message: string | null): MockSuggestion | null {
+    if (!message || !message.includes('To mock this function add this to your')) {
+        return null;
+    }
+    const lines = message.split(/\r?\n/);
+    let functionName = '';
+    let params = '[]';
+    let mockFileName = 'commonMocks.yaml';
+    let addToTestFile = false;
+    let inSnippet = false;
+    const snippetLines: string[] = [];
+
+    for (const line of lines) {
+        const fnMatch = line.match(/^Function:\s*@\.(.+)$/);
+        if (fnMatch) {
+            functionName = fnMatch[1].trim();
+            continue;
+        }
+        const paramsMatch = line.match(/^Parameters:\s*(.+)$/);
+        if (paramsMatch) {
+            params = paramsMatch[1].trim();
+            continue;
+        }
+        const testFileMatch = line.match(/To mock this function add this to your test file \[([^\]]+)\]/);
+        if (testFileMatch) {
+            mockFileName = testFileMatch[1].trim();
+            addToTestFile = true;
+            inSnippet = true;
+            continue;
+        }
+        const fileMatch = line.match(/To mock this function add this to your \[([^\]]+)\]/);
+        if (fileMatch) {
+            mockFileName = fileMatch[1].trim();
+            inSnippet = true;
+            continue;
+        }
+        if (inSnippet) {
+            if (line.trim() === '' || line.trim() === 'func:') continue;
+            if (line.startsWith('- ') || (line.match(/^\s{2,}/) && snippetLines.length > 0)) {
+                snippetLines.push(line);
+            } else if (snippetLines.length > 0) {
+                break;
+            }
+        }
+    }
+    if (!functionName && snippetLines.length === 0) return null;
+    const yamlSnippet = snippetLines.join('\n').trim();
+    if (!yamlSnippet) return null;
+    return { functionName, params, mockFileName, yamlSnippet, addToTestFile: addToTestFile || undefined };
+}
+
+/**
+ * Build a TestMessage for a failed test. If the message is a "No mock matched" / "Unmocked function" error,
+ * appends an "Add mock to test file (setup)" or "Add mock to <file>" link that runs the add-mock command.
+ */
+export function buildTestFailureMessage(
+    rawMessage: string,
+    testUri: vscode.Uri | undefined,
+    suggestion: MockSuggestion | null
+): vscode.TestMessage {
+    if (!suggestion || !testUri) {
+        return new vscode.TestMessage(rawMessage);
+    }
+    const args = [
+        testUri.toString(),
+        suggestion.mockFileName,
+        suggestion.functionName,
+        suggestion.params,
+        suggestion.yamlSnippet,
+        suggestion.addToTestFile === true
+    ];
+    // Double-stringify so the command receives one string argument with the full array (Test UI may pass only the first element when passing a raw array).
+    const payload = JSON.stringify(JSON.stringify(args));
+    const cmdUri = `command:isl.addMockFromTestError?${encodeURIComponent(payload)}`;
+    const linkLabel = suggestion.addToTestFile ? 'Add mock to test file (setup)' : `Add mock to ${suggestion.mockFileName}`;
+    const md = new vscode.MarkdownString(undefined);
+    md.appendMarkdown(rawMessage);
+    md.appendMarkdown('\n\n---\n\n');
+    md.appendMarkdown(`[**${linkLabel}**](${cmdUri})`);
+    md.isTrusted = true;
+    return new vscode.TestMessage(md);
+}
+
+/**
+ * Add the suggested mock entry to the test file under setup.mocks.func (inline mocks in the test file).
+ */
+export async function addMockToTestFile(
+    testFileUri: vscode.Uri,
+    functionName: string,
+    paramsJson: string,
+    yamlSnippet: string,
+    outputChannel: vscode.OutputChannel
+): Promise {
+    const raw = await vscode.workspace.fs.readFile(testFileUri);
+    const content = new TextDecoder().decode(raw);
+    const doc = (yaml.load(content) as Record) || {};
+    let setup = doc.setup;
+    if (!setup || typeof setup !== 'object') {
+        setup = { islSource: (setup as Record)?.islSource ?? '' };
+        doc.setup = setup;
+    }
+    const setupObj = setup as Record;
+    let mocks = setupObj.mocks;
+    if (!mocks || typeof mocks !== 'object') {
+        mocks = {};
+        setupObj.mocks = mocks;
+    }
+    const mocksObj = mocks as Record;
+    let funcList = mocksObj.func;
+    if (!Array.isArray(funcList)) {
+        funcList = [];
+        mocksObj.func = funcList;
+    }
+    try {
+        const newEntry = yaml.load(yamlSnippet) as unknown;
+        const entry = Array.isArray(newEntry) ? newEntry[0] : newEntry;
+        if (entry && typeof entry === 'object') {
+            (funcList as unknown[]).push(entry);
+        } else {
+            const entryFromParts = { name: functionName, result: '' };
+            try {
+                const p = JSON.parse(paramsJson) as unknown[];
+                if (p.length > 0) (entryFromParts as Record).params = p;
+            } catch {
+                // ignore
+            }
+            (funcList as unknown[]).push(entryFromParts);
+        }
+    } catch {
+        const entryFromParts = { name: functionName, result: '' };
+        try {
+            const p = JSON.parse(paramsJson) as unknown[];
+            if (p.length > 0) (entryFromParts as Record).params = p;
+        } catch {
+            // ignore
+        }
+        (funcList as unknown[]).push(entryFromParts);
+    }
+    const newContent = yaml.dump(doc, { lineWidth: -1, noRefs: true });
+    await vscode.workspace.fs.writeFile(testFileUri, new TextEncoder().encode(newContent));
+    outputChannel.appendLine(`Added mock for @.${functionName} to test file (setup.mocks) at ${testFileUri.fsPath}`);
+    await vscode.window.showTextDocument(testFileUri, { preview: false });
+}
+
+/**
+ * Add the suggested mock entry to the mock file (relative to the test file's directory).
+ * Resolves mockFileName relative to the directory of testFileUri. Creates the file if missing.
+ */
+export async function addMockToFile(
+    testFileUri: vscode.Uri,
+    mockFileName: string,
+    functionName: string,
+    paramsJson: string,
+    yamlSnippet: string,
+    outputChannel: vscode.OutputChannel
+): Promise {
+    const testDir = path.dirname(testFileUri.fsPath);
+    const mockPath = path.resolve(testDir, mockFileName);
+    const mockUri = vscode.Uri.file(mockPath);
+
+    let doc: Record = {};
+    try {
+        const raw = await vscode.workspace.fs.readFile(mockUri);
+        const content = new TextDecoder().decode(raw);
+        if (content.trim()) {
+            doc = (yaml.load(content) as Record) || {};
+        }
+    } catch {
+        // File does not exist or not readable; start with empty doc
+    }
+
+    let funcList = doc.func;
+    if (!Array.isArray(funcList)) {
+        funcList = [];
+    }
+    try {
+        const newEntry = yaml.load(yamlSnippet) as unknown;
+        const entry = Array.isArray(newEntry) ? newEntry[0] : newEntry;
+        if (entry && typeof entry === 'object') {
+            (funcList as unknown[]).push(entry);
+        } else {
+            const entryFromParts = { name: functionName, result: '' };
+            try {
+                const p = JSON.parse(paramsJson) as unknown[];
+                if (p.length > 0) (entryFromParts as Record).params = p;
+            } catch {
+                // ignore
+            }
+            (funcList as unknown[]).push(entryFromParts);
+        }
+    } catch {
+        const entryFromParts = { name: functionName, result: '' };
+        try {
+            const p = JSON.parse(paramsJson) as unknown[];
+            if (p.length > 0) (entryFromParts as Record).params = p;
+        } catch {
+            // ignore
+        }
+        (funcList as unknown[]).push(entryFromParts);
+    }
+
+    doc.func = funcList;
+    const newContent = yaml.dump(doc, { lineWidth: -1, noRefs: true });
+    await vscode.workspace.fs.writeFile(mockUri, new TextEncoder().encode(newContent));
+    outputChannel.appendLine(`Added mock for @.${functionName} to ${mockPath}`);
+    const docOpened = await vscode.workspace.openTextDocument(mockUri);
+    await vscode.window.showTextDocument(docOpened, { preview: false });
+}
+
+/**
+ * Check if a file path is under tests/ (e.g. tests/foo.isl or src/tests/bar.isl)
+ */
+export function isTestFile(uri: vscode.Uri, workspaceFolder: vscode.WorkspaceFolder): boolean {
+    const rel = path.relative(workspaceFolder.uri.fsPath, uri.fsPath);
+    const parts = rel.split(path.sep);
+    return parts.some(p => p === 'tests');
+}
+
+/**
+ * ISL Test Explorer - discovers @test and @setup in tests folder and registers with VS Code Test API.
+ */
+/** Debounce delay for document change handlers (ms) */
+const DOCUMENT_CHANGE_DEBOUNCE_MS = 1500;
+
+/** Diagnostic collection for YAML test assertion diffs (squiggles in expected section). */
+const YAML_TEST_DIAGNOSTIC_SOURCE = 'isl-yaml-test';
+
+export class IslTestController {
+    private readonly controller: vscode.TestController;
+    private readonly watchers: vscode.FileSystemWatcher[] = [];
+    private readonly outputChannel: vscode.OutputChannel;
+    private readonly extensionPath: string;
+    private readonly assertionDiagnostics: vscode.DiagnosticCollection;
+    private documentChangeTimeouts = new Map();
+    private runProfileHandler!: (request: vscode.TestRunRequest, token: vscode.CancellationToken) => Promise;
+
+    constructor(outputChannel: vscode.OutputChannel, extensionPath: string) {
+        this.outputChannel = outputChannel;
+        this.extensionPath = extensionPath;
+        this.assertionDiagnostics = vscode.languages.createDiagnosticCollection(YAML_TEST_DIAGNOSTIC_SOURCE);
+        this.controller = vscode.tests.createTestController('isl-tests', 'ISL Tests');
+
+        this.controller.resolveHandler = async (item) => {
+            if (!item) {
+                await this.discoverAllTestFiles();
+                // Parse all files so the Test Explorer is fully populated (no need to expand each node)
+                for (const [, fileItem] of this.controller.items) {
+                    await this.parseTestsInFile(fileItem);
+                }
+            } else {
+                await this.parseTestsInFile(item);
+            }
+        };
+
+        // Refresh button - re-discover and re-parse all test files
+        this.controller.refreshHandler = async () => {
+            this.documentChangeTimeouts.forEach(t => clearTimeout(t));
+            this.documentChangeTimeouts.clear();
+            this.controller.items.replace([]);
+            await this.discoverAllTestFiles();
+            for (const [, item] of this.controller.items) {
+                await this.parseTestsInFile(item);
+            }
+        };
+
+        this.runProfileHandler = (request, token) => this.runTests(request, token);
+        this.controller.createRunProfile(
+            'Run',
+            vscode.TestRunProfileKind.Run,
+            this.runProfileHandler
+        );
+
+        // Watch for test file changes
+        this.setupWatchers();
+        // Parse open documents
+        vscode.workspace.textDocuments.forEach(doc => this.parseTestsInDocument(doc));
+        vscode.workspace.onDidOpenTextDocument(doc => this.parseTestsInDocument(doc));
+        vscode.workspace.onDidChangeTextDocument(e => this.debouncedParseTestsInDocument(e.document));
+
+        // Populate Test Explorer on load: discover and parse all test files when workspace is ready
+        this.scheduleInitialDiscovery();
+    }
+
+    /** Run discovery + parse once when workspace has folders, so Test Explorer is populated on project load. */
+    private scheduleInitialDiscovery(): void {
+        const run = () => {
+            if (!vscode.workspace.workspaceFolders?.length) return;
+            this.discoverAllTestFiles().then(() => {
+                for (const [, fileItem] of this.controller.items) {
+                    this.parseTestsInFile(fileItem);
+                }
+            });
+        };
+        // Run after a short delay so workspace is ready; also run when folders change (e.g. folder added)
+        setTimeout(run, 800);
+        vscode.workspace.onDidChangeWorkspaceFolders(() => run());
+    }
+
+    private debouncedParseTestsInDocument(doc: vscode.TextDocument): void {
+        if (doc.uri.scheme !== 'file') return;
+        const isIsl = doc.uri.fsPath.endsWith('.isl');
+        const isYamlTests = doc.uri.fsPath.endsWith('.tests.yaml');
+        if (!isIsl && !isYamlTests) return;
+        if (isIsl) {
+            const folder = vscode.workspace.getWorkspaceFolder(doc.uri);
+            if (!folder || !isTestFile(doc.uri, folder)) return;
+        }
+        if (isYamlTests && !yamlHasIslTests(doc.getText())) return;
+
+        const key = doc.uri.toString();
+        const existing = this.documentChangeTimeouts.get(key);
+        if (existing) clearTimeout(existing);
+
+        const timeout = setTimeout(() => {
+            this.documentChangeTimeouts.delete(key);
+            this.parseTestsInDocument(doc);
+        }, DOCUMENT_CHANGE_DEBOUNCE_MS);
+        this.documentChangeTimeouts.set(key, timeout);
+    }
+
+    private setupWatchers(): void {
+        const folders = vscode.workspace.workspaceFolders;
+        if (!folders) return;
+
+        for (const folder of folders) {
+            const islPattern = new vscode.RelativePattern(folder, TEST_FILE_GLOB);
+            const islWatcher = vscode.workspace.createFileSystemWatcher(islPattern);
+            islWatcher.onDidCreate(uri => this.getOrCreateFile(uri));
+            islWatcher.onDidChange(uri => this.parseTestsInFile(this.getOrCreateFile(uri)));
+            islWatcher.onDidDelete(uri => this.controller.items.delete(uri.toString()));
+            this.watchers.push(islWatcher);
+
+            const yamlPattern = new vscode.RelativePattern(folder, YAML_TEST_FILE_GLOB);
+            const yamlWatcher = vscode.workspace.createFileSystemWatcher(yamlPattern);
+            yamlWatcher.onDidCreate(uri => this.getOrCreateFile(uri));
+            yamlWatcher.onDidChange(uri => this.parseTestsInFile(this.getOrCreateFile(uri)));
+            yamlWatcher.onDidDelete(uri => this.controller.items.delete(uri.toString()));
+            this.watchers.push(yamlWatcher);
+        }
+    }
+
+    private getOrCreateFile(uri: vscode.Uri): vscode.TestItem {
+        const id = uri.toString();
+        const existing = this.controller.items.get(id);
+        if (existing) return existing;
+
+        const file = this.controller.createTestItem(id, path.basename(uri.fsPath), uri);
+        file.canResolveChildren = true;
+        this.controller.items.add(file);
+        return file;
+    }
+
+    private async discoverAllTestFiles(): Promise {
+        const folders = vscode.workspace.workspaceFolders;
+        if (!folders) return;
+
+        for (const folder of folders) {
+            const islPattern = new vscode.RelativePattern(folder, TEST_FILE_GLOB);
+            const islFiles = await vscode.workspace.findFiles(islPattern);
+            for (const uri of islFiles) {
+                this.getOrCreateFile(uri);
+            }
+            const yamlPattern = new vscode.RelativePattern(folder, YAML_TEST_FILE_GLOB);
+            const yamlFiles = await vscode.workspace.findFiles(yamlPattern);
+            for (const uri of yamlFiles) {
+                let raw: Uint8Array;
+                try {
+                    raw = await vscode.workspace.fs.readFile(uri);
+                } catch {
+                    raw = new Uint8Array(0);
+                }
+                const content = new TextDecoder().decode(raw);
+                if (yamlHasIslTests(content)) {
+                    this.getOrCreateFile(uri);
+                }
+            }
+        }
+    }
+
+    private parseTestsInDocument(doc: vscode.TextDocument): void {
+        if (doc.uri.scheme !== 'file') return;
+        const isIsl = doc.uri.fsPath.endsWith('.isl');
+        const isYamlTests = doc.uri.fsPath.endsWith('.tests.yaml');
+        if (!isIsl && !isYamlTests) return;
+        if (isIsl) {
+            const folder = vscode.workspace.getWorkspaceFolder(doc.uri);
+            if (!folder || !isTestFile(doc.uri, folder)) return;
+        } else if (isYamlTests && !yamlHasIslTests(doc.getText())) return;
+
+        const file = this.getOrCreateFile(doc.uri);
+        this.parseTestsInFile(file, doc.getText());
+    }
+
+    private async parseTestsInFile(file: vscode.TestItem, contents?: string): Promise {
+        if (!file.uri) return;
+
+        if (contents === undefined) {
+            try {
+                const raw = await vscode.workspace.fs.readFile(file.uri);
+                contents = new TextDecoder().decode(raw);
+            } catch {
+                return;
+            }
+        }
+
+        file.children.replace([]);
+        const isYaml = file.uri.fsPath.endsWith('.tests.yaml');
+        if (isYaml) {
+            if (!yamlHasIslTests(contents)) return;
+            const yamlTests = parseYamlIslTests(file.uri, contents);
+            for (const t of yamlTests) {
+                const item = this.controller.createTestItem(t.id, t.label, file.uri);
+                item.range = t.range;
+                file.children.add(item);
+            }
+        } else {
+            const { tests } = parseIslTests(file.uri, contents);
+            for (const t of tests) {
+                const item = this.controller.createTestItem(t.id, t.label, file.uri);
+                item.range = t.range;
+                if (t.group) {
+                    item.description = t.group;
+                }
+                file.children.add(item);
+            }
+        }
+    }
+
+    private async runTests(request: vscode.TestRunRequest, token: vscode.CancellationToken): Promise {
+        const run = this.controller.createTestRun(request);
+        const testsToRun = this.collectLeafTests(request);
+
+        if (testsToRun.length === 0) {
+            run.end();
+            return;
+        }
+
+        // Resolve file nodes to get children
+        for (const test of testsToRun) {
+            const parent = test.parent;
+            if (parent?.canResolveChildren && parent.children.size === 0) {
+                await this.parseTestsInFile(parent);
+            }
+        }
+
+        const javaPath = await this.findJava();
+        if (!javaPath) {
+            run.appendOutput('Java not found. Install Java 11+ or configure isl.execution.javaHome.\r\n');
+            for (const test of testsToRun) {
+                run.started(test);
+                run.errored(test, new vscode.TestMessage('Java not found'));
+            }
+            run.end();
+            return;
+        }
+
+        const jarPath = path.join(this.extensionPath, 'lib', 'isl-cmd-all.jar');
+        if (!fs.existsSync(jarPath)) {
+            run.appendOutput('ISL runtime not found (isl-cmd-all.jar). Reinstall the extension.\r\n');
+            for (const test of testsToRun) {
+                run.started(test);
+                run.errored(test, new vscode.TestMessage('ISL runtime not found'));
+            }
+            run.end();
+            return;
+        }
+
+        const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
+        if (!workspaceFolder) {
+            run.appendOutput('No workspace folder open.\r\n');
+            for (const test of testsToRun) {
+                run.started(test);
+                run.errored(test, new vscode.TestMessage('No workspace folder'));
+            }
+            run.end();
+            return;
+        }
+
+        // Determine path to run: single file or directory
+        const fileUris = new Set();
+        for (const t of testsToRun) {
+            if (t.uri) fileUris.add(t.uri.toString());
+        }
+        const filePaths = Array.from(fileUris).map(u => vscode.Uri.parse(u).fsPath);
+        const runPath = filePaths.length === 1
+            ? filePaths[0]
+            : this.getCommonParent(filePaths) ?? workspaceFolder.uri.fsPath;
+
+        const outputFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'isl-test-')), 'results.json');
+        const env = { ...process.env };
+        const config = vscode.workspace.getConfiguration('isl.execution');
+        const javaHome = config.get('javaHome', '');
+        if (javaHome) {
+            env.JAVA_HOME = javaHome;
+            env.PATH = `${path.join(javaHome, 'bin')}${path.delimiter}${env.PATH}`;
+        }
+
+        const isFile = fs.existsSync(runPath) && fs.statSync(runPath).isFile();
+        const searchBase = isFile ? path.dirname(runPath) : runPath;
+
+        // Build function filter for ISL CLI -f/--function (run only selected tests)
+        const functionFilters: string[] = [];
+        for (const t of testsToRun) {
+            const fn = t.id.split('#')[1] ?? t.label;
+            if (!t.uri) {
+                functionFilters.push(fn);
+            } else if (filePaths.length === 1) {
+                functionFilters.push(fn);
+            } else {
+                const relPath = path.relative(searchBase, t.uri.fsPath).replace(/\\/g, '/');
+                functionFilters.push(`${relPath}:${fn}`);
+            }
+        }
+
+        run.appendOutput(`=== ISL Test Run ===\r\n`);
+        this.outputChannel.show(true);
+        run.appendOutput(`Path: ${runPath}\r\n`);
+        run.appendOutput(`Mode: ${isFile ? 'single file' : 'directory'}\r\n`);
+        run.appendOutput(`Tests to run: ${testsToRun.map(t => t.id.split('#')[1] ?? t.label).join(', ')}\r\n`);
+        const cmdLine = functionFilters.length > 0
+            ? `java -jar isl-cmd-all.jar test "${runPath}" -o "${outputFile}" ${functionFilters.map(f => `-f "${f}"`).join(' ')}`
+            : `java -jar isl-cmd-all.jar test "${runPath}" -o "${outputFile}"`;
+        run.appendOutput(`Command: ${cmdLine}\r\n`);
+        run.appendOutput(`\r\n`);
+
+        let execStdout = '';
+        let execStderr = '';
+        try {
+            const execResult = await this.execJavaTest(javaPath, jarPath, runPath, outputFile, functionFilters, env);
+            execStdout = execResult.stdout;
+            execStderr = execResult.stderr;
+        } catch (err: unknown) {
+            const msg = err instanceof Error ? err.message : String(err);
+            run.appendOutput(`Execution failed: ${msg}\r\n`);
+            for (const test of testsToRun) {
+                run.started(test);
+                run.errored(test, new vscode.TestMessage(msg));
+            }
+            run.end();
+            return;
+        }
+
+        let results: IslTestResultJson;
+        try {
+            const raw = fs.readFileSync(outputFile, 'utf-8');
+            results = JSON.parse(raw) as IslTestResultJson;
+        } catch {
+            run.appendOutput('Failed to parse test results JSON.\r\n');
+            // Always show runner output so user can see what isl-cmd actually printed
+            if (execStdout) {
+                run.appendOutput(`--- Runner stdout ---\r\n`);
+                run.appendOutput(execStdout.replace(/\n/g, '\r\n') + '\r\n');
+            }
+            if (execStderr) {
+                run.appendOutput(`--- Runner stderr ---\r\n`);
+                run.appendOutput(execStderr.replace(/\n/g, '\r\n') + '\r\n');
+            }
+            try {
+                const rawContent = fs.readFileSync(outputFile, 'utf-8');
+                run.appendOutput(`--- Output file (results.json) ---\r\n`);
+                run.appendOutput(rawContent.length ? rawContent.replace(/\n/g, '\r\n') + '\r\n' : '(empty)\r\n');
+            } catch {
+                run.appendOutput(`--- Output file could not be read ---\r\n`);
+            }
+            for (const test of testsToRun) {
+                run.started(test);
+                run.errored(test, new vscode.TestMessage('Failed to parse results'));
+            }
+            run.end();
+            return;
+        } finally {
+            try { fs.unlinkSync(outputFile); } catch { /* ignore */ }
+            try { fs.rmdirSync(path.dirname(outputFile)); } catch { /* ignore */ }
+        }
+
+        if (execStdout) {
+            run.appendOutput(`--- Runner stdout ---\r\n`);
+            run.appendOutput(execStdout.replace(/\n/g, '\r\n') + '\r\n');
+        }
+        if (execStderr) {
+            run.appendOutput(`--- Runner stderr ---\r\n`);
+            run.appendOutput(execStderr.replace(/\n/g, '\r\n') + '\r\n');
+        }
+
+        run.appendOutput(`--- Parsed results: ${results.results.length} test(s) ---\r\n`);
+        for (const tr of results.results) {
+            run.appendOutput(`  ${tr.testFile} :: ${tr.functionName} => ${tr.success ? 'PASS' : 'FAIL'}\r\n`);
+        }
+        run.appendOutput(`\r\n`);
+
+        const testById = new Map();
+        for (const t of testsToRun) {
+            testById.set(t.id, t);
+        }
+
+        const matched = new Set();
+        const uriToAssertDiags = new Map();
+
+        for (const t of testsToRun) {
+            if (t.uri?.fsPath.endsWith('.tests.yaml')) {
+                this.assertionDiagnostics.delete(t.uri);
+            }
+        }
+
+        for (const tr of results.results) {
+            const test = this.findTestById(testById, tr);
+            if (!test) continue;
+
+            matched.add(test);
+            run.started(test);
+            const duration = 0;
+
+            if (tr.success) {
+                run.passed(test, duration);
+            } else {
+                const rawMessage = tr.message ?? 'Test failed';
+                const mockSuggestion = parseMockSuggestionFromError(tr.message ?? null);
+                const msg = buildTestFailureMessage(rawMessage, test.uri, mockSuggestion);
+                if (tr.errorPosition) {
+                    const fallbackUri = test.uri ?? workspaceFolder.uri;
+                    const loc = this.resolveErrorLocation(tr.errorPosition, searchBase, workspaceFolder.uri.fsPath, fallbackUri);
+                    if (loc) msg.location = loc;
+                }
+                run.failed(test, msg, duration);
+
+                if (test.uri?.fsPath.endsWith('.tests.yaml')) {
+                    const diffs = parseAssertDiffsFromMessage(tr.message ?? null);
+                    if (diffs.length > 0) {
+                        try {
+                            const doc = await vscode.workspace.openTextDocument(test.uri);
+                            const content = doc.getText();
+                            const fn = tr.functionName;
+                            const existing = uriToAssertDiags.get(test.uri.toString()) ?? [];
+                            for (const d of diffs) {
+                                const ranges = findExpectedKeyRangesInYaml(content, fn, d.key);
+                                const message = `Expected: ${d.expected}, Actual: ${d.actual}`;
+                                for (const r of ranges) {
+                                    existing.push(new vscode.Diagnostic(r, message, vscode.DiagnosticSeverity.Error));
+                                }
+                            }
+                            uriToAssertDiags.set(test.uri.toString(), existing);
+                        } catch {
+                            // ignore (e.g. file no longer open)
+                        }
+                    }
+                }
+            }
+        }
+
+        for (const [uriStr, diags] of uriToAssertDiags) {
+            this.assertionDiagnostics.set(vscode.Uri.parse(uriStr), diags);
+        }
+
+        for (const test of testsToRun) {
+            if (!matched.has(test)) {
+                const fn = test.id.split('#')[1] ?? test.label;
+                const file = test.uri ? path.basename(test.uri.fsPath) : '?';
+                run.started(test);
+                run.errored(
+                    test,
+                    new vscode.TestMessage(`Test not found in results. Looking for: file="${file}", function="${fn}". Check that the CLI discovered this test (see parsed results above).`)
+                );
+                run.appendOutput(`  [UNMATCHED] ${file} :: ${fn} (id: ${test.id})\r\n`);
+            }
+        }
+
+        run.appendOutput(`\r\n=== Summary ===\r\n`);
+        run.appendOutput(`Results: ${results.passed} passed, ${results.failed} failed, ${results.total} total\r\n`);
+        run.end();
+    }
+
+    private collectLeafTests(request: vscode.TestRunRequest): vscode.TestItem[] {
+        const leaves: vscode.TestItem[] = [];
+        const queue: vscode.TestItem[] = request.include
+            ? Array.from(request.include)
+            : Array.from(this.controller.items).map(([, item]) => item);
+
+        while (queue.length > 0) {
+            const item = queue.shift()!;
+            if (request.exclude?.includes(item)) continue;
+            if (item.children.size > 0) {
+                item.children.forEach((child) => queue.push(child));
+            } else {
+                leaves.push(item);
+            }
+        }
+        return leaves;
+    }
+
+    private getCommonParent(paths: string[]): string | null {
+        if (paths.length === 0) return null;
+        let common = path.dirname(paths[0]);
+        for (let i = 1; i < paths.length; i++) {
+            const dir = path.dirname(paths[i]);
+            while (!dir.startsWith(common) && common !== path.dirname(common)) {
+                common = path.dirname(common);
+            }
+        }
+        return common;
+    }
+
+    /**
+     * Resolve errorPosition from the CLI to a VS Code Location so the test failure "arrow" opens the correct file/line.
+     * errorPosition.file is relative to the test run cwd (searchBase). Falls back to testUri if resolution fails.
+     */
+    private resolveErrorLocation(
+        errorPosition: NonNullable,
+        searchBase: string,
+        workspaceRoot: string,
+        testUri: vscode.Uri
+    ): vscode.Location | null {
+        const line = Math.max(0, (errorPosition.line ?? 1) - 1);
+        const column = Math.max(0, (errorPosition.column ?? 1) - 1);
+        const file = errorPosition.file?.trim();
+        if (!file) {
+            return new vscode.Location(testUri, new vscode.Position(line, column));
+        }
+        const normalized = file.replace(/\//g, path.sep);
+        let resolved = path.resolve(searchBase, normalized);
+        if (!fs.existsSync(resolved)) {
+            resolved = path.resolve(workspaceRoot, normalized);
+        }
+        if (!fs.existsSync(resolved)) {
+            return new vscode.Location(testUri, new vscode.Position(line, column));
+        }
+        const uri = vscode.Uri.file(resolved);
+        const endLine = errorPosition.endLine != null && errorPosition.endLine > 0
+            ? Math.max(0, errorPosition.endLine - 1)
+            : line;
+        const endColumn = errorPosition.endColumn != null && errorPosition.endColumn > 0
+            ? Math.max(0, errorPosition.endColumn - 1)
+            : column;
+        const range = (endLine !== line || endColumn !== column)
+            ? new vscode.Range(line, column, endLine, endColumn)
+            : new vscode.Range(line, column, line, column);
+        return new vscode.Location(uri, range);
+    }
+
+    private findTestById(testById: Map, tr: IslTestResultJson['results'][0]): vscode.TestItem | undefined {
+        const fnLower = tr.functionName.toLowerCase();
+        const resultFile = tr.testFile.toLowerCase().replace(/\\/g, '/');
+        const resultFileBase = path.basename(resultFile);
+
+        for (const [id, test] of testById) {
+            // Id format: uri#functionName or uri#functionName#index
+            const idFn = id.split('#')[1] ?? '';
+            if (idFn.toLowerCase() !== fnLower) continue;
+
+            if (!test.uri) return test;
+
+            const testBase = path.basename(test.uri.fsPath).toLowerCase();
+            const testPath = test.uri.fsPath.toLowerCase().replace(/\\/g, '/');
+
+            // Match: exact path, exact filename, or result path ends with our filename (or vice versa)
+            if (
+                resultFile === testPath ||
+                resultFile === testBase ||
+                resultFileBase === testBase ||
+                resultFile.endsWith('/' + testBase) ||
+                resultFile.endsWith('\\' + testBase) ||
+                testPath.endsWith(resultFile) ||
+                testPath.endsWith(resultFile.replace(/\//g, path.sep))
+            ) {
+                return test;
+            }
+        }
+        return undefined;
+    }
+
+    private execJavaTest(javaPath: string, jarPath: string, runPath: string, outputFile: string, functionFilters: string[], env: NodeJS.ProcessEnv): Promise<{ stdout: string; stderr: string }> {
+        return new Promise((resolve, reject) => {
+            const args = ['-jar', jarPath, 'test', runPath, '-o', outputFile];
+            for (const f of functionFilters) {
+                args.push('-f', f);
+            }
+            const cwd = fs.existsSync(runPath) && fs.statSync(runPath).isFile()
+                ? path.dirname(runPath)
+                : runPath;
+            cp.execFile(javaPath, args, { env, cwd }, (err, stdout, stderr) => {
+                const stdoutStr = stdout?.toString() ?? '';
+                const stderrStr = stderr?.toString() ?? '';
+                if (stdoutStr) this.outputChannel.appendLine(stdoutStr);
+                if (stderrStr) this.outputChannel.appendLine(stderrStr);
+                if (err && err.code !== 1) {
+                    reject(err);
+                } else {
+                    resolve({ stdout: stdoutStr, stderr: stderrStr });
+                }
+            });
+        });
+    }
+
+    private async findJava(): Promise {
+        const config = vscode.workspace.getConfiguration('isl.execution');
+        const javaHome = config.get('javaHome', '');
+        if (javaHome) {
+            const p = path.join(javaHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
+            if (fs.existsSync(p)) return p;
+        }
+        const envHome = process.env.JAVA_HOME;
+        if (envHome) {
+            const p = path.join(envHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
+            if (fs.existsSync(p)) return p;
+        }
+        return process.platform === 'win32' ? 'java.exe' : 'java';
+    }
+
+    /**
+     * Run all tests in the given file (used by "Run all Tests in file" command).
+     * Supports both tests-folder .isl files and *.tests.yaml files that have islTests.
+     */
+    async runTestsInFile(uri: vscode.Uri): Promise {
+        const folder = vscode.workspace.getWorkspaceFolder(uri);
+        const isIsl = uri.fsPath.endsWith('.isl');
+        const isYamlTests = uri.fsPath.endsWith('.tests.yaml');
+        if (!isIsl && !isYamlTests) return;
+        if (isIsl && (!folder || !isTestFile(uri, folder))) return;
+        if (isYamlTests) {
+            try {
+                const raw = await vscode.workspace.fs.readFile(uri);
+                const content = new TextDecoder().decode(raw);
+                if (!yamlHasIslTests(content)) return;
+            } catch {
+                return;
+            }
+        }
+
+        const fileItem = this.getOrCreateFile(uri);
+        await this.parseTestsInFile(fileItem);
+        if (fileItem.children.size === 0) return;
+
+        const tokenSource = new vscode.CancellationTokenSource();
+        const request: vscode.TestRunRequest = {
+            include: [fileItem],
+            exclude: undefined,
+            profile: undefined as unknown as vscode.TestRunProfile,
+            preserveFocus: false
+        };
+        await this.runProfileHandler(request, tokenSource.token);
+    }
+
+    dispose(): void {
+        this.documentChangeTimeouts.forEach(t => clearTimeout(t));
+        this.documentChangeTimeouts.clear();
+        this.watchers.forEach(w => w.dispose());
+        this.assertionDiagnostics.dispose();
+        this.controller.dispose();
+    }
+}
diff --git a/plugin/src/types.ts b/plugin/src/types.ts
new file mode 100644
index 0000000..7ae04f1
--- /dev/null
+++ b/plugin/src/types.ts
@@ -0,0 +1,729 @@
+import * as vscode from 'vscode';
+import * as yaml from 'js-yaml';
+import { IslExtensionsManager } from './extensions';
+
+export interface SchemaProperty {
+    type?: string;
+    description?: string;
+    /** Resolved nested object schema when this property is an object (from $ref or inline). */
+    schema?: SchemaInfo;
+    /** Enum values when this property is an enum (from $ref to enum schema or inline enum). */
+    enum?: string[];
+    /** Single example value (from example field). */
+    example?: string;
+    /** Multiple examples (from examples array). */
+    examples?: string[];
+    /** OpenAPI/JSON Schema x- extensions (x-example, x-summary, etc.). */
+    extensions?: Record;
+}
+
+export interface SchemaInfo {
+    required: string[];
+    properties: Record;
+}
+
+/** Result when cursor is inside a typed object (root or nested): schema to use for completions. */
+export interface SchemaAtPosition {
+    typeName: string;
+    propertyPath: string[];
+    schema: SchemaInfo;
+}
+
+export interface TypeDeclaration {
+    /** Type name (e.g. "account" or "idx:account") */
+    name: string;
+    source: 'url' | 'inline';
+    url?: string;
+    /** Inline schema string for source === 'inline', e.g. "{ FirstName: string, LastName: string }" */
+    inlineBody?: string;
+}
+
+const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour for schemas (configurable via isl.extensions.cacheTTL)
+
+interface CachedSchema {
+    schema: SchemaInfo;
+    timestamp: number;
+}
+
+interface CachedDocument {
+    doc: Record;
+    timestamp: number;
+}
+
+/**
+ * Parses type declarations from an ISL document and resolves schema from URLs or inline definitions.
+ * Loads $ref dependencies (internal and external) and builds the full object graph including child objects.
+ */
+export class IslTypeManager {
+    private schemaCache: Map = new Map();
+    private documentCache: Map = new Map();
+
+    constructor(
+        private extensionsManager: IslExtensionsManager,
+        private outputChannel?: vscode.OutputChannel
+    ) {}
+
+    private log(message: string, level: 'log' | 'warn' | 'error' = 'log'): void {
+        if (this.outputChannel) {
+            this.outputChannel.appendLine(message);
+        }
+        if (level === 'error') {
+            console.error(message);
+        } else if (level === 'warn') {
+            console.warn(message);
+        }
+    }
+
+    /**
+     * Extracts all type declarations from the document.
+     * - type Name from 'url'
+     * - type Name as { Prop: Type, ... }
+     * Name may be qualified (e.g. idx:account).
+     */
+    getTypeDeclarations(document: vscode.TextDocument): TypeDeclaration[] {
+        const declarations: TypeDeclaration[] = [];
+        const text = document.getText();
+        const lines = text.split('\n');
+
+        for (const line of lines) {
+            // type name from 'url' or type name from "url"
+            const fromMatch = line.match(/^\s*type\s+([a-zA-Z_][a-zA-Z0-9_.:]*)\s+from\s+['"]([^'"]+)['"]\s*;?\s*$/);
+            if (fromMatch) {
+                declarations.push({
+                    name: fromMatch[1].trim(),
+                    source: 'url',
+                    url: fromMatch[2].trim()
+                });
+                continue;
+            }
+
+            // type name as { ... };
+            const asMatch = line.match(/^\s*type\s+([a-zA-Z_][a-zA-Z0-9_.:]*)\s+as\s+\{([^}]*)\}\s*;?\s*$/);
+            if (asMatch) {
+                declarations.push({
+                    name: asMatch[1].trim(),
+                    source: 'inline',
+                    inlineBody: asMatch[2].trim()
+                });
+            }
+        }
+
+        return declarations;
+    }
+
+    /**
+     * Returns the type declaration for a type name if declared in the document (for hover: URL, source).
+     */
+    getDeclarationForType(document: vscode.TextDocument, typeName: string): TypeDeclaration | null {
+        const declarations = this.getTypeDeclarations(document);
+        return declarations.find(d => this.typeNameMatches(d.name, typeName)) ?? null;
+    }
+
+    /**
+     * Returns the schema for a type name (e.g. "account" or "idx:account") if declared in the document.
+     * For URL-based types, fetches and parses the schema (using GitHub/git for GitHub URLs via extensions manager).
+     */
+    async getSchemaForType(document: vscode.TextDocument, typeName: string): Promise {
+        const declarations = this.getTypeDeclarations(document);
+        const decl = declarations.find(d => this.typeNameMatches(d.name, typeName));
+        if (!decl) {
+            return null;
+        }
+
+        if (decl.source === 'inline' && decl.inlineBody !== undefined) {
+            return this.parseInlineSchema(decl.inlineBody);
+        }
+
+        if (decl.source === 'url' && decl.url) {
+            return await this.fetchAndParseSchemaFromUrl(decl.url);
+        }
+
+        return null;
+    }
+
+    private typeNameMatches(declared: string, requested: string): boolean {
+        return declared === requested || declared.replace(/:/g, '.') === requested.replace(/:/g, '.');
+    }
+
+    private parseInlineSchema(inlineBody: string): SchemaInfo {
+        const properties: Record = {};
+        const required: string[] = [];
+        // Split by comma, but respect nested braces
+        const parts = this.splitTopLevel(inlineBody, ',');
+        for (const part of parts) {
+            const colon = part.indexOf(':');
+            if (colon === -1) continue;
+            const propName = part.substring(0, colon).trim();
+            const typePart = part.substring(colon + 1).trim();
+            if (!propName) continue;
+            properties[propName] = { type: typePart || 'any' };
+            required.push(propName);
+        }
+        return { required, properties };
+    }
+
+    private splitTopLevel(str: string, sep: string): string[] {
+        const result: string[] = [];
+        let current = '';
+        let depth = 0;
+        for (let i = 0; i < str.length; i++) {
+            const ch = str[i];
+            if (ch === '{' || ch === '[' || ch === '(') depth++;
+            else if (ch === '}' || ch === ']' || ch === ')') depth--;
+            else if (depth === 0 && ch === sep) {
+                result.push(current.trim());
+                current = '';
+                continue;
+            }
+            current += ch;
+        }
+        if (current.trim()) result.push(current.trim());
+        return result;
+    }
+
+    private getCacheTTL(): number {
+        try {
+            const ttl = vscode.workspace.getConfiguration('isl').get('extensions.cacheTTL', 3600);
+            return ttl * 1000;
+        } catch {
+            return CACHE_TTL_MS;
+        }
+    }
+
+    private async fetchAndParseSchemaFromUrl(urlString: string): Promise {
+        const cacheKey = urlString;
+        const cached = this.schemaCache.get(cacheKey);
+        const ttl = this.getCacheTTL();
+        if (cached && (Date.now() - cached.timestamp) < ttl) {
+            this.log(`[ISL Types] Schema cache hit: ${urlString} (${Object.keys(cached.schema.properties).length} props)`);
+            return cached.schema;
+        }
+
+        try {
+            this.log(`[ISL Types] Loading schema from ${urlString}`);
+            const doc = await this.getDocument(urlString);
+            if (!doc) {
+                this.log(`[ISL Types] No document loaded from ${urlString}`, 'warn');
+                return null;
+            }
+            const preferredName = this.getSchemaNameFromUrl(urlString);
+            const rootSchemaObj = this.getRootSchemaObject(doc, preferredName);
+            if (!rootSchemaObj) {
+                this.log(`[ISL Types] No schema in components.schemas for ${urlString}${preferredName ? ` (tried ${preferredName})` : ''}`, 'warn');
+                return null;
+            }
+            const schema = await this.resolveSchemaGraph(rootSchemaObj, doc, urlString, new Set());
+            if (schema) {
+                this.schemaCache.set(cacheKey, { schema, timestamp: Date.now() });
+                this.log(`[ISL Types] Schema loaded from ${urlString} (${Object.keys(schema.properties).length} top-level properties)`);
+            }
+            return schema;
+        } catch (error) {
+            this.log(`[ISL Types] Failed to fetch schema from ${urlString}: ${error instanceof Error ? error.message : String(error)}`, 'error');
+            if (cached) return cached.schema;
+            return null;
+        }
+    }
+
+    /** Load and cache a document from URL (JSON or YAML). */
+    private async getDocument(urlString: string): Promise | null> {
+        const cached = this.documentCache.get(urlString);
+        const ttl = this.getCacheTTL();
+        if (cached && (Date.now() - cached.timestamp) < ttl) {
+            this.log(`[ISL Types] Document cache hit: ${urlString}`);
+            return cached.doc;
+        }
+        try {
+            this.log(`[ISL Types] Fetching document (cache miss): ${urlString}`);
+            const content = await this.extensionsManager.fetchContentFromUrl(urlString);
+            const trimmed = content.trim();
+            const doc = trimmed.startsWith('{')
+                ? (JSON.parse(content) as Record)
+                : (yaml.load(content) as Record);
+            if (doc && typeof doc === 'object') {
+                this.documentCache.set(urlString, { doc, timestamp: Date.now() });
+                return doc;
+            }
+        } catch (e) {
+            this.log(`[ISL Types] Failed to load document from ${urlString}: ${e instanceof Error ? e.message : String(e)}`, 'error');
+        }
+        return null;
+    }
+
+    /**
+     * Get the base URL for resolving relative $refs. For GitHub blob URLs, refs like
+     * "common/v1/Address.yaml" are relative to the repo root, not the file's directory.
+     */
+    private getRefBaseUrl(documentUrl: string): string {
+        try {
+            const parsed = new URL(documentUrl);
+            const pathname = parsed.pathname;
+            // GitHub blob: /owner/repo/blob/ref/path/to/file.yaml -> base is /owner/repo/blob/ref/
+            const blobMatch = pathname.match(/^(\/[^/]+\/[^/]+\/blob\/[^/]+\/)(?:.+)?$/);
+            if (blobMatch) {
+                return `${parsed.origin}${blobMatch[1]}`;
+            }
+            // raw.githubusercontent.com: /owner/repo/ref/path/to/file -> base is /owner/repo/ref/
+            const rawMatch = pathname.match(/^(\/[^/]+\/[^/]+\/[^/]+\/)(?:.+)?$/);
+            if (rawMatch && parsed.hostname.includes('raw')) {
+                return `${parsed.origin}${rawMatch[1]}`;
+            }
+            // Fallback: use directory of current file (strip last path segment)
+            const lastSlash = pathname.lastIndexOf('/');
+            if (lastSlash > 0) {
+                return `${parsed.origin}${pathname.substring(0, lastSlash + 1)}`;
+            }
+        } catch {
+            /* ignore */
+        }
+        return documentUrl;
+    }
+
+    /** Get value at JSON Pointer path (e.g. ["components","schemas","Order"]). */
+    private getByPath(obj: Record, path: string[]): unknown {
+        let current: unknown = obj;
+        for (const segment of path) {
+            if (current == null || typeof current !== 'object') return undefined;
+            current = (current as Record)[segment];
+        }
+        return current;
+    }
+
+    /** Resolve $ref to document and path. Handles #/path, relative URLs, and absolute URLs with optional fragment. */
+    private async resolveRef(
+        baseUrl: string,
+        ref: string
+    ): Promise<{ doc: Record; path: string[]; resolvedUrl: string } | null> {
+        const refTrim = ref.trim();
+        if (!refTrim) {
+            this.log(`[ISL Types] Empty $ref from ${baseUrl}`, 'warn');
+            return null;
+        }
+
+        this.log(`[ISL Types] Resolving $ref: ${refTrim} (base: ${baseUrl})`);
+
+        if (refTrim.startsWith('#')) {
+            const path = refTrim.slice(1).split('/').filter(Boolean);
+            const doc = await this.getDocument(baseUrl);
+            if (!doc) {
+                this.log(`[ISL Types] $ref ${refTrim}: base document not loaded`, 'warn');
+                return null;
+            }
+            this.log(`[ISL Types] $ref ${refTrim} resolved (internal, path: ${path.join('/') || '(root)'})`);
+            return { doc, path, resolvedUrl: baseUrl };
+        }
+
+        const hashIdx = refTrim.indexOf('#');
+        const urlPart = hashIdx >= 0 ? refTrim.substring(0, hashIdx).trim() : refTrim;
+        const fragment = hashIdx >= 0 ? refTrim.substring(hashIdx + 1).trim() : '';
+        const path = fragment ? fragment.split('/').filter(Boolean) : [];
+
+        // Base for relative refs:
+        // - ./ or ../ or bare filename (OrderExtension.yaml) -> file's directory (same folder)
+        // - path like common/v1/Address.yaml -> repo root
+        const fileDir = baseUrl.replace(/\/[^/]*$/, '/');
+        const refBase =
+            urlPart.startsWith('./') || urlPart.startsWith('../') || !urlPart.includes('/')
+                ? fileDir
+                : this.getRefBaseUrl(baseUrl);
+        let resolvedUrl: string;
+        try {
+            resolvedUrl = urlPart ? new URL(urlPart, refBase).href : baseUrl;
+        } catch {
+            this.log(`[ISL Types] Invalid $ref URL: ${refTrim}`, 'warn');
+            return null;
+        }
+
+        const doc = await this.getDocument(resolvedUrl);
+        if (!doc) {
+            this.log(`[ISL Types] $ref ${refTrim}: failed to load ${resolvedUrl}`, 'warn');
+            return null;
+        }
+        this.log(`[ISL Types] $ref ${refTrim} resolved to ${resolvedUrl} (path: ${path.join('/') || '(root)'})`);
+        return { doc, path, resolvedUrl };
+    }
+
+    /** Derive schema name from URL path (e.g. .../Order.yaml -> Order, .../BaseEntity.yaml -> BaseEntity). */
+    private getSchemaNameFromUrl(urlString: string): string | undefined {
+        try {
+            const parsed = new URL(urlString);
+            const pathname = parsed.pathname;
+            const lastSegment = pathname.split('/').filter(Boolean).pop() || '';
+            const base = lastSegment.replace(/\.(yaml|yml|json)$/i, '');
+            return base || undefined;
+        } catch {
+            return undefined;
+        }
+    }
+
+    /** Get schema object from resolved ref (handles empty path = whole doc → use OpenAPI components.schemas). */
+    private getSchemaObjectFromResolved(
+        resolved: { doc: Record; path: string[]; resolvedUrl: string }
+    ): Record | null {
+        if (resolved.path.length > 0) {
+            const target = this.getByPath(resolved.doc, resolved.path);
+            if (target && typeof target === 'object') return target as Record;
+            return null;
+        }
+        const preferredName = this.getSchemaNameFromUrl(resolved.resolvedUrl);
+        return this.getRootSchemaObject(resolved.doc, preferredName);
+    }
+
+    /**
+     * Get the schema object from an OpenAPI document. Schemas live in components.schemas.[name].
+     * When preferredSchemaName is given (e.g. "Order" from Order.yaml), use that schema if it exists.
+     */
+    private getRootSchemaObject(doc: Record, preferredSchemaName?: string): Record | null {
+        const components = doc.components as Record | undefined;
+        const schemas = components?.schemas as Record | undefined;
+        if (schemas && typeof schemas === 'object') {
+            if (preferredSchemaName && schemas[preferredSchemaName] && typeof schemas[preferredSchemaName] === 'object') {
+                this.log(`[ISL Types] Using OpenAPI schema components.schemas.${preferredSchemaName}`);
+                return schemas[preferredSchemaName] as Record;
+            }
+            const firstKey = Object.keys(schemas)[0];
+            const first = firstKey ? (schemas[firstKey] as Record) : null;
+            if (first && typeof first === 'object') {
+                this.log(`[ISL Types] Using OpenAPI schema components.schemas.${firstKey}`);
+                return first;
+            }
+        }
+        if (doc.properties !== undefined && typeof doc === 'object') return doc;
+        return doc as Record;
+    }
+
+    /**
+     * Resolve a schema object (possibly with $ref, allOf) and build SchemaInfo with nested $refs resolved.
+     * Visited set uses "resolvedUrl#/path" to avoid circular refs.
+     * If a $ref is missing or fails to load, we log and continue (keep other properties / refs).
+     */
+    private async resolveSchemaGraph(
+        schemaObj: Record,
+        doc: Record,
+        baseUrl: string,
+        visited: Set
+    ): Promise {
+        let obj = schemaObj;
+
+        if (typeof obj.$ref === 'string') {
+            const refKey = `${baseUrl}#${obj.$ref}`;
+            if (visited.has(refKey)) {
+                this.log(`[ISL Types] Circular $ref skipped: ${obj.$ref}`);
+                return { required: [], properties: {} };
+            }
+            visited.add(refKey);
+            try {
+                const resolved = await this.resolveRef(baseUrl, obj.$ref);
+                if (resolved) {
+                    const target = this.getSchemaObjectFromResolved(resolved);
+                    if (target) {
+                        obj = target;
+                        doc = resolved.doc;
+                        baseUrl = resolved.resolvedUrl;
+                    } else {
+                        this.log(`[ISL Types] $ref ${obj.$ref}: path not found in document`, 'warn');
+                    }
+                }
+            } catch (err) {
+                this.log(`[ISL Types] $ref ${obj.$ref} failed: ${err instanceof Error ? err.message : String(err)}`, 'warn');
+            }
+        }
+
+        if (Array.isArray(obj.allOf) && obj.allOf.length > 0) {
+            const merged: SchemaInfo = { required: [], properties: {} };
+            for (const item of obj.allOf) {
+                if (!item || typeof item !== 'object') continue;
+                const itemObj = item as Record;
+                const resolved = await this.resolveSchemaGraph(itemObj, doc, baseUrl, visited);
+                this.mergeSchemaInfo(merged, resolved);
+            }
+            this.log(`[ISL Types] allOf merged: ${Object.keys(merged.properties).length} properties from ${obj.allOf.length} item(s)`);
+            const inlineProps = obj.properties as Record | undefined;
+            if (inlineProps && typeof inlineProps === 'object') {
+                const inlineSchema = await this.extractPropertiesFromObject(obj, doc, baseUrl, visited);
+                this.mergeSchemaInfo(merged, inlineSchema);
+                this.log(`[ISL Types] allOf + inline properties: ${Object.keys(merged.properties).length} total`);
+            }
+            return merged;
+        }
+
+        return this.extractPropertiesFromObject(obj, doc, baseUrl, visited);
+    }
+
+    private formatExampleValue(val: unknown): string | undefined {
+        if (val === null) return 'null';
+        if (typeof val === 'string') return val;
+        if (typeof val === 'number' || typeof val === 'boolean') return String(val);
+        if (typeof val === 'object') return JSON.stringify(val, null, 2);
+        return undefined;
+    }
+
+    private mergeSchemaInfo(target: SchemaInfo, source: SchemaInfo): void {
+        for (const [key, val] of Object.entries(source.properties)) {
+            target.properties[key] = val;
+        }
+        const requiredSet = new Set(target.required);
+        for (const r of source.required) {
+            requiredSet.add(r);
+        }
+        target.required = Array.from(requiredSet);
+    }
+
+    private async extractPropertiesFromObject(
+        obj: Record,
+        doc: Record,
+        baseUrl: string,
+        visited: Set
+    ): Promise {
+        const properties = obj.properties as Record | undefined;
+        if (!properties || typeof properties !== 'object') {
+            return { required: [], properties: {} };
+        }
+
+        const required = Array.isArray(obj.required) ? (obj.required as string[]) : [];
+        const result: Record = {};
+
+        for (const [key, val] of Object.entries(properties)) {
+            if (!val || typeof val !== 'object') {
+                result[key] = { type: 'any' };
+                continue;
+            }
+            const v = val as Record;
+            const prop: SchemaProperty = {
+                type: typeof v.type === 'string' ? v.type : undefined,
+                description: typeof v.description === 'string' ? v.description : undefined
+            };
+
+            // Extract example / examples
+            if (v.example !== undefined) {
+                prop.example = this.formatExampleValue(v.example);
+            }
+            if (Array.isArray(v.examples)) {
+                prop.examples = v.examples
+                    .map((e: unknown) => (typeof e === 'object' && e !== null && 'value' in e) ? (e as { value: unknown }).value : e)
+                    .map((e: unknown) => this.formatExampleValue(e))
+                    .filter((s): s is string => s !== undefined);
+            }
+            // Extract x-* extensions
+            const extensions: Record = {};
+            for (const [k, val2] of Object.entries(v)) {
+                if (k.startsWith('x-')) extensions[k] = val2;
+            }
+            if (Object.keys(extensions).length > 0) prop.extensions = extensions;
+
+            if (Array.isArray(v.enum)) {
+                prop.enum = v.enum.filter((e): e is string => typeof e === 'string');
+                this.log(`[ISL Types] Property "${key}" enum: ${prop.enum.join(', ')}`);
+            } else if (typeof v.$ref === 'string') {
+                const refKey = `${baseUrl}#${v.$ref}`;
+                if (!visited.has(refKey)) {
+                    visited.add(refKey);
+                    try {
+                        const resolved = await this.resolveRef(baseUrl, v.$ref);
+                        if (resolved) {
+                            const target = this.getSchemaObjectFromResolved(resolved);
+                            if (target) {
+                                const t = target as Record;
+                                if (!prop.description && typeof t.description === 'string') prop.description = t.description;
+                                if (!prop.example && t.example !== undefined) prop.example = this.formatExampleValue(t.example);
+                                if (!prop.examples && Array.isArray(t.examples)) {
+                                    prop.examples = t.examples
+                                        .map((e: unknown) => (typeof e === 'object' && e !== null && 'value' in e) ? (e as { value: unknown }).value : e)
+                                        .map((e: unknown) => this.formatExampleValue(e))
+                                        .filter((s): s is string => s !== undefined);
+                                }
+                                if (!prop.extensions) {
+                                    const ext: Record = {};
+                                    for (const [k, val2] of Object.entries(t)) {
+                                        if (k.startsWith('x-')) ext[k] = val2;
+                                    }
+                                    if (Object.keys(ext).length > 0) prop.extensions = ext;
+                                }
+                                if (Array.isArray(target.enum)) {
+                                    prop.enum = target.enum.filter((e): e is string => typeof e === 'string');
+                                    this.log(`[ISL Types] Resolved property "${key}" $ref -> enum: ${prop.enum.join(', ')}`);
+                                } else if (target.properties && typeof target.properties === 'object') {
+                                    prop.schema = await this.resolveSchemaGraph(
+                                        target,
+                                        resolved.doc,
+                                        resolved.resolvedUrl,
+                                        visited
+                                    );
+                                    prop.type = prop.type ?? 'object';
+                                    this.log(`[ISL Types] Resolved property "${key}" $ref -> ${Object.keys(prop.schema.properties).length} nested properties`);
+                                }
+                            } else {
+                                this.log(`[ISL Types] Property "${key}" $ref ${v.$ref}: path not found, keeping as plain property`, 'warn');
+                            }
+                        } else {
+                            this.log(`[ISL Types] Property "${key}" $ref ${v.$ref}: could not resolve, keeping as plain property`, 'warn');
+                        }
+                    } catch (err) {
+                        this.log(`[ISL Types] Property "${key}" $ref ${v.$ref} failed: ${err instanceof Error ? err.message : String(err)}`, 'warn');
+                    }
+                }
+            } else if (v.properties && typeof v.properties === 'object') {
+                try {
+                    prop.schema = await this.resolveSchemaGraph(v, doc, baseUrl, visited);
+                    prop.type = prop.type ?? 'object';
+                    this.log(`[ISL Types] Resolved property "${key}" inline -> ${Object.keys(prop.schema.properties).length} nested properties`);
+                } catch (err) {
+                    this.log(`[ISL Types] Property "${key}" inline schema failed: ${err instanceof Error ? err.message : String(err)}`, 'warn');
+                }
+            }
+
+            result[key] = prop;
+        }
+
+        return { required, properties: result };
+    }
+
+    /**
+     * Finds the type name of the object literal at the given position, if any.
+     * Looks for patterns: $var name : TypeName = { or $name : TypeName = { and checks if position is inside the following {}.
+     */
+    getTypeNameForObjectLiteralAt(document: vscode.TextDocument, position: vscode.Position): string | null {
+        const text = document.getText();
+        const offset = document.offsetAt(position);
+
+        // Find the innermost object literal that contains this position (matching { ... })
+        const openBraces: number[] = [];
+        const containingPairs: { start: number; end: number }[] = [];
+
+        for (let i = 0; i < text.length; i++) {
+            const ch = text[i];
+            if (ch === '{') {
+                openBraces.push(i);
+            } else if (ch === '}') {
+                if (openBraces.length > 0) {
+                    const start = openBraces.pop()!;
+                    if (offset >= start && offset <= i) {
+                        containingPairs.push({ start, end: i });
+                    }
+                }
+            }
+        }
+
+        // Use the innermost pair (smallest span)
+        if (containingPairs.length === 0) return null;
+        let best = containingPairs[0];
+        for (const p of containingPairs) {
+            if (p.end - p.start < best.end - best.start) best = p;
+        }
+        const objectStart = best.start;
+        const objectEnd = best.end;
+
+        // Look backward from objectStart for " = " then " : TypeName " then "$var name" or "$name"
+        const before = text.substring(0, objectStart);
+        const eqIdx = before.lastIndexOf('=');
+        if (eqIdx === -1) return null;
+        const afterEq = before.substring(eqIdx + 1).trim();
+        if (afterEq !== '' && afterEq !== '{') return null; // only "= {" or "= {"
+
+        const beforeEq = before.substring(0, eqIdx);
+        // Match : TypeName (optional spaces); TypeName can contain :
+        const colonTypeMatch = beforeEq.match(/\:\s*([a-zA-Z_][a-zA-Z0-9_.:]*)\s*$/);
+        if (!colonTypeMatch) return null;
+
+        return colonTypeMatch[1].trim();
+    }
+
+    /**
+     * Gets the schema for the object at position, including nested objects (e.g. billingAddress inside order).
+     * Returns root type + property path + schema for that nested object.
+     */
+    async getSchemaForObjectAt(document: vscode.TextDocument, position: vscode.Position): Promise {
+        const text = document.getText();
+        const offset = document.offsetAt(position);
+
+        const containingPairs: { start: number; end: number }[] = [];
+        const openBraces: number[] = [];
+        for (let i = 0; i < text.length; i++) {
+            const ch = text[i];
+            if (ch === '{') openBraces.push(i);
+            else if (ch === '}') {
+                if (openBraces.length > 0) {
+                    const start = openBraces.pop()!;
+                    if (offset >= start && offset <= i) containingPairs.push({ start, end: i });
+                }
+            }
+        }
+        if (containingPairs.length === 0) return null;
+
+        const sortedBySpan = [...containingPairs].sort((a, b) => (a.end - a.start) - (b.end - b.start));
+        const innermost = sortedBySpan[0];
+
+        const isRootTypedObject = (objStart: number): { typeName: string } | null => {
+            const before = text.substring(0, objStart);
+            const eqIdx = before.lastIndexOf('=');
+            if (eqIdx === -1) return null;
+            const afterEq = before.substring(eqIdx + 1).trim();
+            if (afterEq !== '' && afterEq !== '{') return null;
+            const beforeEq = before.substring(0, eqIdx);
+            const m = beforeEq.match(/\:\s*([a-zA-Z_][a-zA-Z0-9_.:]*)\s*$/);
+            return m ? { typeName: m[1].trim() } : null;
+        };
+
+        const parentOf = (child: { start: number; end: number }): { start: number; end: number } | null => {
+            for (const p of sortedBySpan) {
+                if (p.start === child.start && p.end === child.end) continue;
+                if (p.start <= child.start && p.end >= child.end && (p.end - p.start) > (child.end - child.start))
+                    return p;
+            }
+            return null;
+        };
+
+        const propNameBefore = (parentStart: number, objStart: number): string | null => {
+            const segment = text.substring(parentStart + 1, objStart + 1);
+            const m = segment.match(/([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*\{\s*$/);
+            return m ? m[1] : null;
+        };
+
+        let current = innermost;
+        const path: string[] = [];
+        let typeName: string | null = null;
+
+        for (;;) {
+            const root = isRootTypedObject(current.start);
+            if (root) {
+                typeName = root.typeName;
+                break;
+            }
+            const parent = parentOf(current);
+            if (!parent) break;
+            const prop = propNameBefore(parent.start, current.start);
+            if (!prop) break;
+            path.unshift(prop);
+            current = parent;
+        }
+
+        if (!typeName) return null;
+
+        const schema = await this.getSchemaForType(document, typeName);
+        if (!schema) {
+            this.log(`[ISL Types] No schema for type ${typeName} at position`, 'warn');
+            return null;
+        }
+
+        let nestedSchema = schema;
+        for (const prop of path) {
+            const propInfo = nestedSchema.properties[prop];
+            if (!propInfo?.schema) {
+                this.log(`[ISL Types] No nested schema for path ${typeName}.${path.join('.')} (${prop} has no schema)`, 'warn');
+                return null;
+            }
+            nestedSchema = propInfo.schema;
+        }
+
+        if (path.length > 0) {
+            this.log(`[ISL Types] Nested object at ${typeName}.${path.join('.')}: ${Object.keys(nestedSchema.properties).length} properties`);
+        }
+
+        return { typeName, propertyPath: path, schema: nestedSchema };
+    }
+
+    dispose(): void {
+        this.schemaCache.clear();
+        this.documentCache.clear();
+    }
+}
diff --git a/plugin/src/validator.ts b/plugin/src/validator.ts
new file mode 100644
index 0000000..b869ac0
--- /dev/null
+++ b/plugin/src/validator.ts
@@ -0,0 +1,2522 @@
+import * as vscode from 'vscode';
+import * as path from 'path';
+import * as fs from 'fs';
+import { IslExtensionsManager, getExtensionFunction, getExtensionModifier } from './extensions';
+import { validateControlFlowBalance as validateControlFlowBalanceUtil } from './controlFlowMatcher';
+import { getBuiltInModifiersSet, getBuiltInFunctionsSet, getBuiltInNamespacesSet } from './language';
+import type { IslTypeManager } from './types';
+
+export class IslValidator {
+    private diagnosticCollection: vscode.DiagnosticCollection;
+    private validationTimeout: NodeJS.Timeout | undefined;
+    private readonly logValidation: (msg: string) => void;
+
+    private readonly builtInModifiers: Set;
+    private readonly builtInFunctions: Set;
+    private readonly builtInNamespaces: Set;
+
+    constructor(
+        private extensionsManager: IslExtensionsManager,
+        options?: { outputChannel?: vscode.OutputChannel; typeManager?: IslTypeManager }
+    ) {
+        this.builtInModifiers = getBuiltInModifiersSet();
+        this.builtInFunctions = getBuiltInFunctionsSet();
+        this.builtInNamespaces = getBuiltInNamespacesSet();
+        this.diagnosticCollection = vscode.languages.createDiagnosticCollection('isl');
+        this.logValidation = options?.outputChannel
+            ? (msg: string) => options.outputChannel!.appendLine(`[ISL Validation] ${msg}`)
+            : () => {};
+        this.typeManager = options?.typeManager;
+    }
+
+    private readonly typeManager?: IslTypeManager;
+
+    public dispose() {
+        this.diagnosticCollection.dispose();
+        if (this.validationTimeout) {
+            clearTimeout(this.validationTimeout);
+        }
+    }
+
+    public validateDebounced(document: vscode.TextDocument) {
+        if (this.validationTimeout) {
+            clearTimeout(this.validationTimeout);
+        }
+        this.validationTimeout = setTimeout(() => this.validate(document), 500);
+    }
+
+    public async validate(document: vscode.TextDocument): Promise {
+        const config = vscode.workspace.getConfiguration('isl.validation');
+        if (!config.get('enabled', true)) {
+            return;
+        }
+
+        const diagnostics: vscode.Diagnostic[] = [];
+        const text = document.getText();
+        const lines = text.split('\n');
+
+        // Load custom extensions
+        const extensions = await this.extensionsManager.getExtensionsForDocument(document);
+
+        // Basic syntax validation
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            const lineNumber = i;
+
+            // Check for common syntax errors
+            this.checkBraceMatching(line, lineNumber, diagnostics, document);
+            this.checkVariableDeclaration(line, lineNumber, diagnostics, document);
+            this.checkStringInterpolation(line, lineNumber, diagnostics, document);
+            this.checkFunctionDeclaration(line, lineNumber, diagnostics, document);
+            this.checkControlFlow(line, lineNumber, diagnostics, document);
+        }
+
+        // Check for overall structure issues
+        this.checkBalancedBraces(text, diagnostics, document);
+        this.checkControlFlowBalance(text, diagnostics, document);
+        this.checkBalancedBackticks(text, diagnostics, document);
+
+        // Semantic validation
+        const userDefinedFunctions = this.extractUserDefinedFunctions(document);
+        const userDefinedModifiers = this.extractUserDefinedModifiers(document);
+        const declaredVariables = this.extractDeclaredVariables(document);
+
+        // Extract imports and their exported functions/modifiers
+        const importedFunctions = await this.extractImportedFunctions(document);
+        const importedModifiers = await this.extractImportedModifiers(document);
+
+        // Extract pagination variables for property validation
+        const paginationVariables = this.extractPaginationVariables(document);
+        
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            this.checkFunctionCalls(line, i, diagnostics, document, userDefinedFunctions, extensions, importedFunctions);
+            this.checkModifierUsage(line, i, diagnostics, document, userDefinedModifiers, extensions, importedModifiers);
+            this.checkVariableUsage(line, i, diagnostics, document, declaredVariables);
+            this.checkPaginationPropertyAccess(line, i, diagnostics, document, paginationVariables);
+            this.checkLongObjectDeclaration(line, i, diagnostics, document);
+            this.checkUnnecessaryStringInterpolation(line, i, diagnostics, document);
+            this.checkUnnecessarySingleInterpolationTemplate(line, i, diagnostics, document);
+            this.checkDefaultModifier(line, i, diagnostics, document);
+            this.checkColonAssignment(line, i, diagnostics, document);
+            this.checkMathInTemplateString(line, i, diagnostics, document);
+            this.checkConsecutiveFilters(line, i, diagnostics, document);
+            this.checkNamingConvention(line, i, diagnostics, document);
+            this.checkMathOutsideBraces(line, i, diagnostics, document);
+            this.checkInconsistentSpacing(line, i, diagnostics, document);
+            this.checkPlusStringConcatenation(line, i, diagnostics, document);
+        }
+        
+        // Multi-line checks
+        this.checkDuplicateFunctionNames(document, diagnostics);
+        this.checkForeachVariableScoping(document, diagnostics);
+        this.checkTypeConversion(document, diagnostics);
+        
+        // Check for foreach loops that can be converted to map (multi-line check)
+        this.checkForeachToMap(document, diagnostics);
+
+        // Check for array declared as [] and only used once with | push(...) -> suggest [ item ] instead
+        this.checkSinglePushArray(document, diagnostics);
+        
+        // Check for functions that should be modifiers (multi-line check)
+        this.checkFunctionToModifier(document, diagnostics);
+
+        // Check for extra properties in typed object literals ($var : Type = { ... })
+        if (this.typeManager) {
+            await this.checkTypedObjectExtraProperties(document, diagnostics);
+        }
+
+        this.diagnosticCollection.set(document.uri, diagnostics);
+    }
+
+    private checkImportStatement(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[]) {
+        const importMatch = line.match(/import\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+from\s+['"]([^'"]+)['"]/);
+        if (!importMatch) return;
+
+        const filePath = importMatch[2];
+        if (filePath.toLowerCase().endsWith('.isl')) {
+            const pathStartInLine = importMatch.index! + importMatch[0].indexOf(filePath);
+            const range = new vscode.Range(lineNumber, pathStartInLine, lineNumber, pathStartInLine + filePath.length);
+            diagnostics.push(new vscode.Diagnostic(
+                range,
+                'Import path should not end with .isl',
+                vscode.DiagnosticSeverity.Error
+            ));
+        }
+    }
+
+    private checkBraceMatching(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Skip this check - it causes false positives on multi-line constructs
+        // The overall balance check in checkBalancedBraces handles this better
+        return;
+    }
+
+    private checkVariableDeclaration(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Check for invalid variable names
+        const varPattern = /\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
+        let match;
+        
+        while ((match = varPattern.exec(line)) !== null) {
+            const varName = match[1];
+            
+            // Check if variable name is a reserved keyword
+            const reservedKeywords = ['if', 'else', 'endif', 'switch', 'endswitch', 'foreach', 'endfor', 'while', 'endwhile', 'fun', 'modifier', 'return', 'import', 'type', 'as', 'from'];
+            if (reservedKeywords.includes(varName)) {
+                const startPos = match.index;
+                const range = new vscode.Range(lineNumber, startPos, lineNumber, startPos + match[0].length);
+                diagnostics.push(new vscode.Diagnostic(
+                    range,
+                    `'${varName}' is a reserved keyword and cannot be used as a variable name`,
+                    vscode.DiagnosticSeverity.Error
+                ));
+            }
+        }
+    }
+
+    private checkStringInterpolation(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Note: Multi-line backtick strings are valid in ISL
+        // We check for balanced backticks at the document level, not line level
+        // This method now only checks for empty interpolation expressions
+        
+        // Check for invalid interpolation syntax (empty ${})
+        // But be careful - the backtick might not close on this line
+        const interpolationPattern = /\$\{([^}]*)\}/g;
+        let match;
+        while ((match = interpolationPattern.exec(line)) !== null) {
+            const expr = match[1];
+            if (expr.trim() === '') {
+                const startPos = match.index;
+                const range = new vscode.Range(lineNumber, startPos, lineNumber, startPos + match[0].length);
+                diagnostics.push(new vscode.Diagnostic(
+                    range,
+                    'Empty interpolation expression',
+                    vscode.DiagnosticSeverity.Warning
+                ));
+            }
+        }
+    }
+
+    private checkFunctionDeclaration(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Check function declarations
+        const funPattern = /^\s*(fun|modifier)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/;
+        const match = line.match(funPattern);
+        
+        // Check for return statements outside functions
+        if (line.trim().startsWith('return ') && !this.isInsideFunction(document, lineNumber)) {
+            const range = new vscode.Range(lineNumber, line.indexOf('return'), lineNumber, line.indexOf('return') + 6);
+            diagnostics.push(new vscode.Diagnostic(
+                range,
+                'Return statement outside of function',
+                vscode.DiagnosticSeverity.Error
+            ));
+        }
+    }
+
+    private checkControlFlow(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Check for proper control flow syntax
+        const trimmed = line.trim();
+        
+        // Check if/else/endif
+        if (trimmed.startsWith('if ') && !trimmed.includes('(')) {
+            const range = new vscode.Range(lineNumber, 0, lineNumber, line.length);
+            diagnostics.push(new vscode.Diagnostic(
+                range,
+                'If statement requires parentheses around condition',
+                vscode.DiagnosticSeverity.Error
+            ));
+        }
+
+        // Check foreach
+        if (trimmed.startsWith('foreach ') && !trimmed.includes(' in ')) {
+            const range = new vscode.Range(lineNumber, 0, lineNumber, line.length);
+            diagnostics.push(new vscode.Diagnostic(
+                range,
+                'Foreach loop requires "in" keyword',
+                vscode.DiagnosticSeverity.Error
+            ));
+        }
+    }
+
+    private checkBalancedBraces(text: string, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        let braceCount = 0;
+        let bracketCount = 0;
+        let parenCount = 0;
+
+        const lines = text.split('\n');
+        
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            
+            // Skip comments
+            const commentIndex = Math.min(
+                line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+                line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+            );
+            const checkLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+
+            braceCount += (checkLine.match(/\{/g) || []).length - (checkLine.match(/\}/g) || []).length;
+            bracketCount += (checkLine.match(/\[/g) || []).length - (checkLine.match(/\]/g) || []).length;
+            parenCount += (checkLine.match(/\(/g) || []).length - (checkLine.match(/\)/g) || []).length;
+        }
+
+        if (braceCount !== 0) {
+            const range = new vscode.Range(0, 0, 0, 0);
+            diagnostics.push(new vscode.Diagnostic(
+                range,
+                `Unbalanced braces: ${braceCount > 0 ? 'missing closing' : 'extra closing'} brace(s)`,
+                vscode.DiagnosticSeverity.Error
+            ));
+        }
+
+        if (bracketCount !== 0) {
+            const range = new vscode.Range(0, 0, 0, 0);
+            diagnostics.push(new vscode.Diagnostic(
+                range,
+                `Unbalanced brackets: ${bracketCount > 0 ? 'missing closing' : 'extra closing'} bracket(s)`,
+                vscode.DiagnosticSeverity.Error
+            ));
+        }
+
+        if (parenCount !== 0) {
+            const range = new vscode.Range(0, 0, 0, 0);
+            diagnostics.push(new vscode.Diagnostic(
+                range,
+                `Unbalanced parentheses: ${parenCount > 0 ? 'missing closing' : 'extra closing'} parenthesis/parentheses`,
+                vscode.DiagnosticSeverity.Error
+            ));
+        }
+    }
+
+    private checkBalancedBackticks(text: string, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Check for balanced backticks across the entire document
+        // Backtick strings can span multiple lines in ISL
+        
+        const lines = text.split('\n');
+        let backtickCount = 0;
+        let lastBacktickLine = -1;
+        let lastBacktickCol = -1;
+        
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            
+            // Process each character to properly handle escaped backticks
+            let inString = false;
+            let stringChar = '';
+            
+            for (let j = 0; j < line.length; j++) {
+                const char = line[j];
+                const prevChar = j > 0 ? line[j - 1] : '';
+                
+                // Skip escaped characters
+                if (prevChar === '\\') {
+                    continue;
+                }
+                
+                // Track single and double quoted strings (backticks don't nest inside them)
+                if ((char === '"' || char === "'") && !inString) {
+                    inString = true;
+                    stringChar = char;
+                    continue;
+                } else if (char === stringChar && inString) {
+                    inString = false;
+                    stringChar = '';
+                    continue;
+                }
+                
+                // Count backticks only outside of regular strings
+                if (char === '`' && !inString) {
+                    backtickCount++;
+                    lastBacktickLine = i;
+                    lastBacktickCol = j;
+                }
+            }
+        }
+        
+        // If backtick count is odd, we have an unclosed template literal
+        if (backtickCount % 2 !== 0 && lastBacktickLine >= 0) {
+            const range = new vscode.Range(lastBacktickLine, lastBacktickCol, lastBacktickLine, lastBacktickCol + 1);
+            diagnostics.push(new vscode.Diagnostic(
+                range,
+                'Unclosed template literal (backtick string)',
+                vscode.DiagnosticSeverity.Error
+            ));
+        }
+    }
+
+    private checkControlFlowBalance(text: string, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Use the shared utility for control flow validation
+        // This properly handles multi-line statements and nested structures
+        const errors = validateControlFlowBalanceUtil(document);
+        for (const { diagnostic } of errors) {
+            diagnostics.push(diagnostic);
+        }
+    }
+
+    /**
+     * Checks for duplicate function/modifier names in the file.
+     * ISL is case-insensitive for function names, so 'foo' and 'Foo' are duplicates.
+     * Adds a red squiggle (Error diagnostic) on the second and subsequent occurrences.
+     */
+    private checkDuplicateFunctionNames(document: vscode.TextDocument, diagnostics: vscode.Diagnostic[]) {
+        const funPattern = /^\s*(fun|modifier)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/;
+        const seenFun = new Map();   // lowerName -> { line, originalName }
+        const seenModifier = new Map();
+
+        const lines = document.getText().split('\n');
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            const match = line.match(funPattern);
+            if (!match) continue;
+
+            const kind = match[1] as 'fun' | 'modifier';
+            const name = match[2];
+            const nameLower = name.toLowerCase();
+            const nameStartCol = match.index! + match[0].indexOf(name);
+            const range = new vscode.Range(i, nameStartCol, i, nameStartCol + name.length);
+
+            const seen = kind === 'fun' ? seenFun : seenModifier;
+            if (seen.has(nameLower)) {
+                const first = seen.get(nameLower)!;
+                const diagnostic = new vscode.Diagnostic(
+                    range,
+                    `Duplicate ${kind} '${name}' - already defined at line ${first.line + 1} (as '${first.originalName}')`,
+                    vscode.DiagnosticSeverity.Error
+                );
+                diagnostic.code = 'duplicate-function';
+                (diagnostic as any).functionName = name;
+                (diagnostic as any).kind = kind;
+                (diagnostic as any).firstDefinitionLine = first.line;
+                diagnostics.push(diagnostic);
+            } else {
+                seen.set(nameLower, { line: i, originalName: name });
+            }
+        }
+    }
+
+    private isInsideFunction(document: vscode.TextDocument, lineNumber: number): boolean {
+        // Track function scopes using brace counting
+        const functionStarts: number[] = [];
+        
+        for (let i = 0; i <= lineNumber; i++) {
+            const fullLine = document.lineAt(i).text;
+            
+            // Skip comments
+            const commentIndex = Math.min(
+                fullLine.indexOf('//') !== -1 ? fullLine.indexOf('//') : Infinity,
+                fullLine.indexOf('#') !== -1 ? fullLine.indexOf('#') : Infinity
+            );
+            const line = commentIndex !== Infinity ? fullLine.substring(0, commentIndex) : fullLine;
+            
+            // Check if this line has a function/modifier definition
+            if (line.match(/\b(fun|modifier)\s+[a-zA-Z_][a-zA-Z0-9_]*\s*\(/)) {
+                functionStarts.push(i);
+            }
+        }
+        
+        // For each function start, check if the current line is within its braces
+        for (const funcStart of functionStarts) {
+            let braceDepth = 0;
+            let foundOpeningBrace = false;
+            
+            for (let i = funcStart; i <= lineNumber; i++) {
+                const fullLine = document.lineAt(i).text;
+                const commentIndex = Math.min(
+                    fullLine.indexOf('//') !== -1 ? fullLine.indexOf('//') : Infinity,
+                    fullLine.indexOf('#') !== -1 ? fullLine.indexOf('#') : Infinity
+                );
+                const line = commentIndex !== Infinity ? fullLine.substring(0, commentIndex) : fullLine;
+                
+                // Count braces
+                const openBraces = (line.match(/\{/g) || []).length;
+                const closeBraces = (line.match(/\}/g) || []).length;
+                
+                braceDepth += openBraces;
+                if (openBraces > 0) {
+                    foundOpeningBrace = true;
+                }
+                braceDepth -= closeBraces;
+                
+                // If we're at the target line and inside this function's braces
+                if (i === lineNumber && foundOpeningBrace && braceDepth > 0) {
+                    return true;
+                }
+                
+                // If we've closed this function completely before reaching the target line
+                if (foundOpeningBrace && braceDepth === 0 && i < lineNumber) {
+                    break; // This function is closed, try the next one
+                }
+            }
+        }
+        
+        return false;
+    }
+
+    // Semantic validation methods
+
+    /**
+     * Extracts import statements from the document
+     * Returns a map of module name -> file path
+     */
+    private extractImports(document: vscode.TextDocument): Map {
+        const imports = new Map();
+        const text = document.getText();
+        const lines = text.split('\n');
+
+        for (const line of lines) {
+            // Match: import ModuleName from 'file.isl' or import ModuleName from "file.isl"
+            const importMatch = line.match(/import\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+from\s+['"]([^'"]+)['"]/);
+            if (importMatch) {
+                const moduleName = importMatch[1];
+                const filePath = importMatch[2];
+                imports.set(moduleName, filePath);
+            }
+        }
+
+        return imports;
+    }
+
+    /**
+     * Resolves the absolute path of an imported file relative to the current document
+     */
+    private resolveImportPath(document: vscode.TextDocument, importPath: string): vscode.Uri | null {
+        const currentDir = path.dirname(document.uri.fsPath);
+        let resolvedPath: string;
+
+        if (path.isAbsolute(importPath)) {
+            resolvedPath = importPath;
+        } else {
+            resolvedPath = path.resolve(currentDir, importPath);
+        }
+
+        // Try with .isl extension if not present
+        if (!resolvedPath.endsWith('.isl')) {
+            const withExtension = resolvedPath + '.isl';
+            if (fs.existsSync(withExtension)) {
+                resolvedPath = withExtension;
+            }
+        }
+
+        if (fs.existsSync(resolvedPath)) {
+            return vscode.Uri.file(resolvedPath);
+        }
+
+        return null;
+    }
+
+    /**
+     * Extracts functions from imported files
+     * Returns a map of module name -> set of function names
+     */
+    private async extractImportedFunctions(document: vscode.TextDocument): Promise>> {
+        const importedFunctions = new Map>();
+        const imports = this.extractImports(document);
+
+        for (const [moduleName, filePath] of imports) {
+            const importedUri = this.resolveImportPath(document, filePath);
+            if (!importedUri) {
+                continue;
+            }
+
+            try {
+                const importedText = fs.readFileSync(importedUri.fsPath, 'utf-8');
+                const importedLines = importedText.split('\n');
+                const functions = new Set();
+
+                for (const line of importedLines) {
+                    // Match function definitions: fun functionName(
+                    const funMatch = line.match(/^\s*fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
+                    if (funMatch) {
+                        functions.add(funMatch[1]);
+                    }
+                }
+
+                importedFunctions.set(moduleName, functions);
+            } catch (error) {
+                // Silently skip if file cannot be read
+                console.warn(`Could not read imported file ${filePath}: ${error}`);
+            }
+        }
+
+        return importedFunctions;
+    }
+
+    /**
+     * Extracts modifiers from imported files
+     * Returns a map of module name -> set of modifier names
+     */
+    private async extractImportedModifiers(document: vscode.TextDocument): Promise>> {
+        const importedModifiers = new Map>();
+        const imports = this.extractImports(document);
+
+        for (const [moduleName, filePath] of imports) {
+            const importedUri = this.resolveImportPath(document, filePath);
+            if (!importedUri) {
+                continue;
+            }
+
+            try {
+                const importedText = fs.readFileSync(importedUri.fsPath, 'utf-8');
+                const importedLines = importedText.split('\n');
+                const modifiers = new Set();
+
+                for (const line of importedLines) {
+                    // Match modifier definitions: modifier modifierName(
+                    const modMatch = line.match(/^\s*modifier\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
+                    if (modMatch) {
+                        modifiers.add(modMatch[1]);
+                    }
+                }
+
+                importedModifiers.set(moduleName, modifiers);
+            } catch (error) {
+                // Silently skip if file cannot be read
+                console.warn(`Could not read imported file ${filePath}: ${error}`);
+            }
+        }
+
+        return importedModifiers;
+    }
+
+    private extractUserDefinedFunctions(document: vscode.TextDocument): Set {
+        const functions = new Set();
+        const text = document.getText();
+        const lines = text.split('\n');
+
+        for (const line of lines) {
+            // Match function definitions: fun functionName(
+            const funMatch = line.match(/^\s*fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
+            if (funMatch) {
+                functions.add(funMatch[1]);
+            }
+        }
+
+        return functions;
+    }
+
+    private extractUserDefinedModifiers(document: vscode.TextDocument): Set {
+        const modifiers = new Set();
+        const text = document.getText();
+        const lines = text.split('\n');
+
+        for (const line of lines) {
+            // Match modifier definitions: modifier modifierName(
+            const modMatch = line.match(/^\s*modifier\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
+            if (modMatch) {
+                modifiers.add(modMatch[1]);
+            }
+        }
+
+        return modifiers;
+    }
+
+    private extractPaginationVariables(document: vscode.TextDocument): Map {
+        const paginationVars = new Map(); // variable name -> pagination type
+        const text = document.getText();
+        const lines = text.split('\n');
+
+        for (const line of lines) {
+            // Match @.Pagination.[Type]( $varName, ... )
+            const cursorMatch = line.match(/@\.Pagination\.Cursor\s*\(\s*\$([a-zA-Z_][a-zA-Z0-9_]*)/);
+            if (cursorMatch) {
+                paginationVars.set(cursorMatch[1], 'Cursor');
+            }
+            
+            const offsetMatch = line.match(/@\.Pagination\.Offset\s*\(\s*\$([a-zA-Z_][a-zA-Z0-9_]*)/);
+            if (offsetMatch) {
+                paginationVars.set(offsetMatch[1], 'Offset');
+            }
+            
+            const pageMatch = line.match(/@\.Pagination\.Page\s*\(\s*\$([a-zA-Z_][a-zA-Z0-9_]*)/);
+            if (pageMatch) {
+                paginationVars.set(pageMatch[1], 'Page');
+            }
+            
+            const keysetMatch = line.match(/@\.Pagination\.Keyset\s*\(\s*\$([a-zA-Z_][a-zA-Z0-9_]*)/);
+            if (keysetMatch) {
+                paginationVars.set(keysetMatch[1], 'Keyset');
+            }
+            
+            const dateMatch = line.match(/@\.Pagination\.Date\s*\(\s*\$([a-zA-Z_][a-zA-Z0-9_]*)/);
+            if (dateMatch) {
+                paginationVars.set(dateMatch[1], 'Date');
+            }
+        }
+
+        return paginationVars;
+    }
+    
+    private extractDeclaredVariables(document: vscode.TextDocument): Map {
+        const variables = new Map(); // variable name -> first declaration line
+        const text = document.getText();
+        const lines = text.split('\n');
+
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            
+            // Match variable declarations: $varName = ... or $varName: ...
+            const varDeclPattern = /\$([a-zA-Z_][a-zA-Z0-9_]*)\s*[=:]/g;
+            let match;
+            
+            while ((match = varDeclPattern.exec(line)) !== null) {
+                const varName = match[1];
+                if (!variables.has(varName)) {
+                    variables.set(varName, i);
+                }
+            }
+
+            // Also track function parameters as declared variables
+            const funParamMatch = line.match(/^\s*(fun|modifier)\s+[a-zA-Z_][a-zA-Z0-9_]*\s*\(([^)]*)\)/);
+            if (funParamMatch) {
+                const params = funParamMatch[2];
+                const paramNames = params.split(',').map(p => p.trim().replace(/^\$/, ''));
+                for (const param of paramNames) {
+                    if (param && !variables.has(param)) {
+                        variables.set(param, i);
+                    }
+                }
+            }
+
+            // Track foreach loop variables: foreach $item in $items
+            // Creates both $item and $itemIndex (zero-based index)
+            const foreachMatch = line.match(/foreach\s+\$([a-zA-Z_][a-zA-Z0-9_]*)\s+in/);
+            if (foreachMatch) {
+                const varName = foreachMatch[1];
+                if (!variables.has(varName)) {
+                    variables.set(varName, i);
+                }
+                
+                // Also add the index variable: $iteratorIndex
+                const indexVarName = varName + 'Index';
+                if (!variables.has(indexVarName)) {
+                    variables.set(indexVarName, i);
+                }
+            }
+            
+            // Track pagination function parameters: @.Pagination.[Type]( $varName, ... )
+            // The first parameter to pagination functions is a variable declaration
+            const paginationMatch = line.match(/@\.Pagination\.[A-Za-z_][A-Za-z0-9_]*\s*\(\s*\$([a-zA-Z_][a-zA-Z0-9_]*)/);
+            if (paginationMatch) {
+                const varName = paginationMatch[1];
+                if (!variables.has(varName)) {
+                    variables.set(varName, i);
+                }
+            }
+        }
+
+        return variables;
+    }
+
+    private checkFunctionCalls(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument, userDefinedFunctions: Set, extensions: import('./extensions').IslExtensions, importedFunctions: Map>) {
+        // Skip comments - only check code part
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+        let match;
+        const processedRanges: Array<{ start: number; end: number }> = [];
+
+        // First: mark all valid @.Name() global extension calls so we never treat them as "module not imported"
+        const globalFunctionPattern = /@\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
+        while ((match = globalFunctionPattern.exec(codeOnlyLine)) !== null) {
+            const name = match[1];
+            if (name && getExtensionFunction(extensions, name)) {
+                processedRanges.push({ start: match.index, end: match.index + match[0].length });
+                this.logValidation(`@.${name}() resolved as global extension (single name)`);
+            }
+        }
+
+        // Now check for @.ModuleName.functionName() pattern (imported functions and built-ins)
+        const importedFunctionPattern = /@\.([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
+        while ((match = importedFunctionPattern.exec(codeOnlyLine)) !== null) {
+            const moduleName = match[1];
+            const funcName = match[2];
+            const matchStart = match.index;
+            const matchEnd = matchStart + match[0].length;
+            const compoundName = `${moduleName}.${funcName}`;
+
+            // Skip if this span is already a valid global extension call (@.name())
+            if (processedRanges.some(r => matchStart >= r.start && matchEnd <= r.end)) {
+                continue;
+            }
+
+            // Global extension with compound name (e.g. @.Call.Api() where "Call.Api" is in extensions)
+            const compoundExtFunc = getExtensionFunction(extensions, compoundName);
+            if (compoundExtFunc) {
+                processedRanges.push({ start: matchStart, end: matchEnd });
+                this.logValidation(`@.${compoundName}() resolved as global extension (compound name)`);
+                continue;
+            }
+
+            // First: if first part is a global extension function, this is wrong form (@.Call.Api → use @.Call())
+            const extFunc = getExtensionFunction(extensions, moduleName);
+            if (extFunc && match[0].startsWith('@.' + moduleName + '.')) {
+                const startPos = match.index + match[0].indexOf(moduleName);
+                const range = new vscode.Range(lineNumber, startPos, lineNumber, startPos + moduleName.length);
+                diagnostics.push(new vscode.Diagnostic(
+                    range,
+                    `Global extension functions are called as @.${moduleName}(), not @.${moduleName}.${funcName}()`,
+                    vscode.DiagnosticSeverity.Warning
+                ));
+                continue;
+            }
+
+            // Track this range so we don't process it again
+            processedRanges.push({ start: matchStart, end: matchEnd });
+
+            // Skip if it's a built-in namespace (Date, Math, This, etc.)
+            if (this.builtInNamespaces.has(moduleName)) {
+                this.logValidation(`@.${compoundName}() checking built-in namespace '${moduleName}'`);
+                // Check if it's a valid built-in function (case-insensitive check)
+                const builtInKey = `${moduleName}.${funcName}`;
+                const builtInKeyLower = builtInKey.toLowerCase();
+                let isBuiltIn = false;
+                
+                for (const builtIn of this.builtInFunctions) {
+                    if (builtIn.toLowerCase() === builtInKeyLower) {
+                        isBuiltIn = true;
+                        break;
+                    }
+                }
+                
+                // For @.This.functionName(), only same-file functions (not global extensions)
+                if (moduleName === 'This') {
+                    if (userDefinedFunctions.has(funcName)) {
+                        this.logValidation(`@.${compoundName}() resolved as same-file function`);
+                        continue;
+                    } else {
+                        const startPos = match.index + match[0].indexOf(funcName);
+                        const range = new vscode.Range(lineNumber, startPos, lineNumber, startPos + funcName.length);
+                        diagnostics.push(new vscode.Diagnostic(
+                            range,
+                            `Function '${funcName}' is not defined`,
+                            vscode.DiagnosticSeverity.Warning
+                        ));
+                    }
+                } else if (isBuiltIn) {
+                    this.logValidation(`@.${compoundName}() resolved as built-in function`);
+                    continue;
+                } else {
+                    // Built-in namespace but function doesn't exist
+                    const startPos = match.index + match[0].indexOf(funcName);
+                    const range = new vscode.Range(lineNumber, startPos, lineNumber, startPos + funcName.length);
+                    diagnostics.push(new vscode.Diagnostic(
+                        range,
+                        `Function '${funcName}' is not a valid ${moduleName} function`,
+                        vscode.DiagnosticSeverity.Warning
+                    ));
+                }
+                continue;
+            }
+
+            // Check if it's an imported module
+            if (importedFunctions.has(moduleName)) {
+                if (importedFunctions.get(moduleName)!.has(funcName)) {
+                    this.logValidation(`@.${compoundName}() resolved as imported function`);
+                    continue;
+                } else {
+                    const startPos = match.index + match[0].indexOf(funcName);
+                    const range = new vscode.Range(lineNumber, startPos, lineNumber, startPos + funcName.length);
+                    diagnostics.push(new vscode.Diagnostic(
+                        range,
+                        `Function '${funcName}' is not exported from module '${moduleName}'`,
+                        vscode.DiagnosticSeverity.Warning
+                    ));
+                }
+            } else {
+                // Module not found - check if it's imported (never report "not imported" for global extension names)
+                const imports = this.extractImports(document);
+                if (getExtensionFunction(extensions, moduleName)) {
+                    // Should have been caught above; treat as wrong form
+                    this.logValidation(`@.${compoundName}() wrong form: global extension '${moduleName}' should be called as @.${moduleName}(), not @.${compoundName}()`);
+                    const startPos = match.index + match[0].indexOf(moduleName);
+                    const range = new vscode.Range(lineNumber, startPos, lineNumber, startPos + moduleName.length);
+                    diagnostics.push(new vscode.Diagnostic(
+                        range,
+                        `Global extension functions are called as @.${moduleName}(), not @.${moduleName}.${funcName}()`,
+                        vscode.DiagnosticSeverity.Warning
+                    ));
+                } else if (getExtensionFunction(extensions, compoundName)) {
+                    // Compound global extension (e.g. Call.Api) – valid, don't report "not imported"
+                    this.logValidation(`@.${compoundName}() resolved as global extension (compound); skipping "module not imported"`);
+                } else if (!imports.has(moduleName)) {
+                    this.logValidation(`@.${compoundName}() module '${moduleName}' is not imported (not in extensions as '${moduleName}' or '${compoundName}')`);
+                    const startPos = match.index + match[0].indexOf(moduleName);
+                    const range = new vscode.Range(lineNumber, startPos, lineNumber, startPos + moduleName.length);
+                    diagnostics.push(new vscode.Diagnostic(
+                        range,
+                        `Module '${moduleName}' is not imported`,
+                        vscode.DiagnosticSeverity.Warning
+                    ));
+                }
+            }
+        }
+
+        // Check for @.SingleName() – global extension functions (called directly by name, like built-ins)
+        // Pattern matches only one identifier after the dot (e.g. @.sendEmail(), not @.Date.Now())
+        globalFunctionPattern.lastIndex = 0;
+        while ((match = globalFunctionPattern.exec(codeOnlyLine)) !== null) {
+            const name = match[1];
+            if (name && getExtensionFunction(extensions, name)) {
+                continue; // valid global extension call
+            }
+            if (this.builtInNamespaces.has(name)) {
+                const startPos = match.index + match[0].indexOf(name);
+                const range = new vscode.Range(lineNumber, startPos, lineNumber, startPos + name.length);
+                diagnostics.push(new vscode.Diagnostic(
+                    range,
+                    `Use @.${name}.method() form, not @.${name}()`,
+                    vscode.DiagnosticSeverity.Warning
+                ));
+            }
+        }
+
+        // Check for @.This.functionName() calls – only same-file functions
+        const thisFunctionPattern = /@\.This\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
+        while ((match = thisFunctionPattern.exec(codeOnlyLine)) !== null) {
+            const matchStart = match.index;
+            const matchEnd = matchStart + match[0].length;
+            
+            if (processedRanges.some(r => matchStart >= r.start && matchEnd <= r.end)) {
+                continue;
+            }
+
+            const funcName = match[1];
+            
+            if (userDefinedFunctions.has(funcName)) {
+                continue;
+            }
+
+            const startPos = match.index + match[0].indexOf(funcName);
+            const range = new vscode.Range(lineNumber, startPos, lineNumber, startPos + funcName.length);
+            diagnostics.push(new vscode.Diagnostic(
+                range,
+                `Function '${funcName}' is not defined`,
+                vscode.DiagnosticSeverity.Warning
+            ));
+        }
+    }
+
+    private checkModifierUsage(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument, userDefinedModifiers: Set, extensions: import('./extensions').IslExtensions, importedModifiers: Map>) {
+        // Skip comments - only check code part
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+        
+        // Check for | modifierName usage (including multi-level like regex.find or Math.sum)
+        // Also check for | ModuleName.modifierName (imported modifiers)
+        const modifierPattern = /\|\s*([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*(?:\(|$|\|)/g;
+        let match;
+
+        while ((match = modifierPattern.exec(codeOnlyLine)) !== null) {
+            const modifierName = match[1];
+            
+            // Check if it's a user-defined modifier or custom extension modifier
+            if (userDefinedModifiers.has(modifierName) || getExtensionModifier(extensions, modifierName)) {
+                continue;
+            }
+            
+            // Check if it's a built-in modifier (exact match)
+            if (this.builtInModifiers.has(modifierName)) {
+                continue;
+            }
+            
+            // Check if it matches a wildcard pattern (e.g., regex.* matches regex.find)
+            let isBuiltIn = false;
+            for (const builtIn of this.builtInModifiers) {
+                if (builtIn.endsWith('.*')) {
+                    const prefix = builtIn.slice(0, -1); // Remove the *
+                    if (modifierName.startsWith(prefix)) {
+                        isBuiltIn = true;
+                        break;
+                    }
+                }
+            }
+            
+            // Check if it's also in the builtInFunctions set (Math.sum, etc. can be used as modifiers)
+            if (this.builtInFunctions.has(modifierName)) {
+                isBuiltIn = true;
+            }
+            
+            if (isBuiltIn) {
+                continue;
+            }
+
+            // Check if it's an imported modifier: | ModuleName.modifierName
+            const parts = modifierName.split('.');
+            if (parts.length === 2) {
+                const [moduleName, modName] = parts;
+                
+                // Skip if it's a built-in namespace (Date, Math, etc.)
+                if (this.builtInNamespaces.has(moduleName)) {
+                    // Check if it's a valid built-in function that can be used as a modifier (case-insensitive)
+                    const builtInKey = `${moduleName}.${modName}`;
+                    const builtInKeyLower = builtInKey.toLowerCase();
+                    let isBuiltIn = false;
+                    
+                    for (const builtIn of this.builtInFunctions) {
+                        if (builtIn.toLowerCase() === builtInKeyLower) {
+                            isBuiltIn = true;
+                            break;
+                        }
+                    }
+                    
+                    if (isBuiltIn) {
+                        continue;
+                    } else {
+                        // Built-in namespace but modifier doesn't exist
+                        const modifierStart = match.index + match[0].indexOf(modifierName) + (moduleName.length + 1);
+                        const range = new vscode.Range(lineNumber, modifierStart, lineNumber, modifierStart + modName.length);
+                        diagnostics.push(new vscode.Diagnostic(
+                            range,
+                            `Modifier '${modName}' is not a valid ${moduleName} modifier`,
+                            vscode.DiagnosticSeverity.Warning
+                        ));
+                        continue;
+                    }
+                }
+
+                // Check if it's an imported module
+                if (importedModifiers.has(moduleName)) {
+                    if (importedModifiers.get(moduleName)!.has(modName)) {
+                        continue;
+                    } else {
+                        const modifierStart = match.index + match[0].indexOf(modifierName) + (moduleName.length + 1);
+                        const range = new vscode.Range(lineNumber, modifierStart, lineNumber, modifierStart + modName.length);
+                        diagnostics.push(new vscode.Diagnostic(
+                            range,
+                            `Modifier '${modName}' is not exported from module '${moduleName}'`,
+                            vscode.DiagnosticSeverity.Warning
+                        ));
+                        continue;
+                    }
+                }
+                // Before "module not imported": check if this is a global extension (function or modifier)
+                if (getExtensionFunction(extensions, moduleName)) {
+                    const moduleStart = match.index + match[0].indexOf(moduleName);
+                    const range = new vscode.Range(lineNumber, moduleStart, lineNumber, moduleStart + moduleName.length);
+                    diagnostics.push(new vscode.Diagnostic(
+                        range,
+                        `Use @.${moduleName}() for extension functions, not | ${moduleName}.${modName}`,
+                        vscode.DiagnosticSeverity.Warning
+                    ));
+                    continue;
+                }
+                if (getExtensionModifier(extensions, moduleName)) {
+                    const moduleStart = match.index + match[0].indexOf(moduleName);
+                    const range = new vscode.Range(lineNumber, moduleStart, lineNumber, moduleStart + moduleName.length);
+                    diagnostics.push(new vscode.Diagnostic(
+                        range,
+                        `Global extension modifiers are used as | ${moduleName}, not | ${moduleName}.${modName}`,
+                        vscode.DiagnosticSeverity.Warning
+                    ));
+                    continue;
+                }
+                const imports = this.extractImports(document);
+                if (!imports.has(moduleName)) {
+                    const moduleStart = match.index + match[0].indexOf(moduleName);
+                    const range = new vscode.Range(lineNumber, moduleStart, lineNumber, moduleStart + moduleName.length);
+                    diagnostics.push(new vscode.Diagnostic(
+                        range,
+                        `Module '${moduleName}' is not imported`,
+                        vscode.DiagnosticSeverity.Warning
+                    ));
+                    continue;
+                }
+            }
+
+            const startPos = match.index + match[0].indexOf(modifierName);
+            const range = new vscode.Range(lineNumber, startPos, lineNumber, startPos + modifierName.length);
+            diagnostics.push(new vscode.Diagnostic(
+                range,
+                `Modifier '${modifierName}' is not defined`,
+                vscode.DiagnosticSeverity.Warning
+            ));
+        }
+    }
+
+    private checkVariableUsage(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument, declaredVariables: Map) {
+        // Skip comments - remove everything after // or #
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+        
+        // Skip if the entire line is a comment
+        if (codeOnlyLine.trim() === '') {
+            return;
+        }
+
+        // Check if this line has an assignment (variable on left side of = or :)
+        // Match: $var = ... or $var.prop = ... or $var.prop.nested = ... or prop: $var or prop: value
+        const assignmentMatch = codeOnlyLine.match(/^(\s*)(\$[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*[=:]\s*(.+)/);
+        if (assignmentMatch) {
+            const leftSide = assignmentMatch[2]; // The variable being assigned to
+            const rightSide = assignmentMatch[3]; // The value being assigned
+            
+            // Extract the base variable from the left side (e.g., $var from $var.prop.nested)
+            const baseVarMatch = leftSide.match(/\$([a-zA-Z_][a-zA-Z0-9_]*)/);
+            const baseVarBeingDeclared = baseVarMatch ? baseVarMatch[1] : null;
+            
+            // Only check variables on the RIGHT side of assignment
+            // But exclude the base variable being declared from the check
+            this.checkVariablesInExpression(rightSide, lineNumber, diagnostics, declaredVariables, codeOnlyLine.indexOf(rightSide), baseVarBeingDeclared);
+            return;
+        }
+        
+        // Also check for object property syntax: propertyName: value (without $ on left)
+        const propertyMatch = codeOnlyLine.match(/^(\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.+)/);
+        if (propertyMatch) {
+            const rightSide = propertyMatch[3];
+            // Check variables on the right side of the colon
+            this.checkVariablesInExpression(rightSide, lineNumber, diagnostics, declaredVariables, codeOnlyLine.indexOf(rightSide), null);
+            return;
+        }
+
+        // Check all variable usages in the code part of the line (not comments)
+        this.checkVariablesInExpression(codeOnlyLine, lineNumber, diagnostics, declaredVariables, 0, null);
+    }
+
+    private checkVariablesInExpression(expression: string, lineNumber: number, diagnostics: vscode.Diagnostic[], declaredVariables: Map, offset: number, excludeVar: string | null) {
+        // Find all variable references: $varName or $varName.property
+        const varPattern = /\$([a-zA-Z_][a-zA-Z0-9_]*)(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*/g;
+        let match;
+
+        while ((match = varPattern.exec(expression)) !== null) {
+            const baseVarName = match[1]; // Just the base variable name without $
+            
+            // Skip special variables created automatically by modifiers and contexts:
+            // - it: current item in reduce, general iteration contexts
+            // - fit: current item being filtered in filter()
+            // - acc: accumulator in reduce()
+            // - index: index in iteration contexts
+            // - key, value: key-value iteration
+            // - this, This: self-reference to current context/function
+            const specialVars = ['it', 'fit', 'acc', 'index', 'key', 'value', 'this', 'This'];
+            if (specialVars.includes(baseVarName)) {
+                continue;
+            }
+
+            // Skip the variable being declared on this line (if any)
+            if (excludeVar && baseVarName === excludeVar) {
+                continue;
+            }
+
+            // Check if variable has been declared
+            if (!declaredVariables.has(baseVarName)) {
+                const startPos = offset + match.index;
+                const range = new vscode.Range(lineNumber, startPos, lineNumber, startPos + match[0].length);
+                diagnostics.push(new vscode.Diagnostic(
+                    range,
+                    `Variable '$${baseVarName}' is used before being declared`,
+                    vscode.DiagnosticSeverity.Warning
+                ));
+            }
+        }
+    }
+
+    private checkLongObjectDeclaration(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Check for long single-line object declarations that should be formatted
+        if (line.length < 100) {
+            return;
+        }
+
+        // Skip comments
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+
+        // Check if line contains object with multiple properties (at least 2 colons indicating properties)
+        const objectMatch = codeOnlyLine.match(/\{[^}]*:[^}]*:[^}]*\}/);
+        if (!objectMatch) {
+            return;
+        }
+
+        // Find the position of the opening brace
+        const bracePos = codeOnlyLine.indexOf('{');
+        if (bracePos === -1) {
+            return;
+        }
+
+        const range = new vscode.Range(lineNumber, bracePos, lineNumber, bracePos + 1);
+        const diagnostic = new vscode.Diagnostic(
+            range,
+            'Long object declaration can be formatted on multiple lines',
+            vscode.DiagnosticSeverity.Hint
+        );
+        diagnostic.code = 'format-object';
+        diagnostics.push(diagnostic);
+    }
+
+    private checkUnnecessaryStringInterpolation(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Skip comments
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+
+        // Check for ${$variable} pattern (without dots - simple variables only)
+        const unnecessaryInterpolation = /\$\{(\$[a-zA-Z_][a-zA-Z0-9_]*)\}/g;
+        let match;
+
+        while ((match = unnecessaryInterpolation.exec(codeOnlyLine)) !== null) {
+            const fullMatch = match[0]; // ${$variable}
+            const variable = match[1];  // $variable
+
+            // Only flag simple variables (no property access)
+            if (!variable.includes('.')) {
+                const startPos = match.index;
+                const endPos = startPos + fullMatch.length;
+                const range = new vscode.Range(lineNumber, startPos, lineNumber, endPos);
+                
+                const diagnostic = new vscode.Diagnostic(
+                    range,
+                    `Unnecessary braces around ${variable} in string interpolation`,
+                    vscode.DiagnosticSeverity.Hint
+                );
+                diagnostic.code = 'simplify-interpolation';
+                diagnostics.push(diagnostic);
+            }
+        }
+    }
+
+    /**
+     * Flags template literals that contain only one ${ ... } and no other text.
+     * E.g. ` ${ $var.abc }` is valid (space in front). ` ${ $var.abc } ` is unnecessary.
+     */
+    private checkUnnecessarySingleInterpolationTemplate(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+
+        const backtickPattern = /`(?:[^`\\]|\\.)*`/g;
+        let backtickMatch;
+
+        while ((backtickMatch = backtickPattern.exec(codeOnlyLine)) !== null) {
+            const templateString = backtickMatch[0];
+            const templateStart = backtickMatch.index;
+            const content = templateString.slice(1, -1);
+
+            const inner = this.parseSingleInterpolationOnly(content);
+            if (inner !== null) {
+                const range = new vscode.Range(
+                    lineNumber,
+                    templateStart,
+                    lineNumber,
+                    templateStart + templateString.length
+                );
+                const diagnostic = new vscode.Diagnostic(
+                    range,
+                    'Unnecessary template literal: string has only one interpolation. Use the expression directly.',
+                    vscode.DiagnosticSeverity.Warning
+                );
+                diagnostic.code = 'unnecessary-template-literal';
+                (diagnostic as vscode.Diagnostic & { innerExpression?: string }).innerExpression = inner;
+                diagnostics.push(diagnostic);
+            }
+        }
+    }
+
+    /**
+     * If content (between backticks) is exactly one ${ ... } with no other text (including no space before/after),
+     * returns the inner expression; otherwise null. Handles nested braces inside ${ }.
+     * E.g. ` ${ ... }` has a space in front so is valid (returns null).
+     */
+    private parseSingleInterpolationOnly(content: string): string | null {
+        const open = content.indexOf('${');
+        if (open === -1) return null;
+        if (open > 0) return null; // any character (including space) before ${ means valid "extra text"
+
+        let depth = 1;
+        let i = open + 2;
+        while (i < content.length) {
+            const c = content[i];
+            if (c === '\\') {
+                i += 2;
+                continue;
+            }
+            if (c === '{') depth++;
+            else if (c === '}') {
+                depth--;
+                if (depth === 0) {
+                    const inner = content.substring(open + 2, i).trim();
+                    const after = content.substring(i + 1);
+                    if (after.length > 0) return null; // any character (including space) after } means valid
+                    return inner;
+                }
+            }
+            i++;
+        }
+        return null;
+    }
+
+    /**
+     * ISL does not support + for string concatenation. Use template literals with ${ } instead.
+     * Example: $val = "a=" + $x + "&b=" + $y  →  $val = `a=${ $x }&b=${ $y }`
+     */
+    private checkPlusStringConcatenation(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex).trimEnd() : line;
+        // Require assignment: $var =  and rhs contains both a string literal and +
+        const assignMatch = codeOnlyLine.match(/^\s*(\$[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/);
+        if (!assignMatch) return;
+        const rhs = assignMatch[2].trim();
+        // Must contain at least one quoted string and a + (concatenation)
+        const hasQuotedString = /["'][^"']*["']/.test(rhs);
+        const hasConcatPlus = /["'][^"']*["']\s*\+|\+\s*["'][^"']*["']/.test(rhs) ||
+            /\$[a-zA-Z_][a-zA-Z0-9_.]*\s*\+|\+\s*\$[a-zA-Z_][a-zA-Z0-9_.]*/.test(rhs);
+        if (!hasQuotedString || !hasConcatPlus) return;
+        const eqIdx = codeOnlyLine.indexOf('=');
+        let rhsStart = eqIdx + 1;
+        while (rhsStart < codeOnlyLine.length && /\s/.test(codeOnlyLine[rhsStart])) rhsStart++;
+        const rhsEnd = codeOnlyLine.length;
+        const range = new vscode.Range(lineNumber, rhsStart, lineNumber, rhsEnd);
+        const diagnostic = new vscode.Diagnostic(
+            range,
+            'ISL does not support + for string concatenation. Use string interpolation: `...${ expr }...` (backtick template literals).',
+            vscode.DiagnosticSeverity.Error
+        );
+        diagnostic.code = 'no-plus-concatenation';
+        diagnostics.push(diagnostic);
+    }
+
+    private checkDefaultModifier(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Skip comments
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+
+        // Check for | default() pattern
+        const defaultPattern = /\|\s*(default)\s*\(/g;
+        let match;
+
+        while ((match = defaultPattern.exec(codeOnlyLine)) !== null) {
+            const startPos = match.index;
+            const endPos = startPos + match[0].length - 1; // Exclude the opening paren
+            const range = new vscode.Range(lineNumber, startPos, lineNumber, endPos);
+            
+            const diagnostic = new vscode.Diagnostic(
+                range,
+                'Consider using ?? operator instead of | default()',
+                vscode.DiagnosticSeverity.Hint
+            );
+            diagnostic.code = 'use-coalesce-operator';
+            diagnostics.push(diagnostic);
+        }
+    }
+
+    private checkPaginationPropertyAccess(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument, paginationVariables: Map) {
+        // Skip comments
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+        
+        // Find pagination variable property access: $varName.propertyName
+        const propertyAccessPattern = /\$([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)/g;
+        let match;
+        
+        while ((match = propertyAccessPattern.exec(codeOnlyLine)) !== null) {
+            const varName = match[1];
+            const propertyName = match[2];
+            
+            if (paginationVariables.has(varName)) {
+                const paginationType = paginationVariables.get(varName)!;
+                const validProperties = this.getValidPaginationProperties(paginationType);
+                
+                if (!validProperties.includes(propertyName)) {
+                    const startPos = match.index + match[0].indexOf(propertyName);
+                    const range = new vscode.Range(lineNumber, startPos, lineNumber, startPos + propertyName.length);
+                    
+                    const validPropsStr = validProperties.join(', ');
+                    diagnostics.push(new vscode.Diagnostic(
+                        range,
+                        `Invalid property '${propertyName}' for pagination ${paginationType}. Valid properties are: ${validPropsStr}`,
+                        vscode.DiagnosticSeverity.Error
+                    ));
+                }
+            }
+        }
+    }
+    
+    private getValidPaginationProperties(paginationType: string): string[] {
+        switch (paginationType) {
+            case 'Cursor':
+                return ['current', 'next'];
+            case 'Page':
+                return ['startIndex', 'pageSize', 'page', 'fromOffset', 'toOffset', 'hasMorePages'];
+            case 'Date':
+                return ['startDate', 'endDate', 'page'];
+            case 'Offset':
+                // Placeholder - will be filled in next request
+                return [];
+            case 'Keyset':
+                // Placeholder - will be filled in next request
+                return [];
+            default:
+                return [];
+        }
+    }
+    
+    private checkColonAssignment(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Skip comments
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+
+        // Check for variable assignment using : instead of =
+        // Match: $varName: (at the start of line, optionally with whitespace)
+        const colonAssignmentPattern = /^(\s*)(\$[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)(\s*)(:)/;
+        const match = codeOnlyLine.match(colonAssignmentPattern);
+
+        if (match) {
+            // Skip when this is typed assignment: $var : type = ... (colon is type separator, = is assignment)
+            const afterColon = codeOnlyLine.substring(match.index! + match[0].length);
+            if (/^\s*[a-zA-Z_][a-zA-Z0-9_.:]*\s*=/.test(afterColon)) {
+                return; // $val: type = { ... } is valid
+            }
+
+            const colonPos = match.index! + match[1].length + match[2].length + match[3].length;
+            const range = new vscode.Range(lineNumber, colonPos, lineNumber, colonPos + 1);
+            
+            const diagnostic = new vscode.Diagnostic(
+                range,
+                'Use = instead of : for variable assignment',
+                vscode.DiagnosticSeverity.Hint
+            );
+            diagnostic.code = 'use-equals-assignment';
+            diagnostics.push(diagnostic);
+        }
+    }
+
+    private checkMathInTemplateString(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Skip comments
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+
+        // Find all template strings (backtick strings) in the line
+        // Pattern to find backtick strings: `...`
+        // We need to be careful about escaped backticks
+        const backtickPattern = /`(?:[^`\\]|\\.)*`/g;
+        let backtickMatch;
+        
+        while ((backtickMatch = backtickPattern.exec(codeOnlyLine)) !== null) {
+            const templateString = backtickMatch[0];
+            const templateStart = backtickMatch.index;
+            
+            // Extract the content inside the backticks (without the backticks themselves)
+            const content = templateString.slice(1, -1);
+            
+            // Helper function to check if a position range is inside any of the exclusion blocks
+            const isInsideExclusionBlocks = (start: number, end: number, exclusionBlocks: Array<{ start: number; end: number }>): boolean => {
+                return exclusionBlocks.some(block => start >= block.start && end <= block.end);
+            };
+            
+            // Find all {{ ... }} blocks to exclude them (these are already correct)
+            const mathBlockPattern = /\{\{([^}]+)\}\}/g;
+            const mathBlocks: Array<{ start: number; end: number }> = [];
+            let mathBlockMatch;
+            while ((mathBlockMatch = mathBlockPattern.exec(content)) !== null) {
+                mathBlocks.push({
+                    start: mathBlockMatch.index,
+                    end: mathBlockMatch.index + mathBlockMatch[0].length
+                });
+            }
+            
+            // Find all ${ ... } interpolation blocks to exclude them (these are correct)
+            const interpolationPattern = /\$\{([^}]+)\}/g;
+            const interpolationBlocks: Array<{ start: number; end: number }> = [];
+            let interpolationMatch;
+            while ((interpolationMatch = interpolationPattern.exec(content)) !== null) {
+                interpolationBlocks.push({
+                    start: interpolationMatch.index,
+                    end: interpolationMatch.index + interpolationMatch[0].length
+                });
+            }
+            
+            // Combine all exclusion blocks
+            const allExclusionBlocks = [...mathBlocks, ...interpolationBlocks];
+            
+            // Find math expressions: $var operator (number|$var) or (number|$var) operator $var
+            // Math operators: +, -, *, /, %
+            // Pattern matches: $var * 1.1, $var + $var, 10 * $var, etc.
+            const mathExpressionPattern = /((?:\$[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*|\d+(?:\.\d+)?))\s*([+\-*/%])\s*((?:\$[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*|\d+(?:\.\d+)?))/g;
+            let mathMatch;
+            while ((mathMatch = mathExpressionPattern.exec(content)) !== null) {
+                const matchStart = mathMatch.index;
+                const matchEnd = matchStart + mathMatch[0].length;
+                
+                // Skip if already inside an exclusion block
+                if (isInsideExclusionBlocks(matchStart, matchEnd, allExclusionBlocks)) {
+                    continue;
+                }
+                
+                // This is a math expression that needs {{ }} wrapping
+                // Calculate the absolute position in the document line
+                const absoluteStart = templateStart + 1 + matchStart; // +1 for opening backtick
+                const absoluteEnd = templateStart + 1 + matchEnd;
+                
+                const range = new vscode.Range(
+                    lineNumber,
+                    absoluteStart,
+                    lineNumber,
+                    absoluteEnd
+                );
+                
+                const diagnostic = new vscode.Diagnostic(
+                    range,
+                    'Math expressions in template strings must be wrapped in {{ }}',
+                    vscode.DiagnosticSeverity.Warning
+                );
+                diagnostic.code = 'math-outside-braces';
+                diagnostics.push(diagnostic);
+            }
+        }
+    }
+
+    private checkConsecutiveFilters(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Skip comments
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+
+        // Find consecutive filter operations: | filter(...) | filter(...)
+        // Pattern: | filter(condition) | filter(condition)
+        const filterPattern = /\|\s*filter\s*\(([^)]+)\)/g;
+        const filterMatches: Array<{ match: RegExpMatchArray; condition: string; start: number; end: number }> = [];
+        
+        let filterMatch;
+        while ((filterMatch = filterPattern.exec(codeOnlyLine)) !== null) {
+            filterMatches.push({
+                match: filterMatch,
+                condition: filterMatch[1].trim(),
+                start: filterMatch.index,
+                end: filterMatch.index + filterMatch[0].length
+            });
+        }
+
+        // Check if we have at least 2 consecutive filters
+        if (filterMatches.length < 2) {
+            return;
+        }
+
+        // Find the longest sequence of consecutive filters
+        let consecutiveStart = -1;
+        let consecutiveEnd = -1;
+        
+        for (let i = 0; i < filterMatches.length - 1; i++) {
+            const currentFilter = filterMatches[i];
+            const nextFilter = filterMatches[i + 1];
+            
+            // Get the text between the two filters
+            const textBetween = codeOnlyLine.substring(currentFilter.end, nextFilter.start);
+            
+            // If there's only whitespace or pipes between them, they're consecutive
+            const betweenTrimmed = textBetween.trim();
+            if (betweenTrimmed === '' || betweenTrimmed === '|') {
+                // Mark the start of consecutive sequence
+                if (consecutiveStart === -1) {
+                    consecutiveStart = currentFilter.start;
+                }
+                consecutiveEnd = nextFilter.end;
+            } else {
+                // If we found a consecutive sequence, create diagnostic and reset
+                if (consecutiveStart !== -1 && consecutiveEnd !== -1) {
+                    const range = new vscode.Range(
+                        lineNumber,
+                        consecutiveStart,
+                        lineNumber,
+                        consecutiveEnd
+                    );
+                    
+                    const diagnostic = new vscode.Diagnostic(
+                        range,
+                        'Consecutive filter operations can be combined with "and" operator',
+                        vscode.DiagnosticSeverity.Hint
+                    );
+                    diagnostic.code = 'inefficient-filter';
+                    diagnostics.push(diagnostic);
+                    
+                    // Reset for next potential sequence
+                    consecutiveStart = -1;
+                    consecutiveEnd = -1;
+                }
+            }
+        }
+        
+        // Check if we ended with a consecutive sequence
+        if (consecutiveStart !== -1 && consecutiveEnd !== -1) {
+            const range = new vscode.Range(
+                lineNumber,
+                consecutiveStart,
+                lineNumber,
+                consecutiveEnd
+            );
+            
+            const diagnostic = new vscode.Diagnostic(
+                range,
+                'Consecutive filter operations can be combined with "and" operator',
+                vscode.DiagnosticSeverity.Hint
+            );
+            diagnostic.code = 'inefficient-filter';
+            diagnostics.push(diagnostic);
+        }
+    }
+
+    private checkForeachToMap(document: vscode.TextDocument, diagnostics: vscode.Diagnostic[]) {
+        const text = document.getText();
+        const lines = text.split('\n');
+        
+        // Find all foreach loops
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            const trimmed = line.trim();
+            
+            // Check if this line starts a foreach loop
+            const foreachMatch = trimmed.match(/^foreach\s+\$([a-zA-Z_][a-zA-Z0-9_]*)\s+in\s+(.+)/);
+            if (!foreachMatch) {
+                continue;
+            }
+            
+            const loopVar = foreachMatch[1];
+            const arrayVar = foreachMatch[2].trim();
+            const foreachStartLine = i;
+            
+            // Find the matching endfor (track foreach/endfor nesting so inner loops don't close outer)
+            let foreachDepth = 1;
+            let foundEndfor = false;
+            let endforLine = -1;
+            
+            for (let j = i + 1; j < lines.length; j++) {
+                const currentLine = lines[j];
+                const currentTrimmed = currentLine.trim();
+                
+                // Skip comments
+                const commentIndex = Math.min(
+                    currentLine.indexOf('//') !== -1 ? currentLine.indexOf('//') : Infinity,
+                    currentLine.indexOf('#') !== -1 ? currentLine.indexOf('#') : Infinity
+                );
+                const codeOnly = commentIndex !== Infinity ? currentLine.substring(0, commentIndex).trim() : currentTrimmed;
+                
+                if (/^foreach\s/.test(codeOnly)) {
+                    foreachDepth++;
+                } else if (codeOnly === 'endfor') {
+                    foreachDepth--;
+                    if (foreachDepth === 0) {
+                        foundEndfor = true;
+                        endforLine = j;
+                        break;
+                    }
+                }
+            }
+            
+            if (!foundEndfor) {
+                continue; // Skip if no matching endfor found
+            }
+            
+            // Check lines before the foreach for array initialization
+            // Look for pattern: $varName: [] or $varName = []
+            let arrayVarName: string | null = null;
+            let arrayInitLine = -1;
+            
+            for (let j = Math.max(0, foreachStartLine - 5); j < foreachStartLine; j++) {
+                const prevLine = lines[j];
+                const commentIndex = Math.min(
+                    prevLine.indexOf('//') !== -1 ? prevLine.indexOf('//') : Infinity,
+                    prevLine.indexOf('#') !== -1 ? prevLine.indexOf('#') : Infinity
+                );
+                const codeLine = commentIndex !== Infinity ? prevLine.substring(0, commentIndex) : prevLine;
+                
+                // Match: $varName: [] or $varName = []
+                const arrayInitMatch = codeLine.match(/\$([a-zA-Z_][a-zA-Z0-9_]*)\s*[=:]\s*\[\s*\]/);
+                if (arrayInitMatch) {
+                    arrayVarName = arrayInitMatch[1];
+                    arrayInitLine = j;
+                    break;
+                }
+            }
+            
+            if (!arrayVarName) {
+                continue; // No array initialization found
+            }
+            
+            // Check the loop body for push operations
+            // Pattern: $arrayVarName: $arrayVarName | push(expression)
+            let foundPush = false;
+            let pushExpression: string | null = null;
+            let pushLine = -1;
+            let hasOtherStatements = false;
+            
+            for (let j = foreachStartLine + 1; j < endforLine; j++) {
+                const bodyLine = lines[j];
+                const trimmedBody = bodyLine.trim();
+                
+                // Skip empty lines and comments
+                if (trimmedBody === '' || trimmedBody.startsWith('//') || trimmedBody.startsWith('#')) {
+                    continue;
+                }
+                
+                const commentIndex = Math.min(
+                    bodyLine.indexOf('//') !== -1 ? bodyLine.indexOf('//') : Infinity,
+                    bodyLine.indexOf('#') !== -1 ? bodyLine.indexOf('#') : Infinity
+                );
+                const codeLine = commentIndex !== Infinity ? bodyLine.substring(0, commentIndex) : bodyLine;
+                const trimmedCode = codeLine.trim();
+                
+                // Match: $arrayVarName: $arrayVarName | push(expression)
+                // Also match: $arrayVarName = $arrayVarName | push(expression)
+                const pushPattern = new RegExp(`\\$${arrayVarName}\\s*[=:]\\s*\\$${arrayVarName}\\s*\\|\\s*push\\s*\\(([^)]+)\\)`, 'g');
+                const pushMatch = pushPattern.exec(trimmedCode);
+                
+                if (pushMatch) {
+                    if (!foundPush) {
+                        foundPush = true;
+                        pushExpression = pushMatch[1].trim();
+                        pushLine = j;
+                    }
+                } else {
+                    // Check if this is another statement (not just whitespace)
+                    if (trimmedCode.length > 0) {
+                        hasOtherStatements = true;
+                    }
+                }
+            }
+            
+            // Only suggest conversion if we found a push and no other statements
+            if (!foundPush || !pushExpression || hasOtherStatements) {
+                continue;
+            }
+            
+            // Check if the push expression uses the loop variable
+            // Replace $loopVar with $ in the expression to convert to map syntax
+            const mapExpression = pushExpression.replace(new RegExp(`\\$${loopVar}`, 'g'), '$');
+            
+            // Create diagnostic covering the foreach loop
+            const foreachLine = document.lineAt(foreachStartLine);
+            const endforLineObj = document.lineAt(endforLine);
+            const range = new vscode.Range(
+                foreachStartLine,
+                0,
+                endforLine,
+                endforLineObj.text.length
+            );
+            
+            const diagnostic = new vscode.Diagnostic(
+                range,
+                'Foreach loop can be replaced with map() modifier',
+                vscode.DiagnosticSeverity.Hint
+            );
+            diagnostic.code = 'foreach-to-map';
+            // Store metadata for the quick fix
+            (diagnostic as any).arrayVarName = arrayVarName;
+            (diagnostic as any).arrayInitLine = arrayInitLine;
+            (diagnostic as any).arrayVar = arrayVar;
+            (diagnostic as any).mapExpression = mapExpression;
+            (diagnostic as any).foreachStartLine = foreachStartLine;
+            (diagnostic as any).endforLine = endforLine;
+            diagnostics.push(diagnostic);
+        }
+    }
+
+    /**
+     * Warn when a variable is declared as empty array [] and only used once with | push(...).
+     * Better pattern is to use [ item ] directly and remove the intermediary variable.
+     */
+    private checkSinglePushArray(document: vscode.TextDocument, diagnostics: vscode.Diagnostic[]) {
+        const text = document.getText();
+        const lines = text.split('\n');
+
+        // Find all variables declared as [] ($var = [] or $var: [])
+        const arrayDeclarations: { varName: string; lineIndex: number }[] = [];
+        for (let i = 0; i < lines.length; i++) {
+            const codeLine = this.getCodeOnlyLine(lines[i]);
+            const declMatch = codeLine.match(/\$([a-zA-Z_][a-zA-Z0-9_]*)\s*[=:]\s*\[\s*\]/);
+            if (declMatch) {
+                arrayDeclarations.push({ varName: declMatch[1], lineIndex: i });
+            }
+        }
+
+        for (const { varName, lineIndex: initLineIndex } of arrayDeclarations) {
+            const varPattern = new RegExp(`\\$${varName}\\b`, 'g');
+            let usageCount = 0;
+            let pushMatch: { startOffset: number; endOffset: number; pushArgument: string } | null = null;
+
+            let lastIndex = 0;
+            let match: RegExpExecArray | null;
+            while ((match = varPattern.exec(text)) !== null) {
+                // Skip the declaration line
+                const matchLine = document.positionAt(match.index).line;
+                if (matchLine === initLineIndex) {
+                    continue;
+                }
+                usageCount++;
+                if (usageCount > 1) break;
+
+                // Check if this usage is in pattern $var | push ( ... )
+                const afterVar = text.slice(match.index);
+                const pushRegex = new RegExp(`^\\$${varName}\\b\\s*\\|\\s*push\\s*\\(`, '');
+                const pushStart = afterVar.match(pushRegex);
+                if (pushStart) {
+                    const openParenOffset = match.index + pushStart[0].length;
+                    const closeParenOffset = this.findMatchingCloseParen(text, openParenOffset);
+                    if (closeParenOffset !== -1) {
+                        const pushArgument = text.slice(openParenOffset, closeParenOffset).trim();
+                        pushMatch = {
+                            startOffset: match.index,
+                            endOffset: closeParenOffset + 1,
+                            pushArgument
+                        };
+                    }
+                }
+            }
+
+            if (usageCount === 1 && pushMatch) {
+                const range = new vscode.Range(
+                    document.positionAt(pushMatch.startOffset),
+                    document.positionAt(pushMatch.endOffset)
+                );
+                const diagnostic = new vscode.Diagnostic(
+                    range,
+                    `Variable '$${varName}' is declared as [] and only used once with | push(). Use [ ${pushMatch.pushArgument} ] instead.`,
+                    vscode.DiagnosticSeverity.Warning
+                );
+                diagnostic.code = 'single-push-array';
+                (diagnostic as any).arrayVarName = varName;
+                (diagnostic as any).initLineIndex = initLineIndex;
+                (diagnostic as any).pushArgument = pushMatch.pushArgument;
+                (diagnostic as any).usageStartOffset = pushMatch.startOffset;
+                (diagnostic as any).usageEndOffset = pushMatch.endOffset;
+                diagnostics.push(diagnostic);
+            }
+        }
+    }
+
+    /** Return offset of matching closing ')' for the '(' at openOffset, or -1. */
+    private findMatchingCloseParen(text: string, openOffset: number): number {
+        let depth = 1;
+        let i = openOffset + 1;
+        while (i < text.length && depth > 0) {
+            const ch = text[i];
+            if (ch === '(') depth++;
+            else if (ch === ')') depth--;
+            i++;
+        }
+        return depth === 0 ? i - 1 : -1;
+    }
+
+    private getCodeOnlyLine(line: string): string {
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        return commentIndex !== Infinity ? line.substring(0, commentIndex).trim() : line.trim();
+    }
+
+    private checkNamingConvention(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Get naming convention from configuration
+        const config = vscode.workspace.getConfiguration('isl.naming');
+        const convention = config.get('convention', 'camelCase');
+        
+        // Skip if convention is not set or disabled
+        if (!convention) {
+            return;
+        }
+
+        // Skip comments
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+
+        // Check function declarations: fun functionName(
+        const funMatch = codeOnlyLine.match(/^\s*fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
+        if (funMatch) {
+            const funcName = funMatch[1];
+            const funcNameStart = codeOnlyLine.indexOf(funcName);
+            
+            if (!this.matchesNamingConvention(funcName, convention)) {
+                const correctName = this.convertToNamingConvention(funcName, convention);
+                const range = new vscode.Range(
+                    lineNumber,
+                    funcNameStart,
+                    lineNumber,
+                    funcNameStart + funcName.length
+                );
+                
+                const diagnostic = new vscode.Diagnostic(
+                    range,
+                    `Function name '${funcName}' should be ${convention}. Suggested: '${correctName}'`,
+                    vscode.DiagnosticSeverity.Hint
+                );
+                diagnostic.code = 'naming-convention';
+                (diagnostic as any).originalName = funcName;
+                (diagnostic as any).correctName = correctName;
+                (diagnostic as any).type = 'function';
+                diagnostics.push(diagnostic);
+            }
+        }
+
+        // Check modifier declarations: modifier modifierName(
+        const modMatch = codeOnlyLine.match(/^\s*modifier\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
+        if (modMatch) {
+            const modName = modMatch[1];
+            const modNameStart = codeOnlyLine.indexOf(modName);
+            
+            if (!this.matchesNamingConvention(modName, convention)) {
+                const correctName = this.convertToNamingConvention(modName, convention);
+                const range = new vscode.Range(
+                    lineNumber,
+                    modNameStart,
+                    lineNumber,
+                    modNameStart + modName.length
+                );
+                
+                const diagnostic = new vscode.Diagnostic(
+                    range,
+                    `Modifier name '${modName}' should be ${convention}. Suggested: '${correctName}'`,
+                    vscode.DiagnosticSeverity.Hint
+                );
+                diagnostic.code = 'naming-convention';
+                (diagnostic as any).originalName = modName;
+                (diagnostic as any).correctName = correctName;
+                (diagnostic as any).type = 'modifier';
+                diagnostics.push(diagnostic);
+            }
+        }
+    }
+
+    private matchesNamingConvention(name: string, convention: string): boolean {
+        switch (convention) {
+            case 'PascalCase':
+                // PascalCase: First letter uppercase, rest can be lowercase or uppercase
+                // Examples: TransformVariant, GetUser, ProcessData
+                return /^[A-Z][a-zA-Z0-9]*$/.test(name);
+            case 'camelCase':
+                // camelCase: First letter lowercase, rest can be mixed
+                // Examples: transformVariant, getUser, processData
+                return /^[a-z][a-zA-Z0-9]*$/.test(name);
+            case 'snake_case':
+                // snake_case: All lowercase with underscores
+                // Examples: transform_variant, get_user, process_data
+                return /^[a-z][a-z0-9_]*$/.test(name) && !/[A-Z]/.test(name);
+            default:
+                return true; // Unknown convention, don't enforce
+        }
+    }
+
+    private convertToNamingConvention(name: string, convention: string): string {
+        // First, normalize the name by splitting on capital letters, underscores, and numbers
+        // This handles: PascalCase, camelCase, snake_case, and mixed cases
+        
+        // Split on capital letters, underscores, and numbers
+        const parts: string[] = [];
+        let currentPart = '';
+        
+        for (let i = 0; i < name.length; i++) {
+            const char = name[i];
+            const isUpper = /[A-Z]/.test(char);
+            const isLower = /[a-z]/.test(char);
+            const isUnderscore = char === '_';
+            const isNumber = /[0-9]/.test(char);
+            
+            if (isUnderscore) {
+                if (currentPart) {
+                    parts.push(currentPart.toLowerCase());
+                    currentPart = '';
+                }
+            } else if (isUpper && currentPart && /[a-z]/.test(currentPart[currentPart.length - 1])) {
+                // Capital letter after lowercase - start new part
+                parts.push(currentPart.toLowerCase());
+                currentPart = char;
+            } else {
+                currentPart += char;
+            }
+        }
+        
+        if (currentPart) {
+            parts.push(currentPart.toLowerCase());
+        }
+        
+        // Filter out empty parts
+        const cleanParts = parts.filter(p => p.length > 0);
+        
+        if (cleanParts.length === 0) {
+            return name; // Can't convert, return original
+        }
+        
+        switch (convention) {
+            case 'PascalCase':
+                // Capitalize first letter of each part
+                return cleanParts.map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('');
+            case 'camelCase':
+                // First part lowercase, rest capitalized
+                return cleanParts[0] + cleanParts.slice(1).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('');
+            case 'snake_case':
+                // All lowercase with underscores
+                return cleanParts.join('_');
+            default:
+                return name; // Unknown convention, return original
+        }
+    }
+
+    private checkFunctionToModifier(document: vscode.TextDocument, diagnostics: vscode.Diagnostic[]) {
+        const text = document.getText();
+        const lines = text.split('\n');
+        
+        // First, extract all user-defined functions
+        const userDefinedFunctions = this.extractUserDefinedFunctions(document);
+        
+        // Track how each function is used
+        const functionUsage: Map = new Map();
+        
+        // Initialize usage tracking
+        for (const funcName of userDefinedFunctions) {
+            functionUsage.set(funcName, { withPipe: 0, withoutPipe: 0, definitionLine: -1 });
+        }
+        
+        // Find function definitions and record their line numbers
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            const funMatch = line.match(/^\s*fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
+            if (funMatch) {
+                const funcName = funMatch[1];
+                if (functionUsage.has(funcName)) {
+                    functionUsage.get(funcName)!.definitionLine = i;
+                }
+            }
+        }
+        
+        // Check how functions are used
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            
+            // Skip comments
+            const commentIndex = Math.min(
+                line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+                line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+            );
+            const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+            
+            // Check for function calls: @.This.functionName( or @.ModuleName.functionName(
+            const functionCallPattern = /@\.([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
+            let match;
+            
+            while ((match = functionCallPattern.exec(codeOnlyLine)) !== null) {
+                const moduleName = match[1];
+                const funcName = match[2];
+                
+                // Only check user-defined functions (skip built-ins)
+                if (moduleName === 'This' && functionUsage.has(funcName)) {
+                    // Check if this call is preceded by a pipe operator
+                    // Look for | before @.This.functionName on the same line
+                    const callStart = match.index;
+                    const beforeCall = codeOnlyLine.substring(0, callStart);
+                    // Check if there's a pipe operator before the call (with optional whitespace)
+                    const hasPipe = /\|\s*$/.test(beforeCall.trim()) || /\|\s+@\.This\./.test(beforeCall);
+                    
+                    if (hasPipe) {
+                        functionUsage.get(funcName)!.withPipe++;
+                    } else {
+                        functionUsage.get(funcName)!.withoutPipe++;
+                    }
+                }
+            }
+        }
+        
+        // Check if any functions are always used with pipe
+        for (const [funcName, usage] of functionUsage.entries()) {
+            // Only suggest conversion if:
+            // 1. Function is used at least once with pipe
+            // 2. Function is never used without pipe (or used with pipe more than without)
+            // 3. We found the definition line
+            if (usage.withPipe > 0 && usage.withoutPipe === 0 && usage.definitionLine >= 0) {
+                const definitionLine = document.lineAt(usage.definitionLine);
+                const funMatch = definitionLine.text.match(/^\s*(fun)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
+                
+                if (funMatch) {
+                    const funKeywordStart = definitionLine.text.indexOf('fun');
+                    const range = new vscode.Range(
+                        usage.definitionLine,
+                        funKeywordStart,
+                        usage.definitionLine,
+                        funKeywordStart + 3
+                    );
+                    
+                    const diagnostic = new vscode.Diagnostic(
+                        range,
+                        `Function '${funcName}' is always used with pipe operator. Consider converting to modifier.`,
+                        vscode.DiagnosticSeverity.Hint
+                    );
+                    diagnostic.code = 'function-to-modifier';
+                    (diagnostic as any).functionName = funcName;
+                    (diagnostic as any).definitionLine = usage.definitionLine;
+                    diagnostics.push(diagnostic);
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks typed object literals ($var : Type = { ... }) for properties not declared in the schema.
+     */
+    private async checkTypedObjectExtraProperties(document: vscode.TextDocument, diagnostics: vscode.Diagnostic[]): Promise {
+        const text = document.getText();
+
+        // Find all { } pairs
+        const pairs: { start: number; end: number }[] = [];
+        const openBraces: number[] = [];
+        for (let i = 0; i < text.length; i++) {
+            const ch = text[i];
+            if (ch === '{') openBraces.push(i);
+            else if (ch === '}') {
+                if (openBraces.length > 0) {
+                    pairs.push({ start: openBraces.pop()!, end: i });
+                }
+            }
+        }
+
+        for (const { start, end } of pairs) {
+            const position = document.positionAt(start + 1);
+            const schemaAt = await this.typeManager!.getSchemaForObjectAt(document, position);
+            if (!schemaAt) continue;
+
+            const { schema } = schemaAt;
+            const content = text.substring(start + 1, end);
+            const propsWithRanges = this.getTopLevelPropertiesWithRanges(document, content, start + 1);
+
+            for (const { name, range } of propsWithRanges) {
+                if (!(name in schema.properties)) {
+                    diagnostics.push(new vscode.Diagnostic(
+                        range,
+                        `Property '${name}' does not seem declared on the schema.`,
+                        vscode.DiagnosticSeverity.Warning
+                    ));
+                }
+            }
+        }
+    }
+
+    /** Extracts top-level property names and their ranges from object content. */
+    private getTopLevelPropertiesWithRanges(
+        document: vscode.TextDocument,
+        content: string,
+        contentStartOffset: number
+    ): Array<{ name: string; range: vscode.Range }> {
+        const result: Array<{ name: string; range: vscode.Range }> = [];
+        let depth = 0;
+        let i = 0;
+        let inString = false;
+        let stringChar = '';
+
+        while (i < content.length) {
+            const c = content[i];
+            if (inString) {
+                if (c === '\\') {
+                    i += 2;
+                    continue;
+                }
+                if (c === stringChar) inString = false;
+                i++;
+                continue;
+            }
+            if (c === '"' || c === "'" || c === '`') {
+                inString = true;
+                stringChar = c;
+                i++;
+                continue;
+            }
+            if (c === '{' || c === '[' || c === '(') {
+                depth++;
+                i++;
+                continue;
+            }
+            if (c === '}' || c === ']' || c === ')') {
+                depth--;
+                i++;
+                continue;
+            }
+            if (depth === 0) {
+                const propMatch = content.slice(i).match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:/);
+                if (propMatch) {
+                    const name = propMatch[1];
+                    const nameStart = contentStartOffset + i;
+                    const nameEnd = nameStart + name.length;
+                    result.push({
+                        name,
+                        range: new vscode.Range(document.positionAt(nameStart), document.positionAt(nameEnd))
+                    });
+                    i += propMatch[0].length;
+                    continue;
+                }
+            }
+            i++;
+        }
+        return result;
+    }
+
+    private checkMathOutsideBraces(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Skip comments
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+
+        // Skip if line is empty or only whitespace
+        if (codeOnlyLine.trim() === '') {
+            return;
+        }
+
+        // Find all {{ ... }} blocks to exclude them
+        const mathBlockPattern = /\{\{([^}]+)\}\}/g;
+        const mathBlocks: Array<{ start: number; end: number }> = [];
+        let mathBlockMatch;
+        while ((mathBlockMatch = mathBlockPattern.exec(codeOnlyLine)) !== null) {
+            mathBlocks.push({
+                start: mathBlockMatch.index,
+                end: mathBlockMatch.index + mathBlockMatch[0].length
+            });
+        }
+
+        // Find all template strings (backtick strings) to exclude them (already handled by checkMathInTemplateString)
+        const backtickPattern = /`(?:[^`\\]|\\.)*`/g;
+        const templateStrings: Array<{ start: number; end: number }> = [];
+        let backtickMatch;
+        while ((backtickMatch = backtickPattern.exec(codeOnlyLine)) !== null) {
+            templateStrings.push({
+                start: backtickMatch.index,
+                end: backtickMatch.index + backtickMatch[0].length
+            });
+        }
+
+        // Combine exclusion blocks
+        const allExclusionBlocks = [...mathBlocks, ...templateStrings];
+
+        // Helper function to check if a position is inside exclusion blocks
+        const isInsideExclusionBlocks = (start: number, end: number): boolean => {
+            return allExclusionBlocks.some(block => start >= block.start && end <= block.end);
+        };
+
+        // Find math operations: $var operator (number|$var) or (number|$var) operator $var
+        // Math operators: +, -, *, /, %
+        // But exclude comparison operators: ==, !=, <, >, <=, >=
+        // Pattern: variable/number operator variable/number
+        const mathExpressionPattern = /((?:\$[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*|\d+(?:\.\d+)?))\s*([+\-*/%])\s*((?:\$[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*|\d+(?:\.\d+)?))/g;
+        let mathMatch;
+        
+        while ((mathMatch = mathExpressionPattern.exec(codeOnlyLine)) !== null) {
+            const matchStart = mathMatch.index;
+            const matchEnd = matchStart + mathMatch[0].length;
+            
+            // Skip if already inside an exclusion block
+            if (isInsideExclusionBlocks(matchStart, matchEnd)) {
+                continue;
+            }
+
+            // Check if this is part of a comparison operator (==, !=, <=, >=, <, >)
+            // Look at characters before and after the operator
+            const beforeOp = codeOnlyLine.substring(Math.max(0, matchStart - 1), matchStart);
+            const afterOp = codeOnlyLine.substring(matchEnd, Math.min(codeOnlyLine.length, matchEnd + 1));
+            const operator = mathMatch[2];
+            
+            // Skip if it's part of a comparison
+            if (operator === '=' && (beforeOp === '=' || beforeOp === '!' || afterOp === '=')) {
+                continue; // == or != or <= or >=
+            }
+            if (operator === '<' && afterOp === '=') {
+                continue; // <=
+            }
+            if (operator === '>' && afterOp === '=') {
+                continue; // >=
+            }
+            if (operator === '<' || operator === '>') {
+                // Could be comparison, but also could be math in some contexts
+                // Check if it's in a condition context (if, while, etc.)
+                const beforeMatch = codeOnlyLine.substring(0, matchStart);
+                if (beforeMatch.match(/\b(if|while|switch|filter|map)\s*\(/)) {
+                    continue; // Likely a comparison in a condition
+                }
+            }
+
+            // This is a math expression that needs {{ }} wrapping
+            const range = new vscode.Range(
+                lineNumber,
+                matchStart,
+                lineNumber,
+                matchEnd
+            );
+            
+            const diagnostic = new vscode.Diagnostic(
+                range,
+                'Math operations must be wrapped in {{ }}',
+                vscode.DiagnosticSeverity.Warning
+            );
+            diagnostic.code = 'math-outside-braces';
+            diagnostics.push(diagnostic);
+        }
+    }
+
+    private checkInconsistentSpacing(line: string, lineNumber: number, diagnostics: vscode.Diagnostic[], document: vscode.TextDocument) {
+        // Skip comments
+        const commentIndex = Math.min(
+            line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+            line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+        );
+        const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+
+        // Check for missing spaces around operators and pipes
+        // Pattern: $var:value (missing space after :)
+        // Pattern: $var|modifier (missing space before |)
+        // Pattern: $var| modifier (missing space after |)
+        
+        // Check for missing space after : in assignments
+        const colonPattern = /(\$[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*:\s*([^\s=:])/g;
+        let colonMatch;
+        while ((colonMatch = colonPattern.exec(codeOnlyLine)) !== null) {
+            const colonPos = codeOnlyLine.indexOf(':', colonMatch.index);
+            if (colonPos !== -1 && codeOnlyLine[colonPos + 1] !== ' ') {
+                const range = new vscode.Range(
+                    lineNumber,
+                    colonPos + 1,
+                    lineNumber,
+                    colonPos + 1
+                );
+                
+                const diagnostic = new vscode.Diagnostic(
+                    range,
+                    'Missing space after :',
+                    vscode.DiagnosticSeverity.Hint
+                );
+                diagnostic.code = 'inconsistent-spacing';
+                diagnostics.push(diagnostic);
+            }
+        }
+
+        // Check for missing spaces around pipe operator |
+        const pipePattern = /([^\s|])\s*\|([^\s|])/g;
+        let pipeMatch;
+        while ((pipeMatch = pipePattern.exec(codeOnlyLine)) !== null) {
+            const pipePos = codeOnlyLine.indexOf('|', pipeMatch.index);
+            if (pipePos !== -1) {
+                const beforePipe = codeOnlyLine[pipePos - 1];
+                const afterPipe = codeOnlyLine[pipePos + 1];
+                
+                if (beforePipe !== ' ' || afterPipe !== ' ') {
+                    const range = new vscode.Range(
+                        lineNumber,
+                        pipePos,
+                        lineNumber,
+                        pipePos + 1
+                    );
+                    
+                    const diagnostic = new vscode.Diagnostic(
+                        range,
+                        'Missing space around | operator',
+                        vscode.DiagnosticSeverity.Hint
+                    );
+                    diagnostic.code = 'inconsistent-spacing';
+                    diagnostics.push(diagnostic);
+                }
+            }
+        }
+    }
+
+    private checkForeachVariableScoping(document: vscode.TextDocument, diagnostics: vscode.Diagnostic[]) {
+        const text = document.getText();
+        const lines = text.split('\n');
+        
+        // Track foreach loops and their variables
+        const foreachLoops: Array<{ loopVar: string; startLine: number; endLine: number }> = [];
+        
+        // Find all foreach loops
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            const trimmed = line.trim();
+            
+            const foreachMatch = trimmed.match(/^foreach\s+\$([a-zA-Z_][a-zA-Z0-9_]*)\s+in\s+(.+)/);
+            if (foreachMatch) {
+                const loopVar = foreachMatch[1];
+                const foreachStartLine = i;
+                
+                // Find the matching endfor (track foreach/endfor nesting so inner loops don't close outer)
+                let foreachDepth = 1;
+                let foundEndfor = false;
+                let endforLine = -1;
+                
+                for (let j = i + 1; j < lines.length; j++) {
+                    const currentLine = lines[j];
+                    const currentTrimmed = currentLine.trim();
+                    
+                    const commentIndex = Math.min(
+                        currentLine.indexOf('//') !== -1 ? currentLine.indexOf('//') : Infinity,
+                        currentLine.indexOf('#') !== -1 ? currentLine.indexOf('#') : Infinity
+                    );
+                    const codeOnly = commentIndex !== Infinity ? currentLine.substring(0, commentIndex).trim() : currentTrimmed;
+                    
+                    if (/^foreach\s/.test(codeOnly)) {
+                        foreachDepth++;
+                    } else if (codeOnly === 'endfor') {
+                        foreachDepth--;
+                        if (foreachDepth === 0) {
+                            foundEndfor = true;
+                            endforLine = j;
+                            break;
+                        }
+                    }
+                }
+                
+                if (foundEndfor) {
+                    foreachLoops.push({
+                        loopVar,
+                        startLine: foreachStartLine,
+                        endLine: endforLine
+                    });
+                }
+            }
+        }
+        
+        // Check if loop variables are used after their loops
+        for (const loop of foreachLoops) {
+            for (let i = loop.endLine + 1; i < lines.length; i++) {
+                const line = lines[i];
+                
+                // Skip comments
+                const commentIndex = Math.min(
+                    line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+                    line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+                );
+                const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+                
+                // Check if loop variable is used
+                const varPattern = new RegExp(`\\$${loop.loopVar}(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*\\b`, 'g');
+                const varMatch = varPattern.exec(codeOnlyLine);
+                
+                if (varMatch) {
+                    const range = new vscode.Range(
+                        i,
+                        varMatch.index,
+                        i,
+                        varMatch.index + varMatch[0].length
+                    );
+                    
+                    const diagnostic = new vscode.Diagnostic(
+                        range,
+                        `Loop variable '$${loop.loopVar}' is used outside its loop scope. Consider using map() instead.`,
+                        vscode.DiagnosticSeverity.Warning
+                    );
+                    diagnostic.code = 'foreach-variable-scoping';
+                    (diagnostic as any).loopVar = loop.loopVar;
+                    (diagnostic as any).foreachStartLine = loop.startLine;
+                    diagnostics.push(diagnostic);
+                    break; // Only flag first occurrence
+                }
+            }
+        }
+    }
+
+    private checkTypeConversion(document: vscode.TextDocument, diagnostics: vscode.Diagnostic[]) {
+        const text = document.getText();
+        const lines = text.split('\n');
+        
+        // Patterns that suggest implicit type conversion
+        // $number: $stringValue (assigning string to number variable)
+        // $boolean: $stringValue (assigning string to boolean variable)
+        // etc.
+        
+        // This is a heuristic check - we look for variable names that suggest types
+        // and assignments from variables that might be different types
+        
+        const typeSuggestingNames = {
+            number: /\b(number|num|count|total|sum|price|amount|quantity|qty|index|id|size|length)\b/i,
+            boolean: /\b(boolean|bool|is|has|can|should|enabled|active|valid|flag)\b/i,
+            string: /\b(string|str|text|name|label|message|description|title|value)\b/i
+        };
+        
+        for (let i = 0; i < lines.length; i++) {
+            const line = lines[i];
+            
+            // Skip comments
+            const commentIndex = Math.min(
+                line.indexOf('//') !== -1 ? line.indexOf('//') : Infinity,
+                line.indexOf('#') !== -1 ? line.indexOf('#') : Infinity
+            );
+            const codeOnlyLine = commentIndex !== Infinity ? line.substring(0, commentIndex) : line;
+            
+            // Match variable assignments: $varName = $otherVar or $varName: $otherVar
+            const assignmentMatch = codeOnlyLine.match(/^\s*\$([a-zA-Z_][a-zA-Z0-9_]*)\s*[=:]\s*\$([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)/);
+            if (!assignmentMatch) {
+                continue;
+            }
+            
+            const leftVar = assignmentMatch[1];
+            const rightVar = assignmentMatch[2];
+            
+            // Check if left variable name suggests a type
+            let suggestedType: string | null = null;
+            for (const [type, pattern] of Object.entries(typeSuggestingNames)) {
+                if (pattern.test(leftVar)) {
+                    suggestedType = type;
+                    break;
+                }
+            }
+            
+            if (!suggestedType) {
+                continue;
+            }
+            
+            // Check if right variable name suggests a different type
+            let rightVarType: string | null = null;
+            for (const [type, pattern] of Object.entries(typeSuggestingNames)) {
+                if (pattern.test(rightVar)) {
+                    rightVarType = type;
+                    break;
+                }
+            }
+            
+            // If types don't match, suggest explicit conversion
+            if (rightVarType && rightVarType !== suggestedType) {
+                const rightVarStart = codeOnlyLine.indexOf('$' + rightVar);
+                const range = new vscode.Range(
+                    i,
+                    rightVarStart,
+                    i,
+                    rightVarStart + rightVar.length + 1 // Include the $
+                );
+                
+                const conversionModifier = `to.${suggestedType === 'number' ? 'number' : suggestedType === 'boolean' ? 'boolean' : 'string'}`;
+                const diagnostic = new vscode.Diagnostic(
+                    range,
+                    `Implicit type conversion detected. Consider using explicit conversion: | ${conversionModifier}`,
+                    vscode.DiagnosticSeverity.Hint
+                );
+                diagnostic.code = 'implicit-type-conversion';
+                (diagnostic as any).conversionModifier = conversionModifier;
+                (diagnostic as any).rightVar = '$' + rightVar;
+                diagnostics.push(diagnostic);
+            }
+        }
+    }
+}
+
diff --git a/plugin/syntaxes/isl-markdown-injection.tmLanguage.json b/plugin/syntaxes/isl-markdown-injection.tmLanguage.json
new file mode 100644
index 0000000..4405453
--- /dev/null
+++ b/plugin/syntaxes/isl-markdown-injection.tmLanguage.json
@@ -0,0 +1,33 @@
+{
+  "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
+  "scopeName": "markdown.isl.codeblock",
+  "injectionSelector": "L:text.html.markdown",
+  "patterns": [
+    { "include": "#isl-code-block" }
+  ],
+  "repository": {
+    "isl-code-block": {
+      "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(isl)(\\s+[^`~]*)?)$",
+      "name": "markup.fenced_code.block.markdown",
+      "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$",
+      "beginCaptures": {
+        "3": { "name": "punctuation.definition.markdown" },
+        "4": { "name": "fenced_code.block.language.markdown" },
+        "5": { "name": "fenced_code.block.language.attributes.markdown" }
+      },
+      "endCaptures": {
+        "3": { "name": "punctuation.definition.markdown" }
+      },
+      "patterns": [
+        {
+          "begin": "(^|\\G)(\\s*)(.*)",
+          "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)",
+          "contentName": "meta.embedded.block.isl",
+          "patterns": [
+            { "include": "source.isl" }
+          ]
+        }
+      ]
+    }
+  }
+}
diff --git a/plugin/syntaxes/isl.tmLanguage.json b/plugin/syntaxes/isl.tmLanguage.json
new file mode 100644
index 0000000..fdd5cd8
--- /dev/null
+++ b/plugin/syntaxes/isl.tmLanguage.json
@@ -0,0 +1,552 @@
+{
+  "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
+  "name": "ISL",
+  "scopeName": "source.isl",
+  "patterns": [
+    { "include": "#comments" },
+    { "include": "#imports" },
+    { "include": "#test-annotations" },
+    { "include": "#types" },
+    { "include": "#functions" },
+    { "include": "#keywords" },
+    { "include": "#strings" },
+    { "include": "#numbers" },
+    { "include": "#constants" },
+    { "include": "#variables" },
+    { "include": "#operators" },
+    { "include": "#function-calls" },
+    { "include": "#modifiers" },
+    { "include": "#punctuation" }
+  ],
+  "repository": {
+    "comments": {
+      "patterns": [
+        {
+          "name": "comment.line.double-slash.isl",
+          "match": "//.*$"
+        },
+        {
+          "name": "comment.line.number-sign.isl",
+          "match": "#.*$"
+        },
+        {
+          "name": "comment.block.isl",
+          "begin": "/\\*",
+          "end": "\\*/"
+        }
+      ]
+    },
+    "imports": {
+      "patterns": [
+        {
+          "match": "\\b(import)\\s+([A-Za-z_][A-Za-z0-9_]*)\\s+(from)\\s+(['\"])([^'\"]+)\\4",
+          "captures": {
+            "1": { "name": "keyword.control.import.isl" },
+            "2": { "name": "variable.other.readwrite.isl" },
+            "3": { "name": "keyword.control.from.isl" },
+            "4": { "name": "punctuation.definition.string.begin.isl" },
+            "5": { "name": "string.quoted.isl" }
+          }
+        }
+      ]
+    },
+    "test-annotations": {
+      "patterns": [
+        {
+          "name": "entity.name.annotation.test.isl",
+          "match": "@test\\s*\\([^)]*\\)"
+        },
+        {
+          "name": "entity.name.annotation.test.isl",
+          "match": "@test\\b"
+        },
+        {
+          "name": "entity.name.annotation.setup.isl",
+          "match": "@setup\\b"
+        }
+      ]
+    },
+    "types": {
+      "patterns": [
+        {
+          "match": "\\b(type)\\s+([A-Za-z_][A-Za-z0-9_]*)\\s+(as|from)",
+          "captures": {
+            "1": { "name": "keyword.other.type.isl" },
+            "2": { "name": "entity.name.type.isl" },
+            "3": { "name": "keyword.other.isl" }
+          }
+        },
+        {
+          "name": "support.type.isl",
+          "match": "\\b(string|number|integer|boolean|object|array|any|null|text|date|datetime|binary)\\b"
+        }
+      ]
+    },
+    "functions": {
+      "patterns": [
+        {
+          "match": "\\b(fun|modifier)\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(",
+          "captures": {
+            "1": { "name": "keyword.control.function.isl" },
+            "2": { "name": "entity.name.function.isl" }
+          }
+        },
+        {
+          "name": "keyword.control.cache.isl",
+          "match": "\\b(cache)\\b"
+        }
+      ]
+    },
+    "keywords": {
+      "patterns": [
+        { "include": "#block-if-statement" },
+        { "include": "#block-switch-statement" },
+        { "include": "#inline-if-expression" },
+        {
+          "name": "keyword.control.conditional.else.isl",
+          "match": "\\b(else)\\b"
+        },
+        {
+          "name": "keyword.control.conditional.end.isl",
+          "match": "\\b(endif|endswitch)\\b"
+        },
+        {
+          "name": "keyword.control.loop.isl",
+          "match": "\\b(foreach|endfor|while|endwhile|parallel)\\b"
+        },
+        {
+          "name": "keyword.control.flow.isl",
+          "match": "\\b(return|in)\\b"
+        },
+        {
+          "name": "keyword.other.isl",
+          "match": "\\b(as|from|type|import)\\b"
+        },
+        {
+          "name": "keyword.operator.logical.isl",
+          "match": "\\b(and|or|not)\\b"
+        },
+        {
+          "name": "keyword.operator.comparison.isl",
+          "match": "\\b(contains|startsWith|endsWith|matches|is)\\b"
+        },
+        {
+          "name": "keyword.other.modifier.isl",
+          "match": "\\b(filter|map)\\b"
+        }
+      ]
+    },
+    "strings": {
+      "patterns": [
+        {
+          "name": "string.quoted.backtick.isl",
+          "begin": "`",
+          "end": "`",
+          "patterns": [
+            {
+              "name": "constant.character.escape.isl",
+              "match": "\\\\."
+            },
+            {
+              "name": "meta.embedded.expression.isl",
+              "begin": "(\\$)(\\{)",
+              "end": "\\}",
+              "beginCaptures": {
+                "1": { "name": "punctuation.definition.template-expression.begin.isl keyword.operator.interpolation.isl" },
+                "2": { "name": "punctuation.definition.template-expression.begin.isl keyword.control.interpolation.bracket.isl" }
+              },
+              "endCaptures": {
+                "0": { "name": "punctuation.definition.template-expression.end.isl keyword.control.interpolation.bracket.isl" }
+              },
+              "patterns": [
+                { "include": "#variables" },
+                { "include": "#operators" },
+                { "include": "#function-calls" },
+                { "include": "#modifiers" },
+                { "include": "#numbers" },
+                { "include": "#constants" }
+              ]
+            },
+            {
+              "name": "meta.embedded.math.isl",
+              "contentName": "meta.math-expression.content.isl",
+              "begin": "(\\{)(\\{)",
+              "end": "(\\})(\\})",
+              "beginCaptures": {
+                "0": { "name": "keyword.operator.math.isl punctuation.definition.math-expression.begin.isl" },
+                "1": { "name": "keyword.operator.math.bracket.isl" },
+                "2": { "name": "keyword.operator.math.bracket.isl" }
+              },
+              "endCaptures": {
+                "0": { "name": "keyword.operator.math.isl punctuation.definition.math-expression.end.isl" },
+                "1": { "name": "keyword.operator.math.bracket.isl" },
+                "2": { "name": "keyword.operator.math.bracket.isl" }
+              },
+              "patterns": [
+                { "include": "#numbers" },
+                { "include": "#variables" },
+                { "include": "#math-operators" },
+                { "include": "#function-calls" }
+              ]
+            },
+            {
+              "name": "variable.other.interpolated.isl",
+              "match": "(\\$)([A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)*)",
+              "captures": {
+                "1": { "name": "punctuation.definition.variable.isl keyword.operator.interpolation.isl" },
+                "2": { "name": "variable.other.readwrite.isl" }
+              }
+            },
+            {
+              "name": "meta.function-call.interpolated.isl",
+              "match": "(@\\.)([A-Za-z_][A-Za-z0-9_.]*)(?:\\(([^)]*)\\))?",
+              "captures": {
+                "1": { "name": "punctuation.accessor.isl" },
+                "2": { "name": "entity.name.function.isl" }
+              }
+            }
+          ]
+        },
+        {
+          "name": "string.quoted.double.isl",
+          "begin": "\"",
+          "end": "\"",
+          "patterns": [
+            {
+              "name": "constant.character.escape.isl",
+              "match": "\\\\."
+            }
+          ]
+        },
+        {
+          "name": "string.quoted.single.isl",
+          "begin": "'",
+          "end": "'",
+          "patterns": [
+            {
+              "name": "constant.character.escape.isl",
+              "match": "\\\\."
+            }
+          ]
+        }
+      ]
+    },
+    "numbers": {
+      "patterns": [
+        {
+          "name": "constant.numeric.decimal.isl",
+          "match": "\\b([0-9]+\\.[0-9]+)\\b"
+        },
+        {
+          "name": "constant.numeric.integer.isl",
+          "match": "\\b([0-9]+)\\b"
+        }
+      ]
+    },
+    "constants": {
+      "patterns": [
+        {
+          "name": "constant.language.boolean.isl",
+          "match": "\\b(true|false)\\b"
+        },
+        {
+          "name": "constant.language.null.isl",
+          "match": "\\bnull\\b"
+        }
+      ]
+    },
+    "variables": {
+      "patterns": [
+        {
+          "name": "variable.other.readwrite.isl",
+          "match": "(\\$)([A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*|\\[[0-9]+\\]|\\[\\([^)]+\\)\\])*)",
+          "captures": {
+            "1": { "name": "punctuation.definition.variable.isl keyword.operator.variable.isl" },
+            "2": { "name": "variable.other.readwrite.isl" }
+          }
+        }
+      ]
+    },
+    "operators": {
+      "patterns": [
+        {
+          "name": "keyword.operator.comparison.isl",
+          "match": "==|!=|<=|>=|<|>|!contains|!startsWith|!endsWith|!in|!is|!matches"
+        },
+        {
+          "name": "keyword.operator.arithmetic.isl",
+          "match": "\\+|\\-|\\*|\\/"
+        },
+        {
+          "name": "keyword.operator.assignment.isl",
+          "match": "="
+        },
+        {
+          "name": "keyword.operator.logical.isl",
+          "match": "!|\\?"
+        },
+        {
+          "name": "keyword.operator.coalesce.isl",
+          "match": "\\?\\?"
+        },
+        {
+          "name": "keyword.operator.spread.isl",
+          "match": "\\.\\.\\."
+        },
+        {
+          "name": "keyword.operator.arrow.isl",
+          "match": "->"
+        }
+      ]
+    },
+    "math-operators": {
+      "patterns": [
+        {
+          "name": "keyword.operator.arithmetic.math.isl",
+          "match": "\\+|\\-|\\*|\\/|\\(|\\)"
+        }
+      ]
+    },
+    "function-calls": {
+      "patterns": [
+        {
+          "name": "meta.function-call.isl",
+          "match": "(@\\.)([A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)*)",
+          "captures": {
+            "1": { "name": "punctuation.accessor.isl keyword.operator.accessor.isl" },
+            "2": { "name": "entity.name.function.isl" }
+          }
+        },
+        {
+          "name": "entity.name.function.isl",
+          "match": "\\b(?!(if|switch|while|foreach)\\b)([A-Za-z_][A-Za-z0-9_]*)\\s*(?=\\()"
+        }
+      ]
+    },
+    "modifiers": {
+      "patterns": [
+        {
+          "name": "meta.modifier.namespace.isl",
+          "match": "(\\|)\\s*((to|date|Math|xml|csv|regex|encode|decode|crypto|join)(\\.)(\\w+))",
+          "captures": {
+            "1": { "name": "keyword.control.flow.pipe.isl" },
+            "2": { "name": "entity.name.function.isl" }
+          }
+        },
+        {
+          "name": "meta.modifier.isl",
+          "match": "(\\|)\\s*([A-Za-z_][A-Za-z0-9_]*)",
+          "captures": {
+            "1": { "name": "keyword.control.flow.pipe.isl" },
+            "2": { "name": "entity.name.function.isl" }
+          }
+        }
+      ]
+    },
+    "punctuation": {
+      "patterns": [
+        {
+          "name": "punctuation.separator.isl",
+          "match": "[,;:]"
+        },
+        {
+          "name": "punctuation.section.brackets.isl",
+          "match": "[\\[\\]\\(\\)\\{\\}]"
+        }
+      ]
+    },
+    "inline-if-expression": {
+      "patterns": [
+        {
+          "name": "meta.inline-if-expression.isl",
+          "begin": "\\b(if)\\s*(\\()",
+          "end": "(;)|(?=\\n)|(?=,)",
+          "beginCaptures": {
+            "1": { "name": "keyword.control.conditional.if.isl" },
+            "2": { "name": "punctuation.definition.condition.begin.isl" }
+          },
+          "endCaptures": {
+            "1": { "name": "punctuation.terminator.statement.isl" }
+          },
+          "patterns": [
+            {
+              "name": "meta.condition.isl",
+              "begin": "\\G",
+              "end": "\\)",
+              "endCaptures": {
+                "0": { "name": "punctuation.definition.condition.end.isl" }
+              },
+              "patterns": [
+                { "include": "#variables" },
+                { "include": "#operators" },
+                { "include": "#function-calls" },
+                { "include": "#constants" },
+                { "include": "#numbers" },
+                { "include": "#strings" },
+                {
+                  "name": "keyword.operator.logical.isl",
+                  "match": "\\b(and|or|not)\\b"
+                },
+                {
+                  "name": "keyword.operator.comparison.isl",
+                  "match": "\\b(contains|startsWith|endsWith|matches|is)\\b"
+                }
+              ]
+            },
+            {
+              "name": "keyword.control.conditional.else.isl",
+              "match": "\\b(else)\\b"
+            },
+            {
+              "name": "keyword.control.conditional.endif.isl",
+              "match": "\\b(endif)\\b"
+            },
+            { "include": "#strings" },
+            { "include": "#numbers" },
+            { "include": "#constants" },
+            { "include": "#variables" },
+            { "include": "#function-calls" },
+            { "include": "#modifiers" },
+            { "include": "#operators" }
+          ]
+        }
+      ]
+    },
+    "block-if-statement": {
+      "patterns": [
+        {
+          "name": "meta.block-if-statement.isl",
+          "begin": "\\b(if)\\s*(\\()",
+          "end": "\\b(endif)\\b",
+          "beginCaptures": {
+            "1": { "name": "keyword.control.conditional.if.isl" },
+            "2": { "name": "punctuation.definition.condition.begin.isl" }
+          },
+          "endCaptures": {
+            "1": { "name": "keyword.control.conditional.endif.isl" }
+          },
+          "patterns": [
+            {
+              "name": "meta.condition.isl",
+              "begin": "\\G",
+              "end": "\\)",
+              "endCaptures": {
+                "0": { "name": "punctuation.definition.condition.end.isl" }
+              },
+              "patterns": [
+                { "include": "#variables" },
+                { "include": "#operators" },
+                { "include": "#function-calls" },
+                { "include": "#constants" },
+                { "include": "#numbers" },
+                { "include": "#strings" },
+                {
+                  "name": "keyword.operator.logical.isl",
+                  "match": "\\b(and|or|not)\\b"
+                },
+                {
+                  "name": "keyword.operator.comparison.isl",
+                  "match": "\\b(contains|startsWith|endsWith|matches|is)\\b"
+                }
+              ]
+            },
+            {
+              "name": "keyword.control.conditional.else.isl",
+              "match": "\\b(else)\\b"
+            },
+            { "include": "#block-if-statement" },
+            { "include": "#block-switch-statement" },
+            {
+              "include": "#comments"
+            },
+            {
+              "include": "#strings"
+            },
+            {
+              "include": "#numbers"
+            },
+            {
+              "include": "#constants"
+            },
+            {
+              "include": "#variables"
+            },
+            {
+              "include": "#operators"
+            },
+            {
+              "include": "#function-calls"
+            },
+            {
+              "include": "#modifiers"
+            }
+          ]
+        }
+      ]
+    },
+    "block-switch-statement": {
+      "patterns": [
+        {
+          "name": "meta.block-switch-statement.isl",
+          "begin": "\\b(switch)\\s*(\\()",
+          "end": "\\b(endswitch)\\b",
+          "beginCaptures": {
+            "1": { "name": "keyword.control.conditional.switch.isl" },
+            "2": { "name": "punctuation.definition.condition.begin.isl" }
+          },
+          "endCaptures": {
+            "1": { "name": "keyword.control.conditional.endswitch.isl" }
+          },
+          "patterns": [
+            {
+              "name": "meta.condition.isl",
+              "begin": "\\G",
+              "end": "\\)",
+              "endCaptures": {
+                "0": { "name": "punctuation.definition.condition.end.isl" }
+              },
+              "patterns": [
+                { "include": "#variables" },
+                { "include": "#operators" },
+                { "include": "#function-calls" },
+                { "include": "#constants" },
+                { "include": "#numbers" },
+                { "include": "#strings" }
+              ]
+            },
+            {
+              "name": "keyword.control.conditional.else.isl",
+              "match": "\\b(else)\\b"
+            },
+            { "include": "#block-if-statement" },
+            { "include": "#block-switch-statement" },
+            {
+              "include": "#comments"
+            },
+            {
+              "include": "#strings"
+            },
+            {
+              "include": "#numbers"
+            },
+            {
+              "include": "#constants"
+            },
+            {
+              "include": "#variables"
+            },
+            {
+              "include": "#operators"
+            },
+            {
+              "include": "#function-calls"
+            },
+            {
+              "include": "#modifiers"
+            }
+          ]
+        }
+      ]
+    }
+  }
+}
diff --git a/plugin/tests/customer.isl b/plugin/tests/customer.isl
new file mode 100644
index 0000000..902985b
--- /dev/null
+++ b/plugin/tests/customer.isl
@@ -0,0 +1,3 @@
+fun transformCustomer() {
+    return { id: 123, name: "George" }
+}
diff --git a/plugin/tests/out.json b/plugin/tests/out.json
new file mode 100644
index 0000000..3511e99
--- /dev/null
+++ b/plugin/tests/out.json
@@ -0,0 +1,19 @@
+{
+  "passed" : 0,
+  "failed" : 1,
+  "total" : 1,
+  "success" : false,
+  "results" : [ {
+    "testFile" : "sample.isl",
+    "functionName" : "compilation",
+    "testName" : "compilation",
+    "testGroup" : "sample.isl",
+    "success" : false,
+    "message" : "Failed parsing `=` > extraneous input '=' expecting {'if', 'switch', 'filter', 'map', 'in', 'matches', 'parallel', 'foreach', 'while', 'return', 'import', 'type', 'as', 'from', OPEN_BACKTICK, LOP, '$', '@', '[', '{', CURLYOPENOPEN, BOOL, NUM, ID, QUOTEDSTRING}\n    $val:= { }  // compilation error\n         ^\nat Position(file=sample.isl, line=29, column=9, endLine=null, endColumn=null).",
+    "errorPosition" : {
+      "file" : "sample.isl",
+      "line" : 29,
+      "column" : 9
+    }
+  } ]
+}
\ No newline at end of file
diff --git a/plugin/tests/tests/sample.isl b/plugin/tests/tests/sample.isl
new file mode 100644
index 0000000..54437a9
--- /dev/null
+++ b/plugin/tests/tests/sample.isl
@@ -0,0 +1,45 @@
+import Customer from "../customer.isl";
+
+// Sample ISL test file - tests are discovered by the Test Explorer
+
+@setup
+fun setup() {
+    $x: 1;  // No-op; setup runs before each test
+}
+
+@test
+fun test_simpleAssertion() {
+    $value: 42;
+    @.Assert.equal(42, $value, "Values should match");
+}
+
+@test
+fun test_customer() {
+    $c = @.Customer.transformCustomer();
+    @.Assert.equal(
+        {
+            id: 123, name: "George",
+        }, $c);
+}
+
+@test("Custom test name")
+fun test_withCustomName() {
+    $value: 2;
+    @.Assert.equal(2, $value);
+}
+
+@test({ name: "Grouped test", group: "math" })
+fun test_grouped() {
+    $value: 30;
+    @.Assert.equal(30, $value);
+}
+
+//@test("Failing test for error rendering")
+//fun test_failsOnPurpose() {
+//    $val:= { }  // compilation error
+//}
+
+@test("Failing test for error rendering")
+fun test_failsOnPurpose() {
+    @.Assert.equal(1, 2, "Expected 1 to equal 2 - this test fails on purpose");
+}
diff --git a/plugin/tsconfig.json b/plugin/tsconfig.json
new file mode 100644
index 0000000..80f178e
--- /dev/null
+++ b/plugin/tsconfig.json
@@ -0,0 +1,18 @@
+{
+  "compilerOptions": {
+    "module": "commonjs",
+    "target": "ES2020",
+    "lib": ["ES2020"],
+    "outDir": "out",
+    "rootDir": "src",
+    "sourceMap": true,
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "resolveJsonModule": true
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules", ".vscode-test"]
+}
+
diff --git a/settings.gradle.kts b/settings.gradle.kts
index e0970f3..94a3209 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -3,3 +3,5 @@ rootProject.name = "isl-pom"
 include("isl-transform")
 include("isl-validation")
 include("isl-cmd")
+include("plugin:build-isl-runtime")
+include("isl-test")