diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 3c06cfd..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": ["./eslint.js"], - "env": { - "node": true, - "browser": true - }, - "rules": { - "no-constant-condition": "off" - } -} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..a4df793 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,56 @@ +name: Linter & Types + +on: + pull_request: + branches: + - main +jobs: + lint: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install pnpm + uses: pnpm/action-setup@v3 + with: + version: 9 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v3 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install Node.js dependencies + run: pnpm install + + - name: Prettier + run: pnpm prettier:check + + - name: Node.js Lint + run: pnpm lint + + - name: TypeScript + run: pnpm tc + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..aec8d47 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,50 @@ +name: Tests + +on: + pull_request: + branches: + - main +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install pnpm + uses: pnpm/action-setup@v3 + with: + version: 9 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v3 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install Node.js dependencies + run: pnpm install + + - name: Node.js Test + env: + NODE_ENV: test + run: pnpm test diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..80df6d1 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +.* +dist +build +**/tests/fixtures diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..d0895a2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "jsxSingleQuote": true, + "singleQuote": true, + "trailingComma": "all" +} diff --git a/README.md b/README.md index 61e08f6..291ddf6 100644 --- a/README.md +++ b/README.md @@ -50,4 +50,4 @@ This is just a small example of what PromptL can do. It is a powerful tool that ## Links -[Website](https://promptl.ai/) | [Documentation](https://docs.latitude.so/promptl/getting-started/introduction) \ No newline at end of file +[Website](https://promptl.ai/) | [Documentation](https://docs.latitude.so/promptl/getting-started/introduction) diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..3f397e1 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,63 @@ +import typescriptEslintEslintPlugin from '@typescript-eslint/eslint-plugin' +import prettier from 'eslint-plugin-prettier' +import globals from 'globals' +import tsParser from '@typescript-eslint/parser' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import js from '@eslint/js' +import { FlatCompat } from '@eslint/eslintrc' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}) + +export default [ + { + ignores: ['**/.*.js', '**/node_modules/', '**/dist/'], + }, + ...compat.extends('eslint:recommended'), + { + plugins: { + '@typescript-eslint': typescriptEslintEslintPlugin, + 'prettier': prettier, + }, + + languageOptions: { + globals: { + ...globals.node, + ...globals.browser, + }, + + parser: tsParser, + }, + + settings: { + 'import/resolver': { + typescript: { + project: './tsconfig.json', + }, + }, + }, + + rules: { + 'no-constant-condition': 'off', + 'no-unused-vars': 'off', + + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'all', + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + }, + }, + { + files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'], + }, +] diff --git a/eslint.js b/eslint.js deleted file mode 100644 index 53e546b..0000000 --- a/eslint.js +++ /dev/null @@ -1,39 +0,0 @@ -const { resolve } = require('node:path') - -const project = resolve(process.cwd(), 'tsconfig.json') - -/** @type {import("eslint").Linter.Config} */ -module.exports = { - extends: ['eslint:recommended', 'prettier', 'eslint-config-turbo'], - plugins: ['@typescript-eslint/eslint-plugin'], - parser: '@typescript-eslint/parser', - settings: { - 'import/resolver': { - typescript: { - project, - }, - }, - }, - ignorePatterns: [ - // Ignore dotfiles - '.*.js', - 'node_modules/', - 'dist/', - ], - overrides: [ - { - files: ['*.js?(x)', '*.ts?(x)'], - }, - ], - rules: { - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - args: 'all', - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - }, - ], - }, -} diff --git a/package.json b/package.json index 0852f15..2c5b21b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "promptl-ai", - "version": "0.3.5", + "version": "0.4.5", "author": "Latitude Data", "license": "MIT", "description": "Compiler for PromptL, the prompt language", @@ -28,6 +28,7 @@ "test": "vitest run", "test:watch": "vitest", "prettier": "prettier --write src/**/*.ts", + "prettier:check": "prettier --check src/**/*.ts --ignore-path .prettierrcignore", "lint": "eslint src", "tc": "tsc --noEmit" }, @@ -39,10 +40,17 @@ "zod": "^3.23.8" }, "devDependencies": { + "eslint": "^9.17.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.17.0", "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-typescript": "^11.1.6", "@types/estree": "^1.0.1", "@types/node": "^20.12.12", + "@typescript-eslint/eslint-plugin": "^8.19.0", + "eslint-plugin-prettier": "^5.2.1", + "globals": "^15.14.0", + "prettier": "^3.4.2", "rollup": "^4.10.0", "rollup-plugin-dts": "^6.1.1", "tslib": "^2.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b651cc..cb43c31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,12 @@ importers: specifier: ^3.23.8 version: 3.23.8 devDependencies: + '@eslint/eslintrc': + specifier: ^3.2.0 + version: 3.2.0 + '@eslint/js': + specifier: ^9.17.0 + version: 9.17.0 '@rollup/plugin-alias': specifier: ^5.1.0 version: 5.1.1(rollup@4.27.4) @@ -36,6 +42,21 @@ importers: '@types/node': specifier: ^20.12.12 version: 20.17.8 + '@typescript-eslint/eslint-plugin': + specifier: ^8.19.0 + version: 8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.17.0)(typescript@5.7.2))(eslint@9.17.0)(typescript@5.7.2) + eslint: + specifier: ^9.17.0 + version: 9.17.0 + eslint-plugin-prettier: + specifier: ^5.2.1 + version: 5.2.1(eslint@9.17.0)(prettier@3.4.2) + globals: + specifier: ^15.14.0 + version: 15.14.0 + prettier: + specifier: ^3.4.2 + version: 3.4.2 rollup: specifier: ^4.10.0 version: 4.27.4 @@ -347,6 +368,60 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.19.1': + resolution: {integrity: sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.9.1': + resolution: {integrity: sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.2.0': + resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.17.0': + resolution: {integrity: sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.5': + resolution: {integrity: sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.4': + resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.1': + resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} + engines: {node: '>=18.18'} + '@jest/schemas@29.6.3': resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -354,6 +429,22 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgr/core@0.1.1': + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@rollup/plugin-alias@5.1.1': resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} engines: {node: '>=14.0.0'} @@ -481,9 +572,59 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@20.17.8': resolution: {integrity: sha512-ahz2g6/oqbKalW9sPv6L2iRbhLnojxjYWspAqhjvqSWBgGebEJT5GvRmk0QXPj3sbC6rU0GTQjPLQkmR8CObvA==} + '@typescript-eslint/eslint-plugin@8.19.0': + resolution: {integrity: sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/parser@8.19.0': + resolution: {integrity: sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/scope-manager@8.19.0': + resolution: {integrity: sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.19.0': + resolution: {integrity: sha512-TZs0I0OSbd5Aza4qAMpp1cdCYVnER94IziudE3JU328YUHgWu9gwiwhag+fuLeJ2LkWLXI+F/182TbG+JaBdTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/types@8.19.0': + resolution: {integrity: sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.19.0': + resolution: {integrity: sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/utils@8.19.0': + resolution: {integrity: sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/visitor-keys@8.19.0': + resolution: {integrity: sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/expect@1.6.0': resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} @@ -499,6 +640,11 @@ packages: '@vitest/utils@1.6.0': resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.4: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} @@ -508,27 +654,68 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + chai@4.5.0: resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} engines: {node: '>=4'} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} code-red@1.0.4: resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -549,6 +736,9 @@ packages: resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} engines: {node: '>=6'} + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -563,16 +753,114 @@ packages: engines: {node: '>=18'} hasBin: true + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-prettier@5.2.1: + resolution: {integrity: sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.17.0: + resolution: {integrity: sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.18.0: + resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.2: + resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -591,6 +879,29 @@ packages: get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.14.0: + resolution: {integrity: sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==} + engines: {node: '>=18'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -599,10 +910,34 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + is-core-module@2.15.1: resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} engines: {node: '>= 0.4'} + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} @@ -619,6 +954,26 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + local-pkg@0.5.1: resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} engines: {node: '>=14'} @@ -626,6 +981,13 @@ packages: locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} @@ -635,10 +997,25 @@ packages: merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + mlly@1.7.3: resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} @@ -650,6 +1027,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -658,10 +1038,30 @@ packages: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + p-limit@5.0.0: resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} engines: {node: '>=18'} + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -685,6 +1085,10 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + picomatch@4.0.2: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} @@ -696,13 +1100,37 @@ packages: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + engines: {node: '>=14'} + hasBin: true + pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -710,6 +1138,10 @@ packages: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rollup-plugin-dts@6.1.1: resolution: {integrity: sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA==} engines: {node: '>=16'} @@ -722,6 +1154,14 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -751,13 +1191,25 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + strip-literal@2.1.1: resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + synckit@0.9.2: + resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} + engines: {node: ^14.18.0 || >=16.0.0} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -769,6 +1221,16 @@ packages: resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -777,6 +1239,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + type-detect@4.1.0: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} @@ -792,6 +1258,9 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + vite-node@1.6.0: resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -863,11 +1332,19 @@ packages: engines: {node: '>=8'} hasBin: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + yaml@2.6.1: resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} engines: {node: '>= 14'} hasBin: true + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + yocto-queue@1.1.1: resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} engines: {node: '>=12.20'} @@ -1028,12 +1505,80 @@ snapshots: '@esbuild/win32-x64@0.23.1': optional: true + '@eslint-community/eslint-utils@4.4.1(eslint@9.17.0)': + dependencies: + eslint: 9.17.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.19.1': + dependencies: + '@eslint/object-schema': 2.1.5 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/core@0.9.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.2.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.17.0': {} + + '@eslint/object-schema@2.1.5': {} + + '@eslint/plugin-kit@0.2.4': + dependencies: + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.1': {} + '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.8 '@jridgewell/sourcemap-codec@1.5.0': {} + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.18.0 + + '@pkgr/core@0.1.1': {} + '@rollup/plugin-alias@5.1.1(rollup@4.27.4)': optionalDependencies: rollup: 4.27.4 @@ -1113,10 +1658,89 @@ snapshots: '@types/estree@1.0.6': {} + '@types/json-schema@7.0.15': {} + '@types/node@20.17.8': dependencies: undici-types: 6.19.8 + '@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.17.0)(typescript@5.7.2))(eslint@9.17.0)(typescript@5.7.2)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.19.0(eslint@9.17.0)(typescript@5.7.2) + '@typescript-eslint/scope-manager': 8.19.0 + '@typescript-eslint/type-utils': 8.19.0(eslint@9.17.0)(typescript@5.7.2) + '@typescript-eslint/utils': 8.19.0(eslint@9.17.0)(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.19.0 + eslint: 9.17.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.3(typescript@5.7.2) + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.19.0(eslint@9.17.0)(typescript@5.7.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.19.0 + '@typescript-eslint/types': 8.19.0 + '@typescript-eslint/typescript-estree': 8.19.0(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.19.0 + debug: 4.3.7 + eslint: 9.17.0 + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.19.0': + dependencies: + '@typescript-eslint/types': 8.19.0 + '@typescript-eslint/visitor-keys': 8.19.0 + + '@typescript-eslint/type-utils@8.19.0(eslint@9.17.0)(typescript@5.7.2)': + dependencies: + '@typescript-eslint/typescript-estree': 8.19.0(typescript@5.7.2) + '@typescript-eslint/utils': 8.19.0(eslint@9.17.0)(typescript@5.7.2) + debug: 4.3.7 + eslint: 9.17.0 + ts-api-utils: 1.4.3(typescript@5.7.2) + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.19.0': {} + + '@typescript-eslint/typescript-estree@8.19.0(typescript@5.7.2)': + dependencies: + '@typescript-eslint/types': 8.19.0 + '@typescript-eslint/visitor-keys': 8.19.0 + debug: 4.3.7 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.4.3(typescript@5.7.2) + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.19.0(eslint@9.17.0)(typescript@5.7.2)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0) + '@typescript-eslint/scope-manager': 8.19.0 + '@typescript-eslint/types': 8.19.0 + '@typescript-eslint/typescript-estree': 8.19.0(typescript@5.7.2) + eslint: 9.17.0 + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.19.0': + dependencies: + '@typescript-eslint/types': 8.19.0 + eslint-visitor-keys: 4.2.0 + '@vitest/expect@1.6.0': dependencies: '@vitest/spy': 1.6.0 @@ -1146,18 +1770,52 @@ snapshots: loupe: 2.3.7 pretty-format: 29.7.0 + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + acorn-walk@8.3.4: dependencies: acorn: 8.14.0 acorn@8.14.0: {} + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + ansi-styles@5.2.0: {} + argparse@2.0.1: {} + assertion-error@1.1.0: {} + balanced-match@1.0.2: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + cac@6.7.14: {} + callsites@3.1.0: {} + chai@4.5.0: dependencies: assertion-error: 1.1.0 @@ -1168,6 +1826,11 @@ snapshots: pathval: 1.1.1 type-detect: 4.1.0 + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + check-error@1.0.3: dependencies: get-func-name: 2.0.2 @@ -1180,6 +1843,14 @@ snapshots: estree-walker: 3.0.3 periscopic: 3.1.0 + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + confbox@0.1.8: {} cross-spawn@7.0.6: @@ -1196,6 +1867,8 @@ snapshots: dependencies: type-detect: 4.1.0 + deep-is@0.1.4: {} + diff-sequences@29.6.3: {} esbuild@0.21.5: @@ -1251,12 +1924,87 @@ snapshots: '@esbuild/win32-ia32': 0.23.1 '@esbuild/win32-x64': 0.23.1 + escape-string-regexp@4.0.0: {} + + eslint-plugin-prettier@5.2.1(eslint@9.17.0)(prettier@3.4.2): + dependencies: + eslint: 9.17.0 + prettier: 3.4.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.9.2 + + eslint-scope@8.2.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.17.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.19.1 + '@eslint/core': 0.9.1 + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.17.0 + '@eslint/plugin-kit': 0.2.4 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.1 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.3.7 + escape-string-regexp: 4.0.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.3.0: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + estree-walker@2.0.2: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.6 + esutils@2.0.3: {} + execa@8.0.1: dependencies: cross-spawn: 7.0.6 @@ -1269,6 +2017,46 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.18.0: + dependencies: + reusify: 1.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.2 + keyv: 4.5.4 + + flatted@3.3.2: {} + fsevents@2.3.3: optional: true @@ -1282,16 +2070,49 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@15.14.0: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + hasown@2.0.2: dependencies: function-bind: 1.1.2 human-signals@5.0.0: {} + ignore@5.3.2: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + is-core-module@2.15.1: dependencies: hasown: 2.0.2 + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + is-reference@3.0.3: dependencies: '@types/estree': 1.0.6 @@ -1305,6 +2126,25 @@ snapshots: js-tokens@9.0.1: {} + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + local-pkg@0.5.1: dependencies: mlly: 1.7.3 @@ -1312,6 +2152,12 @@ snapshots: locate-character@3.0.0: {} + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + loupe@2.3.7: dependencies: get-func-name: 2.0.2 @@ -1322,8 +2168,23 @@ snapshots: merge-stream@2.0.0: {} + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + mimic-fn@4.0.0: {} + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + mlly@1.7.3: dependencies: acorn: 8.14.0 @@ -1335,6 +2196,8 @@ snapshots: nanoid@3.3.8: {} + natural-compare@1.4.0: {} + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -1343,10 +2206,33 @@ snapshots: dependencies: mimic-fn: 4.0.0 + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + p-limit@5.0.0: dependencies: yocto-queue: 1.1.1 + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + path-key@3.1.1: {} path-key@4.0.0: {} @@ -1365,6 +2251,8 @@ snapshots: picocolors@1.1.1: {} + picomatch@2.3.1: {} + picomatch@4.0.2: {} pkg-types@1.2.1: @@ -1379,14 +2267,28 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.4.2: {} + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 react-is: 18.3.1 + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + react-is@18.3.1: {} + resolve-from@4.0.0: {} + resolve-pkg-maps@1.0.0: {} resolve@1.22.8: @@ -1395,6 +2297,8 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + reusify@1.0.4: {} + rollup-plugin-dts@6.1.1(rollup@4.27.4)(typescript@5.7.2): dependencies: magic-string: 0.30.14 @@ -1427,6 +2331,12 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.27.4 fsevents: 2.3.3 + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + semver@7.6.3: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -1445,18 +2355,37 @@ snapshots: strip-final-newline@3.0.0: {} + strip-json-comments@3.1.1: {} + strip-literal@2.1.1: dependencies: js-tokens: 9.0.1 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + synckit@0.9.2: + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.8.1 + tinybench@2.9.0: {} tinypool@0.8.4: {} tinyspy@2.2.1: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@1.4.3(typescript@5.7.2): + dependencies: + typescript: 5.7.2 + tslib@2.8.1: {} tsx@4.19.2: @@ -1466,6 +2395,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + type-detect@4.1.0: {} typescript@5.7.2: {} @@ -1474,6 +2407,10 @@ snapshots: undici-types@6.19.8: {} + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + vite-node@1.6.0(@types/node@20.17.8): dependencies: cac: 6.7.14 @@ -1544,8 +2481,12 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + word-wrap@1.2.5: {} + yaml@2.6.1: {} + yocto-queue@0.1.0: {} + yocto-queue@1.1.1: {} zod@3.23.8: {} diff --git a/src/compiler/base/nodes/tags/content.ts b/src/compiler/base/nodes/tags/content.ts index 6c2fcbd..b296d59 100644 --- a/src/compiler/base/nodes/tags/content.ts +++ b/src/compiler/base/nodes/tags/content.ts @@ -107,7 +107,7 @@ export async function compile( if (toolArguments && typeof toolArguments === 'string') { try { rest['arguments'] = JSON.parse(toolArguments) - } catch (e) { + } catch { baseNodeError(errors.invalidToolCallArguments, node) } } diff --git a/src/compiler/chain-serialize.test.ts b/src/compiler/chain-serialize.test.ts new file mode 100644 index 0000000..da77470 --- /dev/null +++ b/src/compiler/chain-serialize.test.ts @@ -0,0 +1,134 @@ +import { getExpectedError } from '$promptl/test/helpers' +import { describe, expect, it } from 'vitest' + +import { Chain } from './chain' +import { removeCommonIndent } from './utils' +import { Adapters } from '$promptl/providers' +import { MessageRole } from '$promptl/types' + +describe('serialize chain', async () => { + it('fails when trying to serialize without running step', async () => { + const prompt = removeCommonIndent(` + + Before step + + + After step + + `) + + const chain = new Chain({ prompt, adapter: Adapters.default }) + + const action = () => chain.serialize() + const error = await getExpectedError(action, Error) + expect(error.message).toBe( + 'The chain has not started yet. You must call `step` at least once before calling `serialize`.', + ) + }) + + it('serialize with single step', async () => { + const prompt = removeCommonIndent(` + --- + provider: OpenAI_PATATA + model: gpt-4 + --- + {{ foo = 'foo' }} + A message + `) + + const chain = new Chain({ + prompt: removeCommonIndent(prompt), + parameters: {}, + adapter: Adapters.default, + }) + await chain.step() + const serialized = chain.serialize() + expect(serialized).toEqual({ + rawText: prompt, + scope: { + pointers: { foo: 0 }, + stash: ['foo'], + }, + adapterType: 'default', + compilerOptions: {}, + globalConfig: { + provider: 'OpenAI_PATATA', + model: 'gpt-4', + }, + ast: expect.any(Object), + globalMessages: [ + { + role: 'system', + content: [{ type: 'text', text: 'A message' }], + }, + ], + }) + }) + + it('serialize 2 steps', async () => { + const prompt = removeCommonIndent(` + + {{foo = 5}} + + + {{foo += 1}} + + + {{foo}} + + `) + + const chain = new Chain({ prompt, adapter: Adapters.openai }) + + await chain.step() + await chain.step('First step response') + const serialized = chain.serialize() + expect(serialized).toEqual({ + rawText: prompt, + scope: { + pointers: { foo: 0 }, + stash: [6], + }, + adapterType: 'openai', + compilerOptions: { includeSourceMap: false }, + globalConfig: undefined, + ast: expect.any(Object), + globalMessages: [ + { + role: 'assistant', + content: [{ type: 'text', text: 'First step response' }], + }, + ], + }) + }) + + it('serialize parameters', async () => { + const prompt = removeCommonIndent(` + Hello {{name}} + `) + + const chain = new Chain({ + prompt, + parameters: { name: 'Paco' }, + adapter: Adapters.default, + defaultRole: MessageRole.user, + }) + + await chain.step() + const serialized = chain.serialize() + expect(serialized).toEqual({ + rawText: prompt, + adapterType: 'default', + scope: { pointers: { name: 0 }, stash: ['Paco'] }, + compilerOptions: { defaultRole: 'user' }, + ast: expect.any(Object), + globalConfig: undefined, + globalMessages: [ + { + role: 'user', + content: [{ type: 'text', text: 'Hello Paco' }], + }, + ], + }) + }) +}) diff --git a/src/compiler/chain.test.ts b/src/compiler/chain.test.ts index 3a07019..f8fef30 100644 --- a/src/compiler/chain.test.ts +++ b/src/compiler/chain.test.ts @@ -36,7 +36,7 @@ describe('chain', async () => { const chain = new Chain({ prompt: removeCommonIndent(prompt), parameters: {}, - adapter: Adapters.default + adapter: Adapters.default, }) const { steps, messages } = await complete({ chain }) expect(steps).toBe(1) @@ -173,9 +173,7 @@ describe('chain', async () => { const chain = new Chain({ prompt, adapter: Adapters.default }) const { steps, messages } = await complete({ chain }) expect(steps).toBe(3) - const stepMessages = messages.filter( - (m) => m.role === MessageRole.system, - ) + const stepMessages = messages.filter((m) => m.role === MessageRole.system) expect(stepMessages.length).toBe(3) expect((stepMessages[0]!.content[0] as TextContent).text).toBe('Step 1') expect((stepMessages[1]!.content[0] as TextContent).text).toBe('Step 3') @@ -199,9 +197,7 @@ describe('chain', async () => { const { steps, messages } = await complete({ chain }) expect(steps).toBe(1) expect(messages.length).toBe(2) - expect((messages[0]!.content[0] as TextContent).text).toBe( - 'Step 1', - ) + expect((messages[0]!.content[0] as TextContent).text).toBe('Step 1') }) it('isolated steps do not include previous messages', async () => { @@ -268,7 +264,7 @@ describe('chain', async () => { func1, func2, }, - adapter: Adapters.default + adapter: Adapters.default, }) const { messages } = await complete({ chain }) @@ -360,7 +356,10 @@ describe('chain', async () => { {{bar}} `) - const correctChain = new Chain({ prompt: correctPrompt, adapter: Adapters.default }) + const correctChain = new Chain({ + prompt: correctPrompt, + adapter: Adapters.default, + }) const { messages } = await complete({ chain: correctChain }) expect(messages[messages.length - 2]!).toEqual({ @@ -376,7 +375,7 @@ describe('chain', async () => { const incorrectChain = new Chain({ prompt: incorrectPrompt, parameters: {}, - adapter: Adapters.default + adapter: Adapters.default, }) const action = () => complete({ chain: incorrectChain }) @@ -389,7 +388,7 @@ describe('chain', async () => { {{ foo = 0 }} {{for element in [1, 2, 3]}} - + {{foo}} @@ -406,15 +405,9 @@ describe('chain', async () => { const { messages } = await complete({ chain, maxSteps: 6 }) expect(messages.length).toBe(8) - expect((messages[0]!.content[0]! as TextContent).text).toBe( - '0', - ) - expect((messages[2]!.content[0]! as TextContent).text).toBe( - '1', - ) - expect((messages[4]!.content[0]! as TextContent).text).toBe( - '2', - ) + expect((messages[0]!.content[0]! as TextContent).text).toBe('0') + expect((messages[2]!.content[0]! as TextContent).text).toBe('1') + expect((messages[4]!.content[0]! as TextContent).text).toBe('2') expect(messages[6]).toEqual({ role: MessageRole.system, content: [ @@ -466,9 +459,7 @@ describe('chain', async () => { const chain = new Chain({ prompt, adapter: Adapters.default }) const { messages } = await complete({ chain }) - const userMessages = messages.filter( - (m) => m.role === MessageRole.user, - ) + const userMessages = messages.filter((m) => m.role === MessageRole.user) const userMessageText = userMessages .map((m) => m.content.map((c) => (c as TextContent).text).join(' ')) .join('\n') @@ -500,7 +491,7 @@ describe('chain', async () => { it('saves the response in a variable', async () => { const prompt = removeCommonIndent(` <${TAG_NAMES.step} raw="rawResponse" as="responseText"/> - + {{rawResponse}} @@ -560,3 +551,30 @@ describe('chain', async () => { expect(finalConversation.config.temperature).toBe(0.5) }) }) + +describe('chain global messages count', async () => { + it('display messages count', async () => { + const prompt = removeCommonIndent(` + {{ foo = 'foo' }} + System message + + {{for element in [1, 2, 3]}} + + User message: {{element}} + + {{endfor}} + + + Assistant message: {{foo}} + + `) + + const chain = new Chain({ + prompt: removeCommonIndent(prompt), + parameters: {}, + adapter: Adapters.default, + }) + await complete({ chain }) + expect(chain.globalMessagesCount).toBe(6) + }) +}) diff --git a/src/compiler/chain.ts b/src/compiler/chain.ts index 575bab6..a716454 100644 --- a/src/compiler/chain.ts +++ b/src/compiler/chain.ts @@ -1,3 +1,7 @@ +import { + deserializeChain, + SerializedProps, +} from '$promptl/compiler/deserializeChain' import { CHAIN_STEP_ISOLATED_ATTR } from '$promptl/constants' import parse from '$promptl/parser' import { Fragment } from '$promptl/parser/interfaces' @@ -25,10 +29,16 @@ type ChainStep = ProviderConversation & { type StepResponse = | string + | M[] | (Omit & { role?: M['role'] }) +type BuildStepResponseContent = { + messages?: Message[] + contents: MessageContent[] | undefined +} + export class Chain { public rawText: string @@ -43,90 +53,87 @@ export class Chain { private globalConfig: Config | undefined private wasLastStepIsolated: boolean = false + static deserialize(args: SerializedProps) { + return deserializeChain(args) + } + constructor({ prompt, parameters = {}, + serialized, adapter = Adapters.openai as ProviderAdapter, ...compileOptions }: { prompt: string parameters?: Record adapter?: ProviderAdapter + serialized?: { + ast: Fragment + scope: Scope + globalConfig: Config | undefined + globalMessages: Message[] + } } & CompileOptions) { this.rawText = prompt - this.ast = parse(prompt) - this.scope = new Scope(parameters) + + // Init from a serialized chain + this.ast = serialized?.ast ?? parse(prompt) + this.scope = serialized?.scope ?? new Scope(parameters) + this.globalConfig = serialized?.globalConfig + this.globalMessages = serialized?.globalMessages ?? [] + this.didStart = !!serialized + this.adapter = adapter this.compileOptions = compileOptions + if (this.adapter !== Adapters.default) { this.compileOptions.includeSourceMap = false } } - private buildStepResponseContent( - response?: StepResponse, - ): MessageContent[] | undefined { - if (response == undefined) return response - - if (typeof response === 'string') { - return [{ type: ContentType.text, text: response }] - } - - const responseMessage = { - ...response, - role: response.role ?? MessageRole.assistant, - } as M - - const convertedMessages = this.adapter.toPromptl({ - config: this.globalConfig ?? {}, - messages: [responseMessage], - }) - - return convertedMessages.messages[0]!.content - } - async step(response?: StepResponse): Promise> { if (this._completed) { throw new Error('The chain has already completed') } + if (!this.didStart && response !== undefined) { throw new Error('A response is not allowed before the chain has started') } + if (this.didStart && response === undefined) { throw new Error('A response is required to continue the chain') } + this.didStart = true const responseContent = this.buildStepResponseContent(response) - if (responseContent && !this.wasLastStepIsolated) { - this.globalMessages.push({ - role: MessageRole.assistant, - content: responseContent ?? [], - }) + const newGlobalMessages = this.buildGlobalMessages(responseContent) + + if (newGlobalMessages.length > 0) { + this.globalMessages = [ + ...this.globalMessages, + ...(newGlobalMessages as Message[]), + ] } const compile = new Compile({ ast: this.ast, rawText: this.rawText, globalScope: this.scope, - stepResponse: responseContent, + stepResponse: responseContent.contents, ...this.compileOptions, }) - const { - completed, - scopeStash, - ast, - messages: messages, - globalConfig, - stepConfig, - } = await compile.run() + const { completed, scopeStash, ast, messages, globalConfig, stepConfig } = + await compile.run() this.scope = Scope.withStash(scopeStash).copy(this.scope.getPointers()) this.ast = ast this.globalConfig = globalConfig ?? this.globalConfig - this._completed = completed && !messages.length // If it returned a message, there is still a final step to be taken + + // If it returned a message, there is still a final step to be taken + this._completed = completed && !messages.length const config = { ...this.globalConfig, @@ -140,7 +147,9 @@ export class Chain { ...messages, ] - if (!this.wasLastStepIsolated) this.globalMessages.push(...messages) + if (!this.wasLastStepIsolated) { + this.globalMessages.push(...messages) + } return { ...this.adapter.fromPromptl({ @@ -151,7 +160,77 @@ export class Chain { } } + serialize() { + if (!this.didStart) { + throw new Error( + 'The chain has not started yet. You must call `step` at least once before calling `serialize`.', + ) + } + + return { + ast: this.ast, + scope: this.scope.serialize(), + adapterType: this.adapter.type, + compilerOptions: this.compileOptions, + globalConfig: this.globalConfig, + globalMessages: this.globalMessages, + rawText: this.rawText, + } + } + + get globalMessagesCount(): number { + return this.globalMessages.length + } + get completed(): boolean { return this._completed } + + private buildStepResponseContent( + response?: StepResponse | M[], + ): BuildStepResponseContent { + if (response == undefined) return { contents: undefined } + if (typeof response === 'string') { + return { contents: [{ text: response, type: ContentType.text }] } + } + + if (Array.isArray(response)) { + const converted = this.adapter.toPromptl({ + config: this.globalConfig ?? {}, + messages: response as M[], + }) + const contents = converted.messages.flatMap((m) => m.content) + return { messages: converted.messages as Message[], contents } + } + + const responseMessage = { + ...response, + role: response.role ?? MessageRole.assistant, + } as M + + const convertedMessages = this.adapter.toPromptl({ + config: this.globalConfig ?? {}, + messages: [responseMessage], + }) + + return { contents: convertedMessages.messages[0]!.content } + } + + private buildGlobalMessages( + buildStepResponseContent: BuildStepResponseContent, + ) { + const { messages, contents } = buildStepResponseContent + + if (this.wasLastStepIsolated) return [] + if (!contents) return [] + + if (messages) return messages + + return [ + { + role: MessageRole.assistant, + content: contents ?? [], + }, + ] + } } diff --git a/src/compiler/compile.test.ts b/src/compiler/compile.test.ts index dd1fcb5..a68e285 100644 --- a/src/compiler/compile.test.ts +++ b/src/compiler/compile.test.ts @@ -76,7 +76,11 @@ describe('automatic message grouping', async () => { it('allows defining the default role', async () => { const prompt = 'Hello world!' - const result = await render({ prompt, defaultRole: MessageRole.user, adapter: Adapters.default }) + const result = await render({ + prompt, + defaultRole: MessageRole.user, + adapter: Adapters.default, + }) const message = result.messages[0]! expect(message.role).toBe(MessageRole.user) }) @@ -463,87 +467,96 @@ describe('operators', async () => { }) }) -describe("source map", async () => { - it("does not include source map when not specified", async () => { +describe('source map', async () => { + it('does not include source map when not specified', async () => { const prompt = ` Given a context, answer questions succintly yet complete. {{ context }} Please, help me with {{ question }}! - `; + ` const { messages } = await render({ prompt, parameters: { - context: "context", - question: "question", + context: 'context', + question: 'question', }, adapter: Adapters.default, - }); + }) expect(messages).toEqual([ { role: MessageRole.system, - content: [{ type: ContentType.text, text: "Given a context, answer questions succintly yet complete." }], + content: [ + { + type: ContentType.text, + text: 'Given a context, answer questions succintly yet complete.', + }, + ], }, { role: MessageRole.system, - content: [{ type: ContentType.text, text: "context" }], + content: [{ type: ContentType.text, text: 'context' }], }, { role: MessageRole.user, - content: [{ type: ContentType.text, text: "Please, help me with question!" }], + content: [ + { type: ContentType.text, text: 'Please, help me with question!' }, + ], }, - ]); - }); + ]) + }) - it("does not include source map when non-default adapter", async () => { + it('does not include source map when non-default adapter', async () => { const prompt = ` Given a context, answer questions succintly yet complete. {{ context }} Please, help me with {{ question }}! - `; + ` const { messages } = await render({ prompt, parameters: { - context: "context", - question: "question", + context: 'context', + question: 'question', }, adapter: Adapters.openai, includeSourceMap: true, - }); + }) expect(messages).toEqual([ { role: MessageRole.system, - content: "Given a context, answer questions succintly yet complete.", + content: 'Given a context, answer questions succintly yet complete.', }, { role: MessageRole.system, - content: "context", + content: 'context', }, { role: MessageRole.user, - content: [{ type: ContentType.text, text: "Please, help me with question!" }], + content: [ + { type: ContentType.text, text: 'Please, help me with question!' }, + ], }, - ]); - }); + ]) + }) - describe("includes source map when specified", async () => { - it("returns empty source map when no identifiers", async () => { + describe('includes source map when specified', async () => { + it('returns empty source map when no identifiers', async () => { const prompt = ` Given a context, answer questions succintly yet complete. context Please, help me with question! - `; + ` const { messages } = await render({ prompt, adapter: Adapters.default, includeSourceMap: true, - }); + }) expect(messages).toEqual([ { role: MessageRole.system, content: [ { type: ContentType.text, - text: "Given a context, answer questions succintly yet complete.", + text: 'Given a context, answer questions succintly yet complete.', _promptlSourceMap: [], }, ], @@ -553,7 +566,7 @@ Given a context, answer questions succintly yet complete. content: [ { type: ContentType.text, - text: "context", + text: 'context', _promptlSourceMap: [], }, ], @@ -563,36 +576,36 @@ Given a context, answer questions succintly yet complete. content: [ { type: ContentType.text, - text: "Please, help me with question!", + text: 'Please, help me with question!', _promptlSourceMap: [], }, ], }, - ]); - }); + ]) + }) - it("returns source map when single identifiers per content", async () => { + it('returns source map when single identifiers per content', async () => { const prompt = ` Given a context, answer questions succintly yet complete. {{ context }} Please, help me with {{ question }}! - `; + ` const { messages } = await render({ prompt, parameters: { - context: "context", - question: "question", + context: 'context', + question: 'question', }, adapter: Adapters.default, includeSourceMap: true, - }); + }) expect(messages).toEqual([ { role: MessageRole.system, content: [ { type: ContentType.text, - text: "Given a context, answer questions succintly yet complete.", + text: 'Given a context, answer questions succintly yet complete.', _promptlSourceMap: [], }, ], @@ -602,12 +615,12 @@ Given a context, answer questions succintly yet complete. content: [ { type: ContentType.text, - text: "context", + text: 'context', _promptlSourceMap: [ { start: 0, end: 7, - identifier: "context", + identifier: 'context', }, ], }, @@ -618,44 +631,44 @@ Given a context, answer questions succintly yet complete. content: [ { type: ContentType.text, - text: "Please, help me with question!", + text: 'Please, help me with question!', _promptlSourceMap: [ { start: 21, end: 29, - identifier: "question", + identifier: 'question', }, ], }, ], }, - ]); - }); + ]) + }) - it("returns source map when multiple identifiers per content", async () => { + it('returns source map when multiple identifiers per content', async () => { const prompt = ` Given some context, answer questions succintly yet complete. {{ context_1 }} and {{ context_2 }} Please, help me with {{ question_1 }} and {{ question_2 }}! - `; + ` const { messages } = await render({ prompt, parameters: { - context_1: "context_1", - context_2: "context_2", - question_1: "question_1", - question_2: "question_2", + context_1: 'context_1', + context_2: 'context_2', + question_1: 'question_1', + question_2: 'question_2', }, adapter: Adapters.default, includeSourceMap: true, - }); + }) expect(messages).toEqual([ { role: MessageRole.system, content: [ { type: ContentType.text, - text: "Given some context, answer questions succintly yet complete.", + text: 'Given some context, answer questions succintly yet complete.', _promptlSourceMap: [], }, ], @@ -665,17 +678,17 @@ Given some context, answer questions succintly yet complete. content: [ { type: ContentType.text, - text: "context_1 and context_2", + text: 'context_1 and context_2', _promptlSourceMap: [ { start: 0, end: 9, - identifier: "context_1", + identifier: 'context_1', }, { start: 14, end: 23, - identifier: "context_2", + identifier: 'context_2', }, ], }, @@ -686,47 +699,47 @@ Given some context, answer questions succintly yet complete. content: [ { type: ContentType.text, - text: "Please, help me with question_1 and question_2!", + text: 'Please, help me with question_1 and question_2!', _promptlSourceMap: [ { start: 21, end: 31, - identifier: "question_1", + identifier: 'question_1', }, { start: 36, end: 46, - identifier: "question_2", + identifier: 'question_2', }, ], }, ], }, - ]); - }); + ]) + }) - it("returns source map when duplicated identifiers", async () => { + it('returns source map when duplicated identifiers', async () => { const prompt = ` Given a context, answer questions succintly yet complete. {{ context }} and {{ context }} Please, help me with {{ question }} and {{ question }}! - `; + ` const { messages } = await render({ prompt, parameters: { - context: "context", - question: "question", + context: 'context', + question: 'question', }, adapter: Adapters.default, includeSourceMap: true, - }); + }) expect(messages).toEqual([ { role: MessageRole.system, content: [ { type: ContentType.text, - text: "Given a context, answer questions succintly yet complete.", + text: 'Given a context, answer questions succintly yet complete.', _promptlSourceMap: [], }, ], @@ -736,17 +749,17 @@ Given a context, answer questions succintly yet complete. content: [ { type: ContentType.text, - text: "context and context", + text: 'context and context', _promptlSourceMap: [ { start: 0, end: 7, - identifier: "context", + identifier: 'context', }, { start: 12, end: 19, - identifier: "context", + identifier: 'context', }, ], }, @@ -757,26 +770,26 @@ Given a context, answer questions succintly yet complete. content: [ { type: ContentType.text, - text: "Please, help me with question and question!", + text: 'Please, help me with question and question!', _promptlSourceMap: [ { start: 21, end: 29, - identifier: "question", + identifier: 'question', }, { start: 34, end: 42, - identifier: "question", + identifier: 'question', }, ], }, ], }, - ]); - }); + ]) + }) - it("returns source map when multiple new lines and indents", async () => { + it('returns source map when multiple new lines and indents', async () => { const prompt = ` Given a context, answer questions succintly yet complete. @@ -807,27 +820,27 @@ Given a context, answer questions succintly yet complete. {{empty}}{{empty}} - `; + ` const { messages } = await render({ prompt, parameters: { - context: "context", - question: "question", - lyrics: "lyrics", - image: "image", - file: "file", + context: 'context', + question: 'question', + lyrics: 'lyrics', + image: 'image', + file: 'file', empty: ' ', }, adapter: Adapters.default, includeSourceMap: true, - }); + }) expect(messages).toEqual([ { role: MessageRole.system, content: [ { type: ContentType.text, - text: "Given a context, answer questions succintly yet complete.", + text: 'Given a context, answer questions succintly yet complete.', _promptlSourceMap: [], }, ], @@ -837,12 +850,12 @@ Given a context, answer questions succintly yet complete. content: [ { type: ContentType.text, - text: "context", + text: 'context', _promptlSourceMap: [ { start: 0, end: 7, - identifier: "context", + identifier: 'context', }, ], }, @@ -853,12 +866,12 @@ Given a context, answer questions succintly yet complete. content: [ { type: ContentType.text, - text: "Please, help me\n with question!", + text: 'Please, help me\n with question!', _promptlSourceMap: [ { start: 23, end: 31, - identifier: "question", + identifier: 'question', }, ], }, @@ -869,46 +882,46 @@ Given a context, answer questions succintly yet complete. content: [ { type: ContentType.text, - text: "Is this the real life?", + text: 'Is this the real life?', _promptlSourceMap: [], }, { type: ContentType.text, - text: "Is this just fantasy?\n lyrics", + text: 'Is this just fantasy?\n lyrics', _promptlSourceMap: [ { start: 24, end: 30, - identifier: "lyrics", + identifier: 'lyrics', }, ], }, { type: ContentType.image, - image: "image", + image: 'image', _promptlSourceMap: [ { start: 0, end: 5, - identifier: "image", + identifier: 'image', }, ], }, { type: ContentType.file, - file: "file", - mimeType: "text/plain", + file: 'file', + mimeType: 'text/plain', _promptlSourceMap: [ { start: 0, end: 4, - identifier: "file", + identifier: 'file', }, ], }, ], }, - ]); - }); - }); -}); + ]) + }) + }) +}) diff --git a/src/compiler/compile.ts b/src/compiler/compile.ts index 951b243..5cbe12b 100644 --- a/src/compiler/compile.ts +++ b/src/compiler/compile.ts @@ -149,7 +149,10 @@ export class Compile { this.globalConfig = config } - private getSourceRef(text: string, node?: TemplateNode): PromptlSourceRef | undefined { + private getSourceRef( + text: string, + node?: TemplateNode, + ): PromptlSourceRef | undefined { if (!node) return undefined if (node.type !== 'MustacheTag') return undefined @@ -161,9 +164,9 @@ export class Compile { switch (node.expression.type) { case 'Identifier': sourceRef.identifier = node.expression.name - break; + break default: - break; + break } return sourceRef @@ -184,17 +187,20 @@ export class Compile { ): PromptlSourceRef[] { const indent = getCommonIndent(text) let position = 0 - text = text.split('\n').map((line) => { - const offset = line.length - line.slice(indent).length - line = line.slice(indent) - sourceMap = sourceMap.map((ref) => ({ - ...ref, - start: ref.start >= position ? ref.start - offset : ref.start, - end: ref.end >= position ? ref.end - offset : ref.end, - })) - position += line.length + 1 - return line - }).join('\n') + text = text + .split('\n') + .map((line) => { + const offset = line.length - line.slice(indent).length + line = line.slice(indent) + sourceMap = sourceMap.map((ref) => ({ + ...ref, + start: ref.start >= position ? ref.start - offset : ref.start, + end: ref.end >= position ? ref.end - offset : ref.end, + })) + position += line.length + 1 + return line + }) + .join('\n') const offset = text.length - text.trimStart().length text = text.trimStart() @@ -210,7 +216,7 @@ export class Compile { start: Math.min(ref.start, text.length), end: Math.min(ref.end, text.length), })) - + return sourceMap } @@ -220,7 +226,7 @@ export class Compile { this.accumulatedText.sourceMap, ) const text = removeCommonIndent(this.accumulatedText.text) - + this.accumulatedText.text = '' this.accumulatedText.sourceMap = [] diff --git a/src/compiler/deserializeChain.test.ts b/src/compiler/deserializeChain.test.ts new file mode 100644 index 0000000..8e749ca --- /dev/null +++ b/src/compiler/deserializeChain.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest' + +import { removeCommonIndent } from './utils' +import { Adapters } from '$promptl/providers' +import { Chain } from './chain' + +describe('deserialize chain', async () => { + it('get final step from serialized chain', async () => { + const prompt = removeCommonIndent(` + + {{foo = 5}} + + + {{foo += 1}} + + + + The final result is {{foo}} + + + `) + + const chain = new Chain({ prompt, adapter: Adapters.openai }) + + await chain.step() + await chain.step('First step response') + const serializedChain = chain.serialize() + const serialized = JSON.stringify(serializedChain) + + // In another context we deserialize existing chain + const deserializedChain = Chain.deserialize({ serialized }) + const { messages } = await deserializedChain!.step('Last step') + expect(messages[messages.length - 1]).toEqual({ + role: 'assistant', + tool_calls: undefined, + content: [{ text: 'The final result is 6', type: 'text' }], + }) + }) +}) diff --git a/src/compiler/deserializeChain.ts b/src/compiler/deserializeChain.ts new file mode 100644 index 0000000..e2c2088 --- /dev/null +++ b/src/compiler/deserializeChain.ts @@ -0,0 +1,80 @@ +import { SerializedChain } from '$promptl/compiler' +import { Chain } from '$promptl/compiler/chain' +import Scope from '$promptl/compiler/scope' +import { AdapterKey, Adapters, ProviderAdapter } from '$promptl/providers' +import { Message } from '$promptl/types' + +function getAdapter(adapterType: AdapterKey) { + const adapter = Adapters[adapterType] + + if (!adapter) throw new Error(`Adapter not found: ${adapterType}`) + + return adapter as ProviderAdapter +} + +function safeSerializedData(data: string | SerializedChain): SerializedChain { + try { + const serialized = + typeof data === 'string' + ? JSON.parse(data) + : typeof data === 'object' + ? data + : {} + + const compilerOptions = serialized.compilerOptions || {} + const globalConfig = serialized.globalConfig + const globalMessages = serialized.globalMessages || [] + + if ( + typeof serialized !== 'object' || + typeof serialized.ast !== 'object' || + typeof serialized.scope !== 'object' || + typeof serialized.adapterType !== 'string' || + typeof serialized.rawText !== 'string' + ) { + throw new Error() + } + return { + rawText: serialized.rawText, + ast: serialized.ast, + scope: serialized.scope, + adapterType: serialized.adapterType, + compilerOptions, + globalConfig, + globalMessages, + } + } catch { + throw new Error('Invalid serialized chain data') + } +} + +export type SerializedProps = { + serialized: string | SerializedChain | undefined | null +} +export function deserializeChain({ + serialized, +}: SerializedProps): Chain | undefined { + if (!serialized) return undefined + + const { + rawText, + ast, + scope: serializedScope, + adapterType, + compilerOptions, + globalConfig, + globalMessages, + } = safeSerializedData(serialized) + + const adapter = getAdapter(adapterType) + const scope = new Scope() + scope.setStash(serializedScope.stash) + scope.setPointers(serializedScope.pointers) + + return new Chain({ + prompt: rawText, + serialized: { ast, scope, globalConfig, globalMessages }, + adapter, + ...compilerOptions, + }) +} diff --git a/src/compiler/index.ts b/src/compiler/index.ts index 9c52363..17fdefe 100644 --- a/src/compiler/index.ts +++ b/src/compiler/index.ts @@ -58,4 +58,6 @@ export function scan({ }).run() } -export { Chain, type Document, type ReferencePromptFn } +type SerializedChain = ReturnType + +export { Chain, type SerializedChain, type Document, type ReferencePromptFn } diff --git a/src/compiler/scan.test.ts b/src/compiler/scan.test.ts index a51fc6d..d664c97 100644 --- a/src/compiler/scan.test.ts +++ b/src/compiler/scan.test.ts @@ -1,71 +1,70 @@ -import { TAG_NAMES } from "$promptl/constants"; -import path from "path"; -import CompileError from "$promptl/error/error"; -import { describe, expect, it } from "vitest"; -import { z } from "zod"; +import { TAG_NAMES } from '$promptl/constants' +import path from 'path' +import CompileError from '$promptl/error/error' +import { describe, expect, it } from 'vitest' +import { z } from 'zod' -import { scan } from "."; -import { Document } from "./types"; -import { removeCommonIndent } from "./utils"; +import { scan } from '.' +import { Document } from './types' +import { removeCommonIndent } from './utils' type PromptTree = { - [path: string]: string | PromptTree; -}; + [path: string]: string | PromptTree +} const referenceFn = (prompts: PromptTree) => { return async ( relativePath: string, currentAbsolutePath?: string, ): Promise => { - path.resolve; + path.resolve const refAbsolutePath = currentAbsolutePath ? path - .resolve(path.dirname(`/${currentAbsolutePath}`), relativePath) - .replace(/^\//, "") - : relativePath; + .resolve(path.dirname(`/${currentAbsolutePath}`), relativePath) + .replace(/^\//, '') + : relativePath - if (!(refAbsolutePath in prompts)) return undefined; + if (!(refAbsolutePath in prompts)) return undefined return { path: refAbsolutePath, content: prompts[refAbsolutePath]!, - } as Document; - }; -}; - -describe("hash", async () => { - it("always returns the same hash for the same prompt", async () => { - const prompt1 = "This is a prompt"; - const prompt2 = "This is a prompt"; - const prompt3 = "This is another prompt"; - - const metadata1 = await scan({ prompt: prompt1 }); - const metadata2 = await scan({ prompt: prompt2 }); - const metadata3 = await scan({ prompt: prompt3 }); - - expect(metadata1.hash).toBe(metadata2.hash); - expect(metadata1.hash).not.toBe(metadata3.hash); - }); - - it("includes the content from referenced tags into account when calculating the hash", async () => { - const parent = - 'This is the parent prompt. The end.'; - const child1 = "ABCDEFG"; - const child2 = "1234567"; + } as Document + } +} + +describe('hash', async () => { + it('always returns the same hash for the same prompt', async () => { + const prompt1 = 'This is a prompt' + const prompt2 = 'This is a prompt' + const prompt3 = 'This is another prompt' + + const metadata1 = await scan({ prompt: prompt1 }) + const metadata2 = await scan({ prompt: prompt2 }) + const metadata3 = await scan({ prompt: prompt3 }) + + expect(metadata1.hash).toBe(metadata2.hash) + expect(metadata1.hash).not.toBe(metadata3.hash) + }) + + it('includes the content from referenced tags into account when calculating the hash', async () => { + const parent = 'This is the parent prompt. The end.' + const child1 = 'ABCDEFG' + const child2 = '1234567' const metadata1 = await scan({ prompt: parent, referenceFn: referenceFn({ child: child1 }), - }); + }) const metadata2 = await scan({ prompt: parent, referenceFn: referenceFn({ child: child2 }), - }); + }) - expect(metadata1.hash).not.toBe(metadata2.hash); - }); + expect(metadata1.hash).not.toBe(metadata2.hash) + }) - it("works with multiple levels of nesting", async () => { + it('works with multiple levels of nesting', async () => { const prompts = { parent: removeCommonIndent(` Parent: @@ -81,81 +80,81 @@ describe("hash", async () => { grandchild2: removeCommonIndent(` Grandchild 2. `), - }; + } const parentMetadata1 = await scan({ - prompt: prompts["parent"]!, + prompt: prompts['parent']!, referenceFn: referenceFn({ ...prompts, - grandchild: prompts["grandchild1"], + grandchild: prompts['grandchild1'], }), - }); + }) const parentMetadata2 = await scan({ - prompt: prompts["parent"]!, + prompt: prompts['parent']!, referenceFn: referenceFn({ ...prompts, - grandchild: prompts["grandchild2"], + grandchild: prompts['grandchild2'], }), - }); + }) - expect(parentMetadata1.hash).not.toBe(parentMetadata2.hash); - }); + expect(parentMetadata1.hash).not.toBe(parentMetadata2.hash) + }) - it("extract all the paths of the referenced prompts", async () => { + it('extract all the paths of the referenced prompts', async () => { const prompts = { - rootFile: removeCommonIndent("Root File"), - "siblingParent/sibling": removeCommonIndent("Sibling Parent File"), - "somefolder/parent": removeCommonIndent(` + rootFile: removeCommonIndent('Root File'), + 'siblingParent/sibling': removeCommonIndent('Sibling Parent File'), + 'somefolder/parent': removeCommonIndent(` Parent: `), - "somefolder/children/child1": removeCommonIndent(` + 'somefolder/children/child1': removeCommonIndent(` Child 1: `), - "somefolder/children/grandchildren/grandchild1": removeCommonIndent(` + 'somefolder/children/grandchildren/grandchild1': removeCommonIndent(` Grandchild 1: `), - "somefolder/children/grandchildren/grand-grand-grandChildren/deepestGrandChild": - removeCommonIndent("Grandchild 2"), - "somefolder/children/child2": removeCommonIndent(` + 'somefolder/children/grandchildren/grand-grand-grandChildren/deepestGrandChild': + removeCommonIndent('Grandchild 2'), + 'somefolder/children/child2': removeCommonIndent(` Child 2: `), - "somefolder/children/grandchildren/grandchild2": - removeCommonIndent("Grandchild 2"), - "somefolder/children/childSibling": removeCommonIndent(` + 'somefolder/children/grandchildren/grandchild2': + removeCommonIndent('Grandchild 2'), + 'somefolder/children/childSibling': removeCommonIndent(` Link to grand grand child 2: `), - }; + } const metadata = await scan({ - prompt: prompts["somefolder/parent"], + prompt: prompts['somefolder/parent'], referenceFn: referenceFn(prompts), - fullPath: "somefolder/parent", - }); + fullPath: 'somefolder/parent', + }) - const includedPaths = Array.from(metadata.includedPromptPaths); + const includedPaths = Array.from(metadata.includedPromptPaths) expect(includedPaths).toEqual([ - "somefolder/parent", - "somefolder/children/child1", - "somefolder/children/grandchildren/grandchild1", - "somefolder/children/grandchildren/grand-grand-grandChildren/deepestGrandChild", - "somefolder/children/childSibling", - "siblingParent/sibling", - "somefolder/children/child2", - "somefolder/children/grandchildren/grandchild2", - ]); - }); - - it("works with nested tags", async () => { + 'somefolder/parent', + 'somefolder/children/child1', + 'somefolder/children/grandchildren/grandchild1', + 'somefolder/children/grandchildren/grand-grand-grandChildren/deepestGrandChild', + 'somefolder/children/childSibling', + 'siblingParent/sibling', + 'somefolder/children/child2', + 'somefolder/children/grandchildren/grandchild2', + ]) + }) + + it('works with nested tags', async () => { const prompts = { parent: removeCommonIndent(` {{if foo}} @@ -173,63 +172,63 @@ describe("hash", async () => { child2v2: removeCommonIndent(` baz! `), - }; + } const parentMetadatav1 = await scan({ - prompt: prompts["parent"]!, + prompt: prompts['parent']!, referenceFn: referenceFn({ ...prompts, - child2: prompts["child2v1"], + child2: prompts['child2v1'], }), - }); + }) const parentMetadatav2 = await scan({ - prompt: prompts["parent"]!, + prompt: prompts['parent']!, referenceFn: referenceFn({ ...prompts, - child2: prompts["child2v2"], + child2: prompts['child2v2'], }), - }); + }) - expect(parentMetadatav1.hash).not.toBe(parentMetadatav2.hash); - }); -}); + expect(parentMetadatav1.hash).not.toBe(parentMetadatav2.hash) + }) +}) -describe("resolvedPrompt", async () => { - it("returns the prompt without comments", async () => { +describe('resolvedPrompt', async () => { + it('returns the prompt without comments', async () => { const prompt = ` This is a prompt. /* This is a comment */ This is another prompt. - `; + ` const metadata = await scan({ prompt: removeCommonIndent(prompt), - }); + }) expect(metadata.resolvedPrompt).toBe( - "This is a prompt.\n\nThis is another prompt.", - ); - }); + 'This is a prompt.\n\nThis is another prompt.', + ) + }) - it("Replaces reference tags with scope tags", async () => { + it('Replaces reference tags with scope tags', async () => { const prompt = removeCommonIndent(` This is a prompt. This is another prompt. - `); + `) const childPrompt = removeCommonIndent(` This is the referenced prompt. {{ bar }} - `); + `) const metadata = await scan({ prompt, referenceFn: referenceFn({ child: childPrompt }), - }); + }) expect(metadata.resolvedPrompt).toBe( removeCommonIndent(` @@ -240,17 +239,17 @@ describe("resolvedPrompt", async () => { This is another prompt. `), - ); - }); + ) + }) - it("only includes the parent config", async () => { + it('only includes the parent config', async () => { const prompt = removeCommonIndent(` --- config: parent --- - `); + `) const childPrompt = removeCommonIndent(` --- @@ -259,12 +258,12 @@ describe("resolvedPrompt", async () => { --- This is the child prompt. - `); + `) const metadata = await scan({ prompt, referenceFn: referenceFn({ child: childPrompt }), - }); + }) expect(metadata.resolvedPrompt).toBe( removeCommonIndent(` @@ -275,13 +274,13 @@ describe("resolvedPrompt", async () => { This is the child prompt. `), - ); - expect(metadata.config).toEqual({ config: "parent" }); - }); -}); + ) + expect(metadata.config).toEqual({ config: 'parent' }) + }) +}) -describe("config", async () => { - it("compiles the YAML written in the config section and returns it as the config attribute in the result", async () => { +describe('config', async () => { + it('compiles the YAML written in the config section and returns it as the config attribute in the result', async () => { const prompt = ` --- foo: bar @@ -289,18 +288,18 @@ describe("config", async () => { - qux - quux --- - `; + ` const metadata = await scan({ prompt: removeCommonIndent(prompt), - }); + }) expect(metadata.config).toEqual({ - foo: "bar", - baz: ["qux", "quux"], - }); - }); + foo: 'bar', + baz: ['qux', 'quux'], + }) + }) - it("does not confuse several dashes with a config section", async () => { + it('does not confuse several dashes with a config section', async () => { const prompt = removeCommonIndent(` This is not config: -------------------- @@ -310,18 +309,18 @@ describe("config", async () => { This ain't either: -- - `); + `) const metadata = await scan({ prompt: removeCommonIndent(prompt), - }); + }) - expect(metadata.errors[0]?.toString()).toBeUndefined(); - expect(metadata.errors.length).toBe(0); - expect(metadata.config).toEqual({}); - }); + expect(metadata.errors[0]?.toString()).toBeUndefined() + expect(metadata.errors.length).toBe(0) + expect(metadata.config).toEqual({}) + }) - it("can be escaped", async () => { + it('can be escaped', async () => { const prompt = removeCommonIndent(` This is NOT a config: \\--- @@ -330,14 +329,14 @@ describe("config", async () => { - qux - quux \\--- - `); + `) - const metadata = await scan({ prompt }); + const metadata = await scan({ prompt }) - expect(metadata.config).toEqual({}); - }); + expect(metadata.config).toEqual({}) + }) - it("fails when there is content before the config section", async () => { + it('fails when there is content before the config section', async () => { const prompt = removeCommonIndent(` Lorem ipsum --- @@ -346,32 +345,32 @@ describe("config", async () => { - qux - quux --- - `); + `) - const metadata = await scan({ prompt }); + const metadata = await scan({ prompt }) - expect(metadata.errors.length).toBe(1); - expect(metadata.errors[0]).toBeInstanceOf(CompileError); - expect(metadata.errors[0]!.code).toBe("invalid-config-placement"); - }); + expect(metadata.errors.length).toBe(1) + expect(metadata.errors[0]).toBeInstanceOf(CompileError) + expect(metadata.errors[0]!.code).toBe('invalid-config-placement') + }) - it("fails when the config is not valid YAML", async () => { + it('fails when the config is not valid YAML', async () => { const prompt = removeCommonIndent(` --- foo: bar baa --- - `); + `) - const metadata = await scan({ prompt }); + const metadata = await scan({ prompt }) - expect(metadata.config).toEqual({ foo: "bar", baa: null }); - expect(metadata.errors.length).toBe(1); - expect(metadata.errors[0]).toBeInstanceOf(CompileError); - expect(metadata.errors[0]!.code).toBe("invalid-config"); - }); + expect(metadata.config).toEqual({ foo: 'bar', baa: null }) + expect(metadata.errors.length).toBe(1) + expect(metadata.errors[0]).toBeInstanceOf(CompileError) + expect(metadata.errors[0]!.code).toBe('invalid-config') + }) - it("fails when there are multiple config sections", async () => { + it('fails when there are multiple config sections', async () => { const prompt = removeCommonIndent(` --- foo: bar @@ -379,69 +378,69 @@ describe("config", async () => { --- baz: qux --- - `); + `) - const metadata = await scan({ prompt }); + const metadata = await scan({ prompt }) - expect(metadata.errors.length).toBe(1); - expect(metadata.errors[0]).toBeInstanceOf(CompileError); - expect(metadata.errors[0]!.code).toBe("config-already-declared"); - }); + expect(metadata.errors.length).toBe(1) + expect(metadata.errors[0]).toBeInstanceOf(CompileError) + expect(metadata.errors[0]!.code).toBe('config-already-declared') + }) - it("fails when a schema is provided and there is no config section", async () => { + it('fails when a schema is provided and there is no config section', async () => { const prompt = removeCommonIndent(` Lorem ipsum - `); + `) const metadata = await scan({ prompt, configSchema: z.object({ foo: z.string(), }), - }); + }) - expect(metadata.errors.length).toBe(1); - expect(metadata.errors[0]).toBeInstanceOf(CompileError); - expect(metadata.errors[0]!.code).toBe("config-not-found"); - }); + expect(metadata.errors.length).toBe(1) + expect(metadata.errors[0]).toBeInstanceOf(CompileError) + expect(metadata.errors[0]!.code).toBe('config-not-found') + }) - it("fails when the configSchema is not validated", async () => { + it('fails when the configSchema is not validated', async () => { const prompt = removeCommonIndent(` --- foo: 2 --- - `); + `) const metadata = await scan({ prompt, configSchema: z.object({ foo: z.string(), }), - }); + }) - expect(metadata.errors.length).toBe(1); - expect(metadata.errors[0]).toBeInstanceOf(CompileError); - expect(metadata.errors[0]!.code).toBe("invalid-config"); - }); + expect(metadata.errors.length).toBe(1) + expect(metadata.errors[0]).toBeInstanceOf(CompileError) + expect(metadata.errors[0]!.code).toBe('invalid-config') + }) - it("does not fail when the config schema is validated", async () => { + it('does not fail when the config schema is validated', async () => { const prompt = removeCommonIndent(` --- foo: bar --- - `); + `) const metadata = await scan({ prompt, configSchema: z.object({ foo: z.string(), }), - }); + }) - expect(metadata.errors.length).toBe(0); - }); + expect(metadata.errors.length).toBe(0) + }) - it("returns the correct positions of parsing errors", async () => { + it('returns the correct positions of parsing errors', async () => { const prompt = removeCommonIndent(` /* Lorem ipsum @@ -450,100 +449,100 @@ describe("config", async () => { foo: bar baa --- - `); + `) - const expectedErrorPosition = prompt.indexOf("baa"); + const expectedErrorPosition = prompt.indexOf('baa') - const metadata = await scan({ prompt }); + const metadata = await scan({ prompt }) - expect(metadata.errors.length).toBe(1); - expect(metadata.errors[0]).toBeInstanceOf(CompileError); - expect(metadata.errors[0]!.code).toBe("invalid-config"); - expect(metadata.errors[0]!.pos).toBe(expectedErrorPosition); - }); + expect(metadata.errors.length).toBe(1) + expect(metadata.errors[0]).toBeInstanceOf(CompileError) + expect(metadata.errors[0]!.code).toBe('invalid-config') + expect(metadata.errors[0]!.pos).toBe(expectedErrorPosition) + }) - it("returns the correct positions of schema errors", async () => { + it('returns the correct positions of schema errors', async () => { const prompt = removeCommonIndent(` --- foo: bar --- - `); + `) const metadata = await scan({ prompt, configSchema: z.object({ foo: z.number(), }), - }); - const expectedErrorPosition = prompt.indexOf("bar"); + }) + const expectedErrorPosition = prompt.indexOf('bar') - expect(metadata.errors.length).toBe(1); - expect(metadata.errors[0]).toBeInstanceOf(CompileError); - expect(metadata.errors[0]!.code).toBe("invalid-config"); - expect(metadata.errors[0]!.pos).toBe(expectedErrorPosition); - }); + expect(metadata.errors.length).toBe(1) + expect(metadata.errors[0]).toBeInstanceOf(CompileError) + expect(metadata.errors[0]!.code).toBe('invalid-config') + expect(metadata.errors[0]!.pos).toBe(expectedErrorPosition) + }) - it("fails when the config section is defined inside an if block", async () => { + it('fails when the config section is defined inside an if block', async () => { const prompt = removeCommonIndent(` {{ if true }} --- foo: bar --- {{ endif }} - `); + `) - const metadata = await scan({ prompt }); + const metadata = await scan({ prompt }) - expect(metadata.errors.length).toBe(1); - expect(metadata.errors[0]).toBeInstanceOf(CompileError); - expect(metadata.errors[0]!.code).toBe("config-outside-root"); - }); -}); + expect(metadata.errors.length).toBe(1) + expect(metadata.errors[0]).toBeInstanceOf(CompileError) + expect(metadata.errors[0]!.code).toBe('config-outside-root') + }) +}) -describe("parameters", async () => { - it("detects undefined variables being used in the prompt", async () => { +describe('parameters', async () => { + it('detects undefined variables being used in the prompt', async () => { const prompt = ` {{ foo }} - `; + ` const metadata = await scan({ prompt: removeCommonIndent(prompt), - }); + }) - expect(metadata.parameters).toEqual(new Set(["foo"])); - }); + expect(metadata.parameters).toEqual(new Set(['foo'])) + }) - it("ignores variables that are defined in the prompt", async () => { + it('ignores variables that are defined in the prompt', async () => { const prompt = ` {{ foo = 5 }} {{ foo }} - `; + ` const metadata = await scan({ prompt: removeCommonIndent(prompt), - }); + }) - expect(metadata.parameters).toEqual(new Set()); - }); + expect(metadata.parameters).toEqual(new Set()) + }) - it("adds the correct parameters to the scope context", async () => { + it('adds the correct parameters to the scope context', async () => { const prompt = ` {{ foo }} {{ bar }} {{ for val in arr }} {{ endfor }} - `; + ` const metadata = await scan({ prompt: removeCommonIndent(prompt), - }); + }) - expect(metadata.parameters).toEqual(new Set(["foo", "bar", "arr"])); - }); -}); + expect(metadata.parameters).toEqual(new Set(['foo', 'bar', 'arr'])) + }) +}) -describe("referenced prompts", async () => { - it("does not include parameters from referenced prompts", async () => { +describe('referenced prompts', async () => { + it('does not include parameters from referenced prompts', async () => { const prompts = { parent: removeCommonIndent(` This is the parent prompt. @@ -554,18 +553,18 @@ describe("referenced prompts", async () => { child: removeCommonIndent(` {{ childParam }} `), - }; + } const metadata = await scan({ - prompt: prompts["parent"]!, + prompt: prompts['parent']!, referenceFn: referenceFn(prompts), - }); + }) - expect(metadata.parameters).toContain("parentParam"); - expect(metadata.parameters).not.toContain("childParam"); - }); + expect(metadata.parameters).toContain('parentParam') + expect(metadata.parameters).not.toContain('childParam') + }) - it("returns an error if a child param is not included in the reference tag", async () => { + it('returns an error if a child param is not included in the reference tag', async () => { const prompts = { child: removeCommonIndent(` {{ childParam }} @@ -580,87 +579,87 @@ describe("referenced prompts", async () => { The end. `), - }; + } const metadataCorrect = await scan({ - prompt: prompts["parentCorrect"]!, + prompt: prompts['parentCorrect']!, referenceFn: referenceFn(prompts), - }); + }) - expect(metadataCorrect.errors.length).toBe(0); + expect(metadataCorrect.errors.length).toBe(0) const metadataWrong = await scan({ - prompt: prompts["parentWrong"]!, + prompt: prompts['parentWrong']!, referenceFn: referenceFn(prompts), - }); + }) - expect(metadataWrong.errors.length).toBe(1); - }); -}); + expect(metadataWrong.errors.length).toBe(1) + }) +}) -describe("scope tags", async () => { - it("can scan prompts with scope tags", async () => { +describe('scope tags', async () => { + it('can scan prompts with scope tags', async () => { const prompt = removeCommonIndent(` This is the prompt. - `); + `) - const metadata = await scan({ prompt }); + const metadata = await scan({ prompt }) - expect(metadata.parameters).toEqual(new Set()); - }); + expect(metadata.parameters).toEqual(new Set()) + }) - it("Does not add parameters from the scope tag to the parent scope", async () => { + it('Does not add parameters from the scope tag to the parent scope', async () => { const prompt = removeCommonIndent(` {{ foo }} - `); + `) - const metadata = await scan({ prompt }); + const metadata = await scan({ prompt }) - expect(metadata.parameters).toEqual(new Set(["bar"])); - }); + expect(metadata.parameters).toEqual(new Set(['bar'])) + }) - it("returns an error when the scope tag is trying to use a variable that is not defined", async () => { + it('returns an error when the scope tag is trying to use a variable that is not defined', async () => { const prompt = removeCommonIndent(` {{ foo1 }} {{ foo2 }} - `); + `) const metadata = await scan({ prompt: removeCommonIndent(prompt), - }); - - expect(metadata.parameters).toEqual(new Set(["bar1"])); - expect(metadata.errors.length).toBe(1); - expect(metadata.errors[0]).toBeInstanceOf(CompileError); - const error = metadata.errors[0] as CompileError; - expect(error.code).toBe("reference-missing-parameter"); - }); -}); - -describe("syntax errors", async () => { - it("returns CompileErrors when the prompt syntax is invalid", async () => { + }) + + expect(metadata.parameters).toEqual(new Set(['bar1'])) + expect(metadata.errors.length).toBe(1) + expect(metadata.errors[0]).toBeInstanceOf(CompileError) + const error = metadata.errors[0] as CompileError + expect(error.code).toBe('reference-missing-parameter') + }) +}) + +describe('syntax errors', async () => { + it('returns CompileErrors when the prompt syntax is invalid', async () => { const prompt = ` - `; + ` const metadata = await scan({ prompt, - }); + }) - expect(metadata.errors.length).toBe(1); - expect(metadata.errors[0]).toBeInstanceOf(CompileError); - }); + expect(metadata.errors.length).toBe(1) + expect(metadata.errors[0]).toBeInstanceOf(CompileError) + }) - it("finds circular references", async () => { + it('finds circular references', async () => { const prompts = { parent: removeCommonIndent(` This is the parent prompt. @@ -671,19 +670,19 @@ describe("syntax errors", async () => { This is the child prompt. `), - }; + } const metadata = await scan({ - prompt: prompts["parent"]!, + prompt: prompts['parent']!, referenceFn: referenceFn(prompts), - }); + }) - expect(metadata.errors.length).toBe(1); - expect(metadata.errors[0]).toBeInstanceOf(CompileError); - expect(metadata.errors[0]!.code).toBe("circular-reference"); - }); + expect(metadata.errors.length).toBe(1) + expect(metadata.errors[0]).toBeInstanceOf(CompileError) + expect(metadata.errors[0]!.code).toBe('circular-reference') + }) - it("shows errors from referenced prompts as errors in the parent", async () => { + it('shows errors from referenced prompts as errors in the parent', async () => { const prompts = { parent: removeCommonIndent(` This is the parent prompt. @@ -695,35 +694,35 @@ describe("syntax errors", async () => { Error: (close unopened tag) `), - }; + } const metadata = await scan({ - prompt: prompts["parent"]!, + prompt: prompts['parent']!, referenceFn: referenceFn(prompts), - }); + }) - expect(metadata.errors.length).toBe(1); - expect(metadata.errors[0]).toBeInstanceOf(CompileError); - expect(metadata.errors[0]!.code).toBe("reference-error"); + expect(metadata.errors.length).toBe(1) + expect(metadata.errors[0]).toBeInstanceOf(CompileError) + expect(metadata.errors[0]!.code).toBe('reference-error') expect(metadata.errors[0]!.message).contains( - "The referenced prompt contains an error:", - ); + 'The referenced prompt contains an error:', + ) expect(metadata.errors[0]!.message).contains( `Unexpected closing tag for ${TAG_NAMES.message}`, - ); - }); + ) + }) - it("allows message tags inside steps", async () => { + it('allows message tags inside steps', async () => { const prompt = removeCommonIndent(` {{ user_message }} - `); + `) - const metadata = await scan({ prompt }); + const metadata = await scan({ prompt }) - expect(metadata.errors.length).toBe(0); - }); -}); + expect(metadata.errors.length).toBe(0) + }) +}) diff --git a/src/compiler/scan.ts b/src/compiler/scan.ts index 4dc0519..945da0e 100644 --- a/src/compiler/scan.ts +++ b/src/compiler/scan.ts @@ -1,14 +1,14 @@ -import { createHash } from "crypto"; +import { createHash } from 'crypto' import { CUSTOM_MESSAGE_ROLE_ATTR, REFERENCE_DEPTH_LIMIT, REFERENCE_PROMPT_ATTR, TAG_NAMES, -} from "$promptl/constants"; -import CompileError, { error } from "$promptl/error/error"; -import errors from "$promptl/error/errors"; -import parse from "$promptl/parser/index"; +} from '$promptl/constants' +import CompileError, { error } from '$promptl/error/error' +import errors from '$promptl/error/errors' +import parse from '$promptl/parser/index' import type { Attribute, BaseNode, @@ -16,20 +16,20 @@ import type { ElementTag, Fragment, TemplateNode, -} from "$promptl/parser/interfaces"; +} from '$promptl/parser/interfaces' import { Config, ContentTypeTagName, ConversationMetadata, MessageRole, -} from "$promptl/types"; -import { Node as LogicalExpression } from "estree"; -import yaml, { Node as YAMLItem } from "yaml"; -import { z } from "zod"; - -import { updateScopeContextForNode } from "./logic"; -import { ScopeContext } from "./scope"; -import { Document, ReferencePromptFn } from "./types"; +} from '$promptl/types' +import { Node as LogicalExpression } from 'estree' +import yaml, { Node as YAMLItem } from 'yaml' +import { z } from 'zod' + +import { updateScopeContextForNode } from './logic' +import { ScopeContext } from './scope' +import { Document, ReferencePromptFn } from './types' import { findYAMLItemPosition, isChainStepTag, @@ -37,37 +37,37 @@ import { isMessageTag, isRefTag, isScopeTag, -} from "./utils"; +} from './utils' function copyScopeContext(scopeContext: ScopeContext): ScopeContext { return { ...scopeContext, definedVariables: new Set(scopeContext.definedVariables), - }; + } } export class Scan { - includedPromptPaths: Set; + includedPromptPaths: Set - private rawText: string; - private referenceFn?: ReferencePromptFn; - private fullPath: string; - private withParameters?: string[]; - private configSchema?: z.ZodType; + private rawText: string + private referenceFn?: ReferencePromptFn + private fullPath: string + private withParameters?: string[] + private configSchema?: z.ZodType - private config?: Config; - private configPosition?: { start: number; end: number }; - private resolvedPrompt: string; - private resolvedPromptOffset: number = 0; - private hasContent: boolean = false; - private stepTagsCount: number = 0; + private config?: Config + private configPosition?: { start: number; end: number } + private resolvedPrompt: string + private resolvedPromptOffset: number = 0 + private hasContent: boolean = false + private stepTagsCount: number = 0 - private accumulatedToolCalls: ContentTag[] = []; - private errors: CompileError[] = []; + private accumulatedToolCalls: ContentTag[] = [] + private errors: CompileError[] = [] - private references: { [from: string]: string[] } = {}; - private referencedHashes: string[] = []; - private referenceDepth: number = 0; + private references: { [from: string]: string[] } = {} + private referencedHashes: string[] = [] + private referenceDepth: number = 0 constructor({ document, @@ -75,19 +75,19 @@ export class Scan { withParameters, configSchema, }: { - document: Document; - referenceFn?: ReferencePromptFn; - withParameters?: string[]; - configSchema?: z.ZodType; + document: Document + referenceFn?: ReferencePromptFn + withParameters?: string[] + configSchema?: z.ZodType }) { - this.rawText = document.content; - this.referenceFn = referenceFn; - this.fullPath = document.path; - this.withParameters = withParameters; - this.configSchema = configSchema; - - this.resolvedPrompt = document.content; - this.includedPromptPaths = new Set([this.fullPath]); + this.rawText = document.content + this.referenceFn = referenceFn + this.fullPath = document.path + this.withParameters = withParameters + this.configSchema = configSchema + + this.resolvedPrompt = document.content + this.includedPromptPaths = new Set([this.fullPath]) } async run(): Promise { @@ -97,19 +97,19 @@ export class Scan { : undefined, usedUndefinedVariables: new Set(), definedVariables: new Set(), - }; + } - let fragment: Fragment; + let fragment: Fragment try { - fragment = parse(this.rawText); + fragment = parse(this.rawText) } catch (e) { - const parseError = e as CompileError; + const parseError = e as CompileError if (parseError instanceof CompileError) { - this.errors.push(parseError); - fragment = parseError.fragment!; + this.errors.push(parseError) + fragment = parseError.fragment! } else { - throw parseError; + throw parseError } } @@ -120,39 +120,39 @@ export class Scan { isInsideMessageTag: false, isInsideContentTag: false, isRoot: true, - }); + }) if (this.configSchema && !this.config) { - this.baseNodeError(errors.missingConfig, fragment, { start: 0, end: 0 }); + this.baseNodeError(errors.missingConfig, fragment, { start: 0, end: 0 }) } const resolvedPrompt = Object.keys(this.config ?? {}).length > 0 - ? "---\n" + - yaml.stringify(this.config, { indent: 2 }) + - "---\n" + - this.resolvedPrompt - : this.resolvedPrompt; + ? '---\n' + + yaml.stringify(this.config, { indent: 2 }) + + '---\n' + + this.resolvedPrompt + : this.resolvedPrompt const setConfig = (config: Config) => { - const start = this.configPosition?.start ?? 0; - const end = this.configPosition?.end ?? 0; + const start = this.configPosition?.start ?? 0 + const end = this.configPosition?.end ?? 0 if (Object.keys(config).length === 0) { - return this.rawText.slice(0, start) + this.rawText.slice(end); + return this.rawText.slice(0, start) + this.rawText.slice(end) } return ( this.rawText.slice(0, start) + - "---\n" + + '---\n' + yaml.stringify(config, { indent: 2 }) + - "---\n" + + '---\n' + this.rawText.slice(end) - ); - }; + ) + } - const contentToHash = [this.rawText, ...this.referencedHashes].join(""); - const hash = createHash("sha256").update(contentToHash).digest("hex"); + const contentToHash = [this.rawText, ...this.referencedHashes].join('') + const hash = createHash('sha256').update(contentToHash).digest('hex') return { parameters: new Set([ @@ -166,21 +166,21 @@ export class Scan { setConfig, isChain: this.stepTagsCount > 1, includedPromptPaths: this.includedPromptPaths, - }; + } } private async updateScopeContext({ node, scopeContext, }: { - node: LogicalExpression; - scopeContext: ScopeContext; + node: LogicalExpression + scopeContext: ScopeContext }): Promise { await updateScopeContextForNode({ node, scopeContext, raiseError: this.expressionError.bind(this), - }); + }) } private async listTagAttributes({ @@ -188,42 +188,42 @@ export class Scan { scopeContext, literalAttributes = [], // Tags that don't allow Mustache expressions }: { - tagNode: ElementTag; - scopeContext: ScopeContext; - literalAttributes?: string[]; + tagNode: ElementTag + scopeContext: ScopeContext + literalAttributes?: string[] }): Promise> { - const attributeNodes = tagNode.attributes; - if (attributeNodes.length === 0) return new Set(); + const attributeNodes = tagNode.attributes + if (attributeNodes.length === 0) return new Set() - const attributes: Set = new Set(); + const attributes: Set = new Set() for (const attributeNode of attributeNodes) { - const { name, value } = attributeNode; + const { name, value } = attributeNode if (value === true) { - attributes.add(name); - continue; + attributes.add(name) + continue } if (literalAttributes.includes(name)) { - if (value.some((node) => node.type === "MustacheTag")) { + if (value.some((node) => node.type === 'MustacheTag')) { this.baseNodeError( errors.invalidStaticAttribute(name), - value.find((node) => node.type === "MustacheTag")!, - ); - continue; + value.find((node) => node.type === 'MustacheTag')!, + ) + continue } } for await (const node of value) { - if (node.type === "MustacheTag") { - const expression = node.expression; - await this.updateScopeContext({ node: expression, scopeContext }); + if (node.type === 'MustacheTag') { + const expression = node.expression + await this.updateScopeContext({ node: expression, scopeContext }) } } - attributes.add(name); + attributes.add(name) } - return attributes; + return attributes } private async readBaseMetadata({ @@ -234,14 +234,14 @@ export class Scan { isInsideContentTag, isRoot = false, }: { - node: TemplateNode; - scopeContext: ScopeContext; - isInsideStepTag: boolean; - isInsideMessageTag: boolean; - isInsideContentTag: boolean; - isRoot?: boolean; + node: TemplateNode + scopeContext: ScopeContext + isInsideStepTag: boolean + isInsideMessageTag: boolean + isInsideContentTag: boolean + isRoot?: boolean }): Promise { - if (node.type === "Fragment") { + if (node.type === 'Fragment') { for await (const childNode of node.children ?? []) { await this.readBaseMetadata({ node: childNode, @@ -250,106 +250,106 @@ export class Scan { isInsideMessageTag, isInsideContentTag, isRoot, - }); + }) } - return; + return } - if (node.type === "Comment" || node.type === "Config") { + if (node.type === 'Comment' || node.type === 'Config') { /* Remove from the resolved prompt */ - const start = node.start! + this.resolvedPromptOffset; - const end = node.end! + this.resolvedPromptOffset; + const start = node.start! + this.resolvedPromptOffset + const end = node.end! + this.resolvedPromptOffset this.resolvedPrompt = - this.resolvedPrompt.slice(0, start) + this.resolvedPrompt.slice(end); - this.resolvedPromptOffset -= end - start; + this.resolvedPrompt.slice(0, start) + this.resolvedPrompt.slice(end) + this.resolvedPromptOffset -= end - start } - if (node.type === "Config") { + if (node.type === 'Config') { if (this.config) { - this.baseNodeError(errors.configAlreadyDeclared, node); + this.baseNodeError(errors.configAlreadyDeclared, node) } if (!isRoot) { - this.baseNodeError(errors.configOutsideRoot, node); + this.baseNodeError(errors.configOutsideRoot, node) } if (this.hasContent) { - this.baseNodeError(errors.invalidConfigPlacement, node); + this.baseNodeError(errors.invalidConfigPlacement, node) } - this.configPosition = { start: node.start!, end: node.end! }; + this.configPosition = { start: node.start!, end: node.end! } const parsedYaml = yaml.parseDocument(node.value, { keepSourceTokens: true, - }); + }) - const CONFIG_START_OFFSET = 3; // The config is always offsetted by 3 characters due to the `---` + const CONFIG_START_OFFSET = 3 // The config is always offsetted by 3 characters due to the `---` if (parsedYaml.errors.length) { parsedYaml.errors.forEach((error) => { - const [errorStart, errorEnd] = error.pos; + const [errorStart, errorEnd] = error.pos this.baseNodeError(errors.invalidConfig(error.message), node, { start: node.start! + CONFIG_START_OFFSET + errorStart, end: node.start! + CONFIG_START_OFFSET + errorEnd, - }); - }); + }) + }) } - const parsedObj = parsedYaml.toJS() ?? {}; + const parsedObj = parsedYaml.toJS() ?? {} try { - this.configSchema?.parse(parsedObj); + this.configSchema?.parse(parsedObj) } catch (err) { if (err instanceof z.ZodError) { err.errors.forEach((error) => { - const issue = error.message; + const issue = error.message const range = findYAMLItemPosition( parsedYaml.contents as YAMLItem, error.path, - ); + ) const errorStart = range ? node.start! + CONFIG_START_OFFSET + range[0] - : node.start!; + : node.start! const errorEnd = range ? node.start! + CONFIG_START_OFFSET + range[1] + 1 - : node.end!; + : node.end! this.baseNodeError(errors.invalidConfig(issue), node, { start: errorStart, end: errorEnd, - }); - }); + }) + }) } } - this.config = parsedObj; - return; + this.config = parsedObj + return } - if (node.type === "Text") { + if (node.type === 'Text') { if (node.data.trim()) { - this.hasContent = true; + this.hasContent = true } /* do nothing */ - return; + return } - if (node.type === "Comment") { + if (node.type === 'Comment') { /* do nothing */ - return; + return } - if (node.type === "MustacheTag") { - this.hasContent = true; - const expression = node.expression; - await this.updateScopeContext({ node: expression, scopeContext }); - return; + if (node.type === 'MustacheTag') { + this.hasContent = true + const expression = node.expression + await this.updateScopeContext({ node: expression, scopeContext }) + return } - if (node.type === "IfBlock") { - await this.updateScopeContext({ node: node.expression, scopeContext }); - const ifScope = copyScopeContext(scopeContext); - const elseScope = copyScopeContext(scopeContext); + if (node.type === 'IfBlock') { + await this.updateScopeContext({ node: node.expression, scopeContext }) + const ifScope = copyScopeContext(scopeContext) + const elseScope = copyScopeContext(scopeContext) for await (const childNode of node.children ?? []) { await this.readBaseMetadata({ node: childNode, @@ -357,7 +357,7 @@ export class Scan { isInsideStepTag, isInsideMessageTag, isInsideContentTag, - }); + }) } for await (const childNode of node.else?.children ?? []) { await this.readBaseMetadata({ @@ -366,15 +366,15 @@ export class Scan { isInsideStepTag, isInsideMessageTag, isInsideContentTag, - }); + }) } - return; + return } - if (node.type === "ForBlock") { - await this.updateScopeContext({ node: node.expression, scopeContext }); + if (node.type === 'ForBlock') { + await this.updateScopeContext({ node: node.expression, scopeContext }) - const elseScope = copyScopeContext(scopeContext); + const elseScope = copyScopeContext(scopeContext) for await (const childNode of node.else?.children ?? []) { await this.readBaseMetadata({ node: childNode, @@ -382,30 +382,30 @@ export class Scan { isInsideStepTag, isInsideMessageTag, isInsideContentTag, - }); + }) } - const contextVarName = node.context.name; - const indexVarName = node.index?.name; + const contextVarName = node.context.name + const indexVarName = node.index?.name if (scopeContext.definedVariables.has(contextVarName)) { this.expressionError( errors.variableAlreadyDeclared(contextVarName), node.context, - ); - return; + ) + return } if (indexVarName && scopeContext.definedVariables.has(indexVarName)) { this.expressionError( errors.variableAlreadyDeclared(indexVarName), node.index!, - ); - return; + ) + return } - const iterableScope = copyScopeContext(scopeContext); - iterableScope.definedVariables.add(contextVarName); + const iterableScope = copyScopeContext(scopeContext) + iterableScope.definedVariables.add(contextVarName) if (indexVarName) { - iterableScope.definedVariables.add(indexVarName); + iterableScope.definedVariables.add(indexVarName) } for await (const childNode of node.children ?? []) { await this.readBaseMetadata({ @@ -414,33 +414,33 @@ export class Scan { isInsideStepTag, isInsideMessageTag, isInsideContentTag, - }); + }) } - return; + return } - if (node.type === "ElementTag") { - this.hasContent = true; + if (node.type === 'ElementTag') { + this.hasContent = true if (isContentTag(node)) { if (isInsideContentTag) { - this.baseNodeError(errors.contentTagInsideContent, node); + this.baseNodeError(errors.contentTagInsideContent, node) } if (node.name === ContentTypeTagName.toolCall) { - this.accumulatedToolCalls.push(node); + this.accumulatedToolCalls.push(node) const attributes = await this.listTagAttributes({ tagNode: node, scopeContext, - }); + }) - if (!attributes.has("id")) { - this.baseNodeError(errors.toolCallTagWithoutId, node); + if (!attributes.has('id')) { + this.baseNodeError(errors.toolCallTagWithoutId, node) } - if (!attributes.has("name")) { - this.baseNodeError(errors.toolCallWithoutName, node); + if (!attributes.has('name')) { + this.baseNodeError(errors.toolCallWithoutName, node) } } @@ -451,42 +451,42 @@ export class Scan { isInsideStepTag, isInsideMessageTag, isInsideContentTag: true, - }); + }) } - return; + return } if (isMessageTag(node)) { if (isInsideContentTag || isInsideMessageTag) { - this.baseNodeError(errors.messageTagInsideMessage, node); + this.baseNodeError(errors.messageTagInsideMessage, node) } const attributes = await this.listTagAttributes({ tagNode: node, scopeContext, - }); + }) - const role = node.name as MessageRole; + const role = node.name as MessageRole if (node.name === TAG_NAMES.message) { if (!attributes.has(CUSTOM_MESSAGE_ROLE_ATTR)) { - this.baseNodeError(errors.messageTagWithoutRole, node); - return; + this.baseNodeError(errors.messageTagWithoutRole, node) + return } - attributes.delete(CUSTOM_MESSAGE_ROLE_ATTR); + attributes.delete(CUSTOM_MESSAGE_ROLE_ATTR) } - if (role === MessageRole.tool && !attributes.has("id")) { - this.baseNodeError(errors.toolMessageWithoutId, node); - return; + if (role === MessageRole.tool && !attributes.has('id')) { + this.baseNodeError(errors.toolMessageWithoutId, node) + return } if (this.accumulatedToolCalls.length > 0) { this.accumulatedToolCalls.forEach((toolCallNode) => { - this.baseNodeError(errors.invalidToolCallPlacement, toolCallNode); - return; - }); + this.baseNodeError(errors.invalidToolCallPlacement, toolCallNode) + return + }) } - this.accumulatedToolCalls = []; + this.accumulatedToolCalls = [] for await (const childNode of node.children ?? []) { await this.readBaseMetadata({ @@ -495,7 +495,7 @@ export class Scan { isInsideStepTag, isInsideMessageTag: true, isInsideContentTag, - }); + }) } if ( @@ -503,163 +503,163 @@ export class Scan { this.accumulatedToolCalls.length > 0 ) { this.accumulatedToolCalls.forEach((toolCallNode) => { - this.baseNodeError(errors.invalidToolCallPlacement, toolCallNode); - return; - }); + this.baseNodeError(errors.invalidToolCallPlacement, toolCallNode) + return + }) } - this.accumulatedToolCalls = []; - return; + this.accumulatedToolCalls = [] + return } if (isRefTag(node)) { if (node.children?.length ?? 0 > 0) { - this.baseNodeError(errors.referenceTagHasContent, node); - return; + this.baseNodeError(errors.referenceTagHasContent, node) + return } const attributes = await this.listTagAttributes({ tagNode: node, scopeContext, literalAttributes: [REFERENCE_PROMPT_ATTR], - }); + }) if (!attributes.has(REFERENCE_PROMPT_ATTR)) { - this.baseNodeError(errors.referenceTagWithoutPrompt, node); - return; + this.baseNodeError(errors.referenceTagWithoutPrompt, node) + return } if (!this.referenceFn) { - this.baseNodeError(errors.missingReferenceFunction, node); - return; + this.baseNodeError(errors.missingReferenceFunction, node) + return } if (this.referenceDepth > REFERENCE_DEPTH_LIMIT) { - this.baseNodeError(errors.referenceDepthLimit, node); - return; + this.baseNodeError(errors.referenceDepthLimit, node) + return } const refPromptAttribute = node.attributes.find( (attribute: Attribute) => attribute.name === REFERENCE_PROMPT_ATTR, - ) as Attribute; + ) as Attribute const refPromptPath = (refPromptAttribute.value as TemplateNode[]) .map((node) => node.data) - .join(""); + .join('') - attributes.delete(REFERENCE_PROMPT_ATTR); // The rest of the attributes are used as parameters + attributes.delete(REFERENCE_PROMPT_ATTR) // The rest of the attributes are used as parameters - const currentReferences = this.references[this.fullPath] ?? []; + const currentReferences = this.references[this.fullPath] ?? [] - const start = node.start! + this.resolvedPromptOffset; - const end = node.end! + this.resolvedPromptOffset; - let resolvedRefPrompt = this.resolvedPrompt.slice(start, end); + const start = node.start! + this.resolvedPromptOffset + const end = node.end! + this.resolvedPromptOffset + let resolvedRefPrompt = this.resolvedPrompt.slice(start, end) const resolveRef = async () => { if (!this.referenceFn) { - this.baseNodeError(errors.missingReferenceFunction, node); - return; + this.baseNodeError(errors.missingReferenceFunction, node) + return } if (currentReferences.includes(refPromptPath)) { - this.baseNodeError(errors.circularReference, node); - return; + this.baseNodeError(errors.circularReference, node) + return } const refDocument = await this.referenceFn( refPromptPath, this.fullPath, - ); + ) if (!refDocument) { - this.baseNodeError(errors.referenceNotFound, node); - return; + this.baseNodeError(errors.referenceNotFound, node) + return } const refScan = new Scan({ document: refDocument, referenceFn: this.referenceFn, - }); - refScan.accumulatedToolCalls = this.accumulatedToolCalls; + }) + refScan.accumulatedToolCalls = this.accumulatedToolCalls refScan.references = { ...this.references, [this.fullPath]: [...currentReferences, refPromptPath], - }; + } - this.includedPromptPaths.add(refDocument.path); + this.includedPromptPaths.add(refDocument.path) - refScan.referenceDepth = this.referenceDepth + 1; + refScan.referenceDepth = this.referenceDepth + 1 - const refPromptMetadata = await refScan.run(); + const refPromptMetadata = await refScan.run() refPromptMetadata.includedPromptPaths.forEach((path) => { - this.includedPromptPaths.add(path); - }); + this.includedPromptPaths.add(path) + }) refPromptMetadata.parameters.forEach((paramName: string) => { if (!attributes.has(paramName)) { this.baseNodeError( errors.referenceMissingParameter(paramName), node, - ); + ) } - }); + }) refPromptMetadata.errors.forEach((error: CompileError) => { if ( - error.code === "reference-error" || - error.code === "circular-reference" + error.code === 'reference-error' || + error.code === 'circular-reference' ) { this.baseNodeError( { code: error.code, message: error.message }, node, - ); - return; + ) + return } - this.baseNodeError(errors.referenceError(error), node); - }); - this.accumulatedToolCalls = refScan.accumulatedToolCalls; - this.referencedHashes.push(refPromptMetadata.hash); - resolvedRefPrompt = refScan.resolvedPrompt; - }; + this.baseNodeError(errors.referenceError(error), node) + }) + this.accumulatedToolCalls = refScan.accumulatedToolCalls + this.referencedHashes.push(refPromptMetadata.hash) + resolvedRefPrompt = refScan.resolvedPrompt + } try { - await resolveRef(); + await resolveRef() } catch (error: unknown) { - this.baseNodeError(errors.referenceError(error), node); + this.baseNodeError(errors.referenceError(error), node) } - const pretext = this.resolvedPrompt.slice(0, start); - const posttext = this.resolvedPrompt.slice(end); + const pretext = this.resolvedPrompt.slice(0, start) + const posttext = this.resolvedPrompt.slice(end) const attributeTags = node.attributes .filter((a) => a.name !== REFERENCE_PROMPT_ATTR) .map((attr) => { - const attrStart = attr.start! + this.resolvedPromptOffset; - const attrEnd = attr.end! + this.resolvedPromptOffset; - return this.resolvedPrompt.slice(attrStart, attrEnd); - }); + const attrStart = attr.start! + this.resolvedPromptOffset + const attrEnd = attr.end! + this.resolvedPromptOffset + return this.resolvedPrompt.slice(attrStart, attrEnd) + }) const resolvedNode = - `<${TAG_NAMES.scope} ${attributeTags.join(" ")}>` + + `<${TAG_NAMES.scope} ${attributeTags.join(' ')}>` + resolvedRefPrompt + - ``; + `` - this.resolvedPrompt = pretext + resolvedNode + posttext; - this.resolvedPromptOffset += resolvedNode.length - (end - start); + this.resolvedPrompt = pretext + resolvedNode + posttext + this.resolvedPromptOffset += resolvedNode.length - (end - start) - return; + return } if (isScopeTag(node)) { const attributes = await this.listTagAttributes({ tagNode: node, scopeContext, - }); + }) const newScopeContext: ScopeContext = { onlyPredefinedVariables: scopeContext.onlyPredefinedVariables, usedUndefinedVariables: new Set(), definedVariables: attributes, - }; + } for await (const childNode of node.children ?? []) { await this.readBaseMetadata({ @@ -668,32 +668,29 @@ export class Scan { isInsideStepTag, isInsideMessageTag, isInsideContentTag, - }); + }) } newScopeContext.usedUndefinedVariables.forEach((variable) => { if (!attributes.has(variable)) { - this.baseNodeError( - errors.referenceMissingParameter(variable), - node, - ); + this.baseNodeError(errors.referenceMissingParameter(variable), node) } - }); + }) - return; + return } if (isChainStepTag(node)) { - this.stepTagsCount += 1; + this.stepTagsCount += 1 if (isInsideStepTag) { - this.baseNodeError(errors.stepTagInsideStep, node); + this.baseNodeError(errors.stepTagInsideStep, node) } const attributes = await this.listTagAttributes({ tagNode: node, scopeContext, - literalAttributes: ["as", "raw"], - }); + literalAttributes: ['as', 'raw'], + }) for await (const childNode of node.children ?? []) { await this.readBaseMetadata({ @@ -702,35 +699,35 @@ export class Scan { isInsideStepTag: true, isInsideMessageTag, isInsideContentTag, - }); + }) } - if (attributes.has("as")) { - const asAttribute = node.attributes.find((a) => a.name === "as")!; + if (attributes.has('as')) { + const asAttribute = node.attributes.find((a) => a.name === 'as')! if (asAttribute.value !== true) { - const asValue = asAttribute.value.map((n) => n.data).join(""); - scopeContext.definedVariables.add(asValue); + const asValue = asAttribute.value.map((n) => n.data).join('') + scopeContext.definedVariables.add(asValue) } } - if (attributes.has("raw")) { - const rawAttribute = node.attributes.find((a) => a.name === "raw")!; + if (attributes.has('raw')) { + const rawAttribute = node.attributes.find((a) => a.name === 'raw')! if (rawAttribute.value !== true) { - const asValue = rawAttribute.value.map((n) => n.data).join(""); - scopeContext.definedVariables.add(asValue); + const asValue = rawAttribute.value.map((n) => n.data).join('') + scopeContext.definedVariables.add(asValue) } } - return; + return } // Should not be reachable, as non-recognized tags are caught by the parser - this.baseNodeError(errors.unknownTag(node.name), node); - return; + this.baseNodeError(errors.unknownTag(node.name), node) + return } //@ts-ignore - Linter knows this should be unreachable. That's what this error is for. - this.baseNodeError(errors.unsupportedBaseNodeType(node.type), node); + this.baseNodeError(errors.unsupportedBaseNodeType(node.type), node) } private baseNodeError( @@ -740,14 +737,14 @@ export class Scan { ): void { try { error(message, { - name: "CompileError", + name: 'CompileError', code, - source: this.rawText || "", + source: this.rawText || '', start: customPos?.start || node.start || 0, end: customPos?.end || node.end || undefined, - }); + }) } catch (error) { - this.errors.push(error as CompileError); + this.errors.push(error as CompileError) } } @@ -755,28 +752,27 @@ export class Scan { { code, message }: { code: string; message: string }, node: LogicalExpression, ): void { - const source = (node.loc?.source ?? this.rawText)!.split("\n"); + const source = (node.loc?.source ?? this.rawText)!.split('\n') const start = source .slice(0, node.loc?.start.line! - 1) .reduce((acc, line) => acc + line.length + 1, 0) + - node.loc?.start.column!; + node.loc?.start.column! const end = source .slice(0, node.loc?.end.line! - 1) - .reduce((acc, line) => acc + line.length + 1, 0) + - node.loc?.end.column!; + .reduce((acc, line) => acc + line.length + 1, 0) + node.loc?.end.column! try { error(message, { - name: "CompileError", + name: 'CompileError', code, - source: this.rawText || "", + source: this.rawText || '', start, end, - }); + }) } catch (error) { - this.errors.push(error as CompileError); + this.errors.push(error as CompileError) } } } diff --git a/src/compiler/scope.ts b/src/compiler/scope.ts index ab796e5..eb21365 100644 --- a/src/compiler/scope.ts +++ b/src/compiler/scope.ts @@ -22,8 +22,11 @@ export default class Scope { * Local pointers * Every scope has its own local pointers that contains the indexes of the variables in the global stash. */ - private globalStash: ScopeStash = [] // Stash of every variable value in the global scope - private localPointers: ScopePointers = {} // Index of every variable in the stash in the current scope + + // Stash of every variable value in the global scope + private globalStash: ScopeStash = [] + // Index of every variable in the stash in the current scope + private localPointers: ScopePointers = {} constructor(initialState: Record = {}) { for (const [key, value] of Object.entries(initialState)) { @@ -37,27 +40,17 @@ export default class Scope { return scope } - private readFromStash(index: number): unknown { - return this.globalStash[index] - } - - private addToStash(value: unknown): number { - this.globalStash.push(value) - return this.globalStash.length - 1 - } - - private modifyStash(index: number, value: unknown): void { - this.globalStash[index] = value - } - exists(name: string): boolean { return name in this.localPointers } get(name: string): unknown { const index = this.localPointers[name] ?? undefined - if (index === undefined) + + if (index === undefined) { throw new Error(`Variable '${name}' does not exist`) + } + return this.readFromStash(index) } @@ -88,10 +81,37 @@ export default class Scope { setPointers(pointers: ScopePointers): void { this.localPointers = pointers } + + setStash(stash: ScopeStash): void { + this.globalStash = stash + } + + serialize(): { stash: ScopeStash; pointers: ScopePointers } { + return { + stash: this.globalStash, + pointers: this.localPointers, + } + } + + private readFromStash(index: number): unknown { + return this.globalStash[index] + } + + private addToStash(value: unknown): number { + this.globalStash.push(value) + return this.globalStash.length - 1 + } + + private modifyStash(index: number, value: unknown): void { + this.globalStash[index] = value + } } export type ScopeContext = { - onlyPredefinedVariables?: Set // If defined, all usedUndefinedVariables that are not in this set will return an error - usedUndefinedVariables: Set // Variables that are not in current scope but have been used - definedVariables: Set // Variables that are in current scope + // If defined, all usedUndefinedVariables that are not in this set will return an error + onlyPredefinedVariables?: Set + // Variables that are not in current scope but have been used + usedUndefinedVariables: Set + // Variables that are in current scope + definedVariables: Set } diff --git a/src/compiler/test/helpers.ts b/src/compiler/test/helpers.ts index cc7bd67..25f2231 100644 --- a/src/compiler/test/helpers.ts +++ b/src/compiler/test/helpers.ts @@ -38,21 +38,23 @@ export async function complete({ steps: number }> { let steps = 0 - let responseMessage: Omit | undefined + while (true) { - const { completed, messages, config } = - await chain.step(responseMessage) + const { completed, messages, config } = await chain.step(responseMessage) - if (completed) + if (completed) { return { messages, config, steps, response: responseMessage!.content as MessageContent[], } + } - const response = callback ? await callback({ messages, config }) : 'RESPONSE' + const response = callback + ? await callback({ messages, config }) + : 'RESPONSE' responseMessage = { content: [{ type: ContentType.text, text: response }] } steps++ diff --git a/src/compiler/utils.ts b/src/compiler/utils.ts index 3a7bab1..ea31e0e 100644 --- a/src/compiler/utils.ts +++ b/src/compiler/utils.ts @@ -22,17 +22,20 @@ export async function hasContent(iterable: Iterable) { } export function getCommonIndent(text: string): number { - return text.split('\n').reduce((acc: number | null, line: string) => { - if (line.trim() === '') return acc - const indent = line.match(/^\s*/)![0] - if (acc === null) return indent.length - return indent.length < acc ? indent.length : acc - }, null) ?? 0 + return ( + text.split('\n').reduce((acc: number | null, line: string) => { + if (line.trim() === '') return acc + const indent = line.match(/^\s*/)![0] + if (acc === null) return indent.length + return indent.length < acc ? indent.length : acc + }, null) ?? 0 + ) } export function removeCommonIndent(text: string): string { const indent = getCommonIndent(text) - return text.split('\n') + return text + .split('\n') .map((line) => line.slice(indent)) .join('\n') .trim() diff --git a/src/parser/parser.test.ts b/src/parser/parser.test.ts index 90984c1..a6b50a9 100644 --- a/src/parser/parser.test.ts +++ b/src/parser/parser.test.ts @@ -115,12 +115,12 @@ describe('Tags', async () => { it('parses tags with unknown tag names as plain text even if the name starts like a known tag', async () => { const fragment1 = parse('') const fragment2 = parse('') - + expect(fragment1.children.length).toBe(1) const node1 = fragment1.children[0]! expect(node1.type).toBe('Text') expect(node1.data).toBe('') - + expect(fragment2.children.length).toBe(1) const node2 = fragment2.children[0]! expect(node2.type).toBe('Text') diff --git a/src/providers/adapter.ts b/src/providers/adapter.ts index 28f37c2..514ec19 100644 --- a/src/providers/adapter.ts +++ b/src/providers/adapter.ts @@ -1,3 +1,4 @@ +import { AdapterKey } from '$promptl/providers' import { Message, Conversation as PromptlConversation } from '$promptl/types' export type ProviderConversation = { @@ -6,11 +7,13 @@ export type ProviderConversation = { } export type ProviderAdapter = { + type: AdapterKey toPromptl(conversation: ProviderConversation): PromptlConversation fromPromptl(conversation: PromptlConversation): ProviderConversation } export const defaultAdapter: ProviderAdapter = { + type: 'default', toPromptl: (c) => c, fromPromptl: (c) => c, } diff --git a/src/providers/anthropic/adapter.ts b/src/providers/anthropic/adapter.ts index e67d23a..8b9797a 100644 --- a/src/providers/anthropic/adapter.ts +++ b/src/providers/anthropic/adapter.ts @@ -26,6 +26,7 @@ import { } from './types' export const AnthropicAdapter: ProviderAdapter = { + type: 'anthropic', fromPromptl( promptlConversation: PromptlConversation, ): ProviderConversation { @@ -158,11 +159,11 @@ function toAnthropicFile( ...rest, type: AnthropicContentType.document, source: { - type: 'base64', + type: 'base64', media_type: mimeType, data: file.toString('base64'), }, - } + } } return { diff --git a/src/providers/index.ts b/src/providers/index.ts index 24c208b..e8311c5 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -10,6 +10,7 @@ export const Adapters = { anthropic: AnthropicAdapter, } as const +export type AdapterKey = keyof typeof Adapters export type AdapterMessageType< T extends keyof typeof Adapters = keyof typeof Adapters, > = ReturnType<(typeof Adapters)[T]['fromPromptl']>['messages'][number] diff --git a/src/providers/openai/adapter.ts b/src/providers/openai/adapter.ts index 6b4abf1..892aa52 100644 --- a/src/providers/openai/adapter.ts +++ b/src/providers/openai/adapter.ts @@ -23,6 +23,7 @@ import { } from './types' export const OpenAIAdapter: ProviderAdapter = { + type: 'openai', fromPromptl( promptlConversation: PromptlConversation, ): ProviderConversation { @@ -54,7 +55,7 @@ function toOpenAiFile( type: OpenAIContentType.input_audio, data: file.toString('base64'), format: mimeType.split('/').at(-1)!, - } + } } return { diff --git a/src/types/index.ts b/src/types/index.ts index 2064179..6e13828 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -20,4 +20,5 @@ export type ConversationMetadata = { includedPromptPaths: Set } +export { type SerializedChain } from '$promptl/compiler' export * from './message' diff --git a/src/types/message.ts b/src/types/message.ts index fd18956..204f2a0 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -50,7 +50,11 @@ export type ToolCallContent = { toolArguments: Record } -export type MessageContent = TextContent | ImageContent | FileContent | ToolCallContent +export type MessageContent = + | TextContent + | ImageContent + | FileContent + | ToolCallContent /* Message */