An educational pure functional programming library in TypeScript
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add docs site, replace loops with functional patterns, add nullability guards and stricter tsconfig

+4251 -571
+1
.gitignore
··· 25 25 # caches 26 26 .eslintcache 27 27 .cache 28 + .astro 28 29 *.tsbuildinfo 29 30 30 31 # IntelliJ based IDEs
+71
docs-site/astro.config.ts
··· 1 + import { defineConfig } from "astro/config"; 2 + import starlight from "@astrojs/starlight"; 3 + 4 + export default defineConfig({ 5 + integrations: [ 6 + starlight({ 7 + title: "purus-ts", 8 + description: 9 + "Pure functional programming in TypeScript — effects, fibers, branded types, and more.", 10 + social: [ 11 + { 12 + icon: "github", 13 + label: "GitHub", 14 + href: "https://tangled.sh/oleksify.me/purus-ts", 15 + }, 16 + ], 17 + customCss: ["./src/styles/custom.css", "./src/styles/stories.css"], 18 + sidebar: [ 19 + { 20 + label: "Tutorial", 21 + items: [ 22 + { slug: "tutorial" }, 23 + { 24 + label: "Chapters", 25 + autogenerate: { directory: "tutorial", collapsed: false }, 26 + }, 27 + ], 28 + }, 29 + { 30 + label: "Concepts", 31 + collapsed: true, 32 + items: [ 33 + { slug: "concepts" }, 34 + { 35 + label: "Articles", 36 + autogenerate: { directory: "concepts", collapsed: true }, 37 + }, 38 + ], 39 + }, 40 + { 41 + label: "Stories", 42 + items: [ 43 + { slug: "stories" }, 44 + { 45 + label: "The Forest Election", 46 + items: [ 47 + { slug: "stories/forest-election" }, 48 + "stories/forest-election/01-the-ballot-box-problem", 49 + "stories/forest-election/02-counting-day", 50 + "stories/forest-election/03-the-announcement", 51 + ], 52 + }, 53 + { 54 + label: "Beaver's Big System", 55 + items: [ 56 + { slug: "stories/beavers-big-system" }, 57 + "stories/beavers-big-system/01-the-mixup", 58 + "stories/beavers-big-system/02-the-categorization", 59 + "stories/beavers-big-system/03-the-queue", 60 + ], 61 + }, 62 + ], 63 + }, 64 + { 65 + label: "Examples", 66 + autogenerate: { directory: "examples" }, 67 + }, 68 + ], 69 + }), 70 + ], 71 + });
+931
docs-site/bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "configVersion": 1, 4 + "workspaces": { 5 + "": { 6 + "name": "purus-ts-docs", 7 + "dependencies": { 8 + "@astrojs/starlight": "^0.34.0", 9 + "astro": "^5.0.0", 10 + "purus-ts": "file:../", 11 + }, 12 + }, 13 + }, 14 + "packages": { 15 + "@astrojs/compiler": ["@astrojs/compiler@2.13.1", "", {}, "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg=="], 16 + 17 + "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.5", "", {}, "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA=="], 18 + 19 + "@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.10", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.5", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.19.0", "smol-toml": "^1.5.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-kk4HeYR6AcnzC4QV8iSlOfh+N8TZ3MEStxPyenyCtemqn8IpEATBFMTJcfrNW32dgpt6MY3oCkMM/Tv3/I4G3A=="], 20 + 21 + "@astrojs/mdx": ["@astrojs/mdx@4.3.13", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.10", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-IHDHVKz0JfKBy3//52JSiyWv089b7GVSChIXLrlUOoTLWowG3wr2/8hkaEgEyd/vysvNQvGk+QhysXpJW5ve6Q=="], 22 + 23 + "@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], 24 + 25 + "@astrojs/sitemap": ["@astrojs/sitemap@3.7.0", "", { "dependencies": { "sitemap": "^8.0.2", "stream-replace-string": "^2.0.0", "zod": "^3.25.76" } }, "sha512-+qxjUrz6Jcgh+D5VE1gKUJTA3pSthuPHe6Ao5JCxok794Lewx8hBFaWHtOnN0ntb2lfOf7gvOi9TefUswQ/ZVA=="], 26 + 27 + "@astrojs/starlight": ["@astrojs/starlight@0.34.8", "", { "dependencies": { "@astrojs/markdown-remark": "^6.3.1", "@astrojs/mdx": "^4.2.3", "@astrojs/sitemap": "^3.3.0", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", "astro-expressive-code": "^0.41.1", "bcp-47": "^2.1.0", "hast-util-from-html": "^2.0.1", "hast-util-select": "^6.0.2", "hast-util-to-string": "^3.0.0", "hastscript": "^9.0.0", "i18next": "^23.11.5", "js-yaml": "^4.1.0", "klona": "^2.0.6", "mdast-util-directive": "^3.0.0", "mdast-util-to-markdown": "^2.1.0", "mdast-util-to-string": "^4.0.0", "pagefind": "^1.3.0", "rehype": "^13.0.1", "rehype-format": "^5.0.0", "remark-directive": "^3.0.0", "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "vfile": "^6.0.2" }, "peerDependencies": { "astro": "^5.5.0" } }, "sha512-XuYz0TfCZhje2u1Q9FNtmTdm7/B9QP91RDI1VkPgYvDhSYlME3k8gwgcBMHnR9ASDo2p9gskrqe7t1Pub/qryg=="], 28 + 29 + "@astrojs/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="], 30 + 31 + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], 32 + 33 + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], 34 + 35 + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], 36 + 37 + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], 38 + 39 + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], 40 + 41 + "@biomejs/biome": ["@biomejs/biome@2.3.14", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.14", "@biomejs/cli-darwin-x64": "2.3.14", "@biomejs/cli-linux-arm64": "2.3.14", "@biomejs/cli-linux-arm64-musl": "2.3.14", "@biomejs/cli-linux-x64": "2.3.14", "@biomejs/cli-linux-x64-musl": "2.3.14", "@biomejs/cli-win32-arm64": "2.3.14", "@biomejs/cli-win32-x64": "2.3.14" }, "bin": { "biome": "bin/biome" } }, "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA=="], 42 + 43 + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A=="], 44 + 45 + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA=="], 46 + 47 + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ=="], 48 + 49 + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg=="], 50 + 51 + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA=="], 52 + 53 + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ=="], 54 + 55 + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A=="], 56 + 57 + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.14", "", { "os": "win32", "cpu": "x64" }, "sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ=="], 58 + 59 + "@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="], 60 + 61 + "@ctrl/tinycolor": ["@ctrl/tinycolor@4.2.0", "", {}, "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A=="], 62 + 63 + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], 64 + 65 + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 66 + 67 + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], 68 + 69 + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], 70 + 71 + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], 72 + 73 + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], 74 + 75 + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], 76 + 77 + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], 78 + 79 + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], 80 + 81 + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], 82 + 83 + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], 84 + 85 + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], 86 + 87 + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], 88 + 89 + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], 90 + 91 + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], 92 + 93 + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], 94 + 95 + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], 96 + 97 + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], 98 + 99 + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], 100 + 101 + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], 102 + 103 + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], 104 + 105 + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], 106 + 107 + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], 108 + 109 + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], 110 + 111 + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], 112 + 113 + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], 114 + 115 + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 116 + 117 + "@expressive-code/core": ["@expressive-code/core@0.41.6", "", { "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-FvJQP+hG0jWi/FLBSmvHInDqWR7jNANp9PUDjdMqSshHb0y7sxx3vHuoOr6SgXjWw+MGLqorZyPQ0aAlHEok6g=="], 118 + 119 + "@expressive-code/plugin-frames": ["@expressive-code/plugin-frames@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6" } }, "sha512-d+hkSYXIQot6fmYnOmWAM+7TNWRv/dhfjMsNq+mIZz8Tb4mPHOcgcfZeEM5dV9TDL0ioQNvtcqQNuzA1sRPjxg=="], 120 + 121 + "@expressive-code/plugin-shiki": ["@expressive-code/plugin-shiki@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6", "shiki": "^3.2.2" } }, "sha512-Y6zmKBmsIUtWTzdefqlzm/h9Zz0Rc4gNdt2GTIH7fhHH2I9+lDYCa27BDwuBhjqcos6uK81Aca9dLUC4wzN+ng=="], 122 + 123 + "@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6" } }, "sha512-PBFa1wGyYzRExMDzBmAWC6/kdfG1oLn4pLpBeTfIRrALPjcGA/59HP3e7q9J0Smk4pC7U+lWkA2LHR8FYV8U7Q=="], 124 + 125 + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], 126 + 127 + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], 128 + 129 + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], 130 + 131 + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], 132 + 133 + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], 134 + 135 + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], 136 + 137 + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], 138 + 139 + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], 140 + 141 + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], 142 + 143 + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], 144 + 145 + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], 146 + 147 + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], 148 + 149 + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], 150 + 151 + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], 152 + 153 + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], 154 + 155 + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], 156 + 157 + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], 158 + 159 + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], 160 + 161 + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], 162 + 163 + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], 164 + 165 + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], 166 + 167 + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], 168 + 169 + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], 170 + 171 + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], 172 + 173 + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], 174 + 175 + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], 176 + 177 + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], 178 + 179 + "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], 180 + 181 + "@pagefind/darwin-arm64": ["@pagefind/darwin-arm64@1.4.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ=="], 182 + 183 + "@pagefind/darwin-x64": ["@pagefind/darwin-x64@1.4.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-e7JPIS6L9/cJfow+/IAqknsGqEPjJnVXGjpGm25bnq+NPdoD3c/7fAwr1OXkG4Ocjx6ZGSCijXEV4ryMcH2E3A=="], 184 + 185 + "@pagefind/default-ui": ["@pagefind/default-ui@1.4.0", "", {}, "sha512-wie82VWn3cnGEdIjh4YwNESyS1G6vRHwL6cNjy9CFgNnWW/PGRjsLq300xjVH5sfPFK3iK36UxvIBymtQIEiSQ=="], 186 + 187 + "@pagefind/freebsd-x64": ["@pagefind/freebsd-x64@1.4.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WcJVypXSZ+9HpiqZjFXMUobfFfZZ6NzIYtkhQ9eOhZrQpeY5uQFqNWLCk7w9RkMUwBv1HAMDW3YJQl/8OqsV0Q=="], 188 + 189 + "@pagefind/linux-arm64": ["@pagefind/linux-arm64@1.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-PIt8dkqt4W06KGmQjONw7EZbhDF+uXI7i0XtRLN1vjCUxM9vGPdtJc2mUyVPevjomrGz5M86M8bqTr6cgDp1Uw=="], 190 + 191 + "@pagefind/linux-x64": ["@pagefind/linux-x64@1.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-z4oddcWwQ0UHrTHR8psLnVlz6USGJ/eOlDPTDYZ4cI8TK8PgwRUPQZp9D2iJPNIPcS6Qx/E4TebjuGJOyK8Mmg=="], 192 + 193 + "@pagefind/windows-x64": ["@pagefind/windows-x64@1.4.0", "", { "os": "win32", "cpu": "x64" }, "sha512-NkT+YAdgS2FPCn8mIA9bQhiBs+xmniMGq1LFPDhcFn0+2yIUEiIG06t7bsZlhdjknEQRTSdT7YitP6fC5qwP0g=="], 194 + 195 + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], 196 + 197 + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], 198 + 199 + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="], 200 + 201 + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="], 202 + 203 + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="], 204 + 205 + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="], 206 + 207 + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="], 208 + 209 + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="], 210 + 211 + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="], 212 + 213 + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="], 214 + 215 + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="], 216 + 217 + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="], 218 + 219 + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="], 220 + 221 + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="], 222 + 223 + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="], 224 + 225 + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="], 226 + 227 + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="], 228 + 229 + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="], 230 + 231 + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="], 232 + 233 + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="], 234 + 235 + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="], 236 + 237 + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="], 238 + 239 + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="], 240 + 241 + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="], 242 + 243 + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="], 244 + 245 + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], 246 + 247 + "@shikijs/core": ["@shikijs/core@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA=="], 248 + 249 + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw=="], 250 + 251 + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA=="], 252 + 253 + "@shikijs/langs": ["@shikijs/langs@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA=="], 254 + 255 + "@shikijs/themes": ["@shikijs/themes@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g=="], 256 + 257 + "@shikijs/types": ["@shikijs/types@3.22.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg=="], 258 + 259 + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], 260 + 261 + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], 262 + 263 + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], 264 + 265 + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 266 + 267 + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], 268 + 269 + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], 270 + 271 + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], 272 + 273 + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], 274 + 275 + "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], 276 + 277 + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], 278 + 279 + "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], 280 + 281 + "@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="], 282 + 283 + "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], 284 + 285 + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], 286 + 287 + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], 288 + 289 + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], 290 + 291 + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], 292 + 293 + "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], 294 + 295 + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], 296 + 297 + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], 298 + 299 + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], 300 + 301 + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], 302 + 303 + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], 304 + 305 + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], 306 + 307 + "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], 308 + 309 + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], 310 + 311 + "astro": ["astro@5.17.1", "", { "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/internal-helpers": "0.7.5", "@astrojs/markdown-remark": "6.3.10", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^4.0.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "acorn": "^8.15.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.3.1", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.1.1", "cssesc": "^3.0.0", "debug": "^4.4.3", "deterministic-object-hash": "^2.0.2", "devalue": "^5.6.2", "diff": "^8.0.3", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.4.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "magic-string": "^0.30.21", "magicast": "^0.5.1", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.1", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.3", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.3", "shiki": "^3.21.0", "smol-toml": "^1.6.0", "svgo": "^4.0.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.7.3", "unist-util-visit": "^5.0.0", "unstorage": "^1.17.4", "vfile": "^6.0.3", "vite": "^6.4.1", "vitefu": "^1.1.1", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.3", "zod": "^3.25.76", "zod-to-json-schema": "^3.25.1", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "astro.js" } }, "sha512-oD3tlxTaVWGq/Wfbqk6gxzVRz98xa/rYlpe+gU2jXJMSD01k6sEDL01ZlT8mVSYB/rMgnvIOfiQQ3BbLdN237A=="], 312 + 313 + "astro-expressive-code": ["astro-expressive-code@0.41.6", "", { "dependencies": { "rehype-expressive-code": "^0.41.6" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" } }, "sha512-l47tb1uhmVIebHUkw+HEPtU/av0G4O8Q34g2cbkPvC7/e9ZhANcjUUciKt9Hp6gSVDdIuXBBLwJQn2LkeGMOAw=="], 314 + 315 + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], 316 + 317 + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], 318 + 319 + "base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="], 320 + 321 + "bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="], 322 + 323 + "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], 324 + 325 + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], 326 + 327 + "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], 328 + 329 + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], 330 + 331 + "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], 332 + 333 + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], 334 + 335 + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], 336 + 337 + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], 338 + 339 + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], 340 + 341 + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], 342 + 343 + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], 344 + 345 + "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], 346 + 347 + "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], 348 + 349 + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], 350 + 351 + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], 352 + 353 + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], 354 + 355 + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], 356 + 357 + "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], 358 + 359 + "common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], 360 + 361 + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], 362 + 363 + "cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], 364 + 365 + "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], 366 + 367 + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], 368 + 369 + "css-selector-parser": ["css-selector-parser@3.3.0", "", {}, "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g=="], 370 + 371 + "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="], 372 + 373 + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], 374 + 375 + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], 376 + 377 + "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], 378 + 379 + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 380 + 381 + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], 382 + 383 + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], 384 + 385 + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], 386 + 387 + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], 388 + 389 + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 390 + 391 + "deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="], 392 + 393 + "devalue": ["devalue@5.6.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="], 394 + 395 + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], 396 + 397 + "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], 398 + 399 + "direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="], 400 + 401 + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], 402 + 403 + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], 404 + 405 + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], 406 + 407 + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], 408 + 409 + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], 410 + 411 + "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], 412 + 413 + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], 414 + 415 + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], 416 + 417 + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], 418 + 419 + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], 420 + 421 + "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], 422 + 423 + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 424 + 425 + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], 426 + 427 + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], 428 + 429 + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], 430 + 431 + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], 432 + 433 + "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], 434 + 435 + "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], 436 + 437 + "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], 438 + 439 + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], 440 + 441 + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], 442 + 443 + "expressive-code": ["expressive-code@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6", "@expressive-code/plugin-frames": "^0.41.6", "@expressive-code/plugin-shiki": "^0.41.6", "@expressive-code/plugin-text-markers": "^0.41.6" } }, "sha512-W/5+IQbrpCIM5KGLjO35wlp1NCwDOOVQb+PAvzEoGkW1xjGM807ZGfBKptNWH6UECvt6qgmLyWolCMYKh7eQmA=="], 444 + 445 + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], 446 + 447 + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 448 + 449 + "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], 450 + 451 + "fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="], 452 + 453 + "fontkitten": ["fontkitten@1.0.2", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-piJxbLnkD9Xcyi7dWJRnqszEURixe7CrF/efBfbffe2DPyabmuIuqraruY8cXTs19QoM8VJzx47BDRVNXETM7Q=="], 454 + 455 + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 456 + 457 + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], 458 + 459 + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], 460 + 461 + "h3": ["h3@1.15.5", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg=="], 462 + 463 + "hast-util-embedded": ["hast-util-embedded@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA=="], 464 + 465 + "hast-util-format": ["hast-util-format@1.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-whitespace": "^3.0.0", "html-whitespace-sensitive-tag-names": "^3.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA=="], 466 + 467 + "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], 468 + 469 + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], 470 + 471 + "hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="], 472 + 473 + "hast-util-is-body-ok-link": ["hast-util-is-body-ok-link@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ=="], 474 + 475 + "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], 476 + 477 + "hast-util-minify-whitespace": ["hast-util-minify-whitespace@1.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-is-element": "^3.0.0", "hast-util-whitespace": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw=="], 478 + 479 + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], 480 + 481 + "hast-util-phrasing": ["hast-util-phrasing@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-has-property": "^3.0.0", "hast-util-is-body-ok-link": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ=="], 482 + 483 + "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], 484 + 485 + "hast-util-select": ["hast-util-select@6.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "bcp-47-match": "^2.0.0", "comma-separated-tokens": "^2.0.0", "css-selector-parser": "^3.0.0", "devlop": "^1.0.0", "direction": "^2.0.0", "hast-util-has-property": "^3.0.0", "hast-util-to-string": "^3.0.0", "hast-util-whitespace": "^3.0.0", "nth-check": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw=="], 486 + 487 + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], 488 + 489 + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], 490 + 491 + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], 492 + 493 + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], 494 + 495 + "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], 496 + 497 + "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], 498 + 499 + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], 500 + 501 + "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], 502 + 503 + "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], 504 + 505 + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], 506 + 507 + "html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="], 508 + 509 + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], 510 + 511 + "i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="], 512 + 513 + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], 514 + 515 + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], 516 + 517 + "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], 518 + 519 + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], 520 + 521 + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], 522 + 523 + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], 524 + 525 + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], 526 + 527 + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], 528 + 529 + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], 530 + 531 + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], 532 + 533 + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], 534 + 535 + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], 536 + 537 + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], 538 + 539 + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], 540 + 541 + "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], 542 + 543 + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], 544 + 545 + "lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="], 546 + 547 + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], 548 + 549 + "magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="], 550 + 551 + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], 552 + 553 + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], 554 + 555 + "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="], 556 + 557 + "mdast-util-directive": ["mdast-util-directive@3.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q=="], 558 + 559 + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], 560 + 561 + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], 562 + 563 + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], 564 + 565 + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], 566 + 567 + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], 568 + 569 + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], 570 + 571 + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], 572 + 573 + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], 574 + 575 + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], 576 + 577 + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], 578 + 579 + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], 580 + 581 + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], 582 + 583 + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], 584 + 585 + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], 586 + 587 + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], 588 + 589 + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], 590 + 591 + "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], 592 + 593 + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], 594 + 595 + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], 596 + 597 + "micromark-extension-directive": ["micromark-extension-directive@3.0.2", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA=="], 598 + 599 + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], 600 + 601 + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], 602 + 603 + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], 604 + 605 + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], 606 + 607 + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], 608 + 609 + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], 610 + 611 + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], 612 + 613 + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], 614 + 615 + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], 616 + 617 + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], 618 + 619 + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], 620 + 621 + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], 622 + 623 + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], 624 + 625 + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], 626 + 627 + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], 628 + 629 + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], 630 + 631 + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], 632 + 633 + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], 634 + 635 + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], 636 + 637 + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], 638 + 639 + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], 640 + 641 + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], 642 + 643 + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], 644 + 645 + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], 646 + 647 + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], 648 + 649 + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], 650 + 651 + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], 652 + 653 + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], 654 + 655 + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], 656 + 657 + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], 658 + 659 + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], 660 + 661 + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], 662 + 663 + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], 664 + 665 + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], 666 + 667 + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 668 + 669 + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 670 + 671 + "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], 672 + 673 + "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], 674 + 675 + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], 676 + 677 + "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], 678 + 679 + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], 680 + 681 + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], 682 + 683 + "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], 684 + 685 + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], 686 + 687 + "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], 688 + 689 + "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], 690 + 691 + "p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="], 692 + 693 + "p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="], 694 + 695 + "p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], 696 + 697 + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], 698 + 699 + "pagefind": ["pagefind@1.4.0", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.4.0", "@pagefind/darwin-x64": "1.4.0", "@pagefind/freebsd-x64": "1.4.0", "@pagefind/linux-arm64": "1.4.0", "@pagefind/linux-x64": "1.4.0", "@pagefind/windows-x64": "1.4.0" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g=="], 700 + 701 + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], 702 + 703 + "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], 704 + 705 + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], 706 + 707 + "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], 708 + 709 + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 710 + 711 + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 712 + 713 + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], 714 + 715 + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], 716 + 717 + "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], 718 + 719 + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], 720 + 721 + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], 722 + 723 + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], 724 + 725 + "purus-ts": ["purus-ts@file:..", { "devDependencies": { "@biomejs/biome": "^2.3.12", "@types/bun": "^1.1.0", "typescript": "^5" } }], 726 + 727 + "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], 728 + 729 + "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], 730 + 731 + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], 732 + 733 + "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], 734 + 735 + "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], 736 + 737 + "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], 738 + 739 + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], 740 + 741 + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], 742 + 743 + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], 744 + 745 + "rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="], 746 + 747 + "rehype-expressive-code": ["rehype-expressive-code@0.41.6", "", { "dependencies": { "expressive-code": "^0.41.6" } }, "sha512-aBMX8kxPtjmDSFUdZlAWJkMvsQ4ZMASfee90JWIAV8tweltXLzkWC3q++43ToTelI8ac5iC0B3/S/Cl4Ql1y2g=="], 748 + 749 + "rehype-format": ["rehype-format@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-format": "^1.0.0" } }, "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ=="], 750 + 751 + "rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="], 752 + 753 + "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], 754 + 755 + "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], 756 + 757 + "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="], 758 + 759 + "remark-directive": ["remark-directive@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^3.0.0", "unified": "^11.0.0" } }, "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A=="], 760 + 761 + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], 762 + 763 + "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], 764 + 765 + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], 766 + 767 + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], 768 + 769 + "remark-smartypants": ["remark-smartypants@3.0.2", "", { "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA=="], 770 + 771 + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], 772 + 773 + "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], 774 + 775 + "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="], 776 + 777 + "retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="], 778 + 779 + "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="], 780 + 781 + "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], 782 + 783 + "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], 784 + 785 + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], 786 + 787 + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], 788 + 789 + "shiki": ["shiki@3.22.0", "", { "dependencies": { "@shikijs/core": "3.22.0", "@shikijs/engine-javascript": "3.22.0", "@shikijs/engine-oniguruma": "3.22.0", "@shikijs/langs": "3.22.0", "@shikijs/themes": "3.22.0", "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g=="], 790 + 791 + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], 792 + 793 + "sitemap": ["sitemap@8.0.2", "", { "dependencies": { "@types/node": "^17.0.5", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/cli.js" } }, "sha512-LwktpJcyZDoa0IL6KT++lQ53pbSrx2c9ge41/SeLTyqy2XUNA6uR4+P9u5IVo5lPeL2arAcOKn1aZAxoYbCKlQ=="], 794 + 795 + "smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="], 796 + 797 + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], 798 + 799 + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 800 + 801 + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], 802 + 803 + "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], 804 + 805 + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], 806 + 807 + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], 808 + 809 + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], 810 + 811 + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], 812 + 813 + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], 814 + 815 + "svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="], 816 + 817 + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], 818 + 819 + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], 820 + 821 + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], 822 + 823 + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], 824 + 825 + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], 826 + 827 + "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], 828 + 829 + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 830 + 831 + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], 832 + 833 + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 834 + 835 + "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], 836 + 837 + "ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="], 838 + 839 + "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], 840 + 841 + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 842 + 843 + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], 844 + 845 + "unifont": ["unifont@0.7.3", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-b0GtQzKCyuSHGsfj5vyN8st7muZ6VCI4XD4vFlr7Uy1rlWVYxC3npnfk8MyreHxJYrz1ooLDqDzFe9XqQTlAhA=="], 846 + 847 + "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], 848 + 849 + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], 850 + 851 + "unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="], 852 + 853 + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], 854 + 855 + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], 856 + 857 + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], 858 + 859 + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], 860 + 861 + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], 862 + 863 + "unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="], 864 + 865 + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], 866 + 867 + "unstorage": ["unstorage@1.17.4", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.5", "lru-cache": "^11.2.0", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw=="], 868 + 869 + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], 870 + 871 + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], 872 + 873 + "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], 874 + 875 + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], 876 + 877 + "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], 878 + 879 + "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], 880 + 881 + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], 882 + 883 + "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], 884 + 885 + "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], 886 + 887 + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], 888 + 889 + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], 890 + 891 + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], 892 + 893 + "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], 894 + 895 + "yocto-spinner": ["yocto-spinner@0.2.3", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ=="], 896 + 897 + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], 898 + 899 + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 900 + 901 + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], 902 + 903 + "zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="], 904 + 905 + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], 906 + 907 + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], 908 + 909 + "@types/sax/@types/node": ["@types/node@25.2.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg=="], 910 + 911 + "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 912 + 913 + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 914 + 915 + "bun-types/@types/node": ["@types/node@25.2.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg=="], 916 + 917 + "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], 918 + 919 + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], 920 + 921 + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], 922 + 923 + "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], 924 + 925 + "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 926 + 927 + "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], 928 + 929 + "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 930 + } 931 + }
+17
docs-site/package.json
··· 1 + { 2 + "name": "purus-ts-docs", 3 + "version": "0.0.1", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "dev": "astro dev", 8 + "build": "bun run validate && astro build", 9 + "preview": "astro preview", 10 + "validate": "bun run scripts/validate-content.ts" 11 + }, 12 + "dependencies": { 13 + "@astrojs/starlight": "^0.34.0", 14 + "astro": "^5.0.0", 15 + "purus-ts": "file:../" 16 + } 17 + }
+3
docs-site/public/favicon.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"> 2 + <text x="2" y="30" font-size="30">&#x03BB;</text> 3 + </svg>
+189
docs-site/scripts/validate-content.ts
··· 1 + /** 2 + * Content validation script for purus-ts docs. 3 + * 4 + * Dogfoods purus-ts: 5 + * - Branded<string, "FilePath"> for type-safe file paths 6 + * - Validation + validate4 to accumulate all content errors 7 + * - matchValidation() for exhaustive error reporting 8 + */ 9 + import { readdirSync, readFileSync, statSync } from "fs"; 10 + import { join, relative } from "path"; 11 + import { 12 + type Branded, 13 + brand, 14 + type Validation, 15 + valid, 16 + invalid, 17 + invalidOne, 18 + validate4, 19 + matchValidation, 20 + pipe, 21 + } from "purus-ts"; 22 + 23 + // ─── Branded type for file paths ─── 24 + 25 + type FilePath = Branded<string, "FilePath">; 26 + const filePath = (s: string): FilePath => brand<string, "FilePath">(s); 27 + 28 + // ─── Error types ─── 29 + 30 + type ContentError = 31 + | { _tag: "MissingFrontmatter"; file: FilePath } 32 + | { _tag: "MissingTitle"; file: FilePath } 33 + | { _tag: "DuplicateH1"; file: FilePath; line: number } 34 + | { _tag: "BrokenInternalLink"; file: FilePath; link: string; line: number }; 35 + 36 + // ─── File discovery ─── 37 + 38 + const CONTENT_DIR = join(import.meta.dir, "../src/content/docs"); 39 + 40 + const findContentFiles = (dir: string): FilePath[] => { 41 + const results: FilePath[] = []; 42 + for (const entry of readdirSync(dir)) { 43 + const full = join(dir, entry); 44 + const stat = statSync(full); 45 + if (stat.isDirectory()) { 46 + results.push(...findContentFiles(full)); 47 + } else if (entry.endsWith(".md") || entry.endsWith(".mdx")) { 48 + results.push(filePath(full)); 49 + } 50 + } 51 + return results; 52 + }; 53 + 54 + // ─── Validators ─── 55 + 56 + const validateFrontmatter = ( 57 + file: FilePath, 58 + content: string, 59 + ): Validation<true, ContentError> => 60 + content.startsWith("---") 61 + ? valid(true as const) 62 + : invalidOne({ _tag: "MissingFrontmatter" as const, file }); 63 + 64 + const validateTitle = ( 65 + file: FilePath, 66 + content: string, 67 + ): Validation<true, ContentError> => { 68 + const titleMatch = content.match(/^title:\s*.+/m); 69 + return titleMatch 70 + ? valid(true as const) 71 + : invalidOne({ _tag: "MissingTitle" as const, file }); 72 + }; 73 + 74 + const validateNoH1 = ( 75 + file: FilePath, 76 + content: string, 77 + ): Validation<true, ContentError> => { 78 + const fmEnd = content.indexOf("---", 3); 79 + if (fmEnd === -1) return valid(true as const); 80 + const body = content.slice(fmEnd + 3); 81 + const lines = body.split("\n"); 82 + let inCodeBlock = false; 83 + for (let i = 0; i < lines.length; i++) { 84 + if (lines[i]!.startsWith("```")) inCodeBlock = !inCodeBlock; 85 + if (!inCodeBlock && /^# [^#]/.test(lines[i]!)) { 86 + return invalidOne({ 87 + _tag: "DuplicateH1" as const, 88 + file, 89 + line: i + 1, 90 + }); 91 + } 92 + } 93 + return valid(true as const); 94 + }; 95 + 96 + const validateInternalLinks = ( 97 + file: FilePath, 98 + content: string, 99 + ): Validation<true, ContentError> => { 100 + const lines = content.split("\n"); 101 + const errors: ContentError[] = []; 102 + const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g; 103 + 104 + for (let i = 0; i < lines.length; i++) { 105 + let m: RegExpExecArray | null; 106 + linkRegex.lastIndex = 0; 107 + while ((m = linkRegex.exec(lines[i]!)) !== null) { 108 + const link = m[2]!; 109 + if ( 110 + link.startsWith("http") || 111 + link.startsWith("#") || 112 + link.startsWith("mailto:") 113 + ) 114 + continue; 115 + if (link.endsWith(".md")) { 116 + errors.push({ 117 + _tag: "BrokenInternalLink", 118 + file, 119 + link, 120 + line: i + 1, 121 + }); 122 + } 123 + } 124 + } 125 + return errors.length > 0 ? invalid(errors) : valid(true as const); 126 + }; 127 + 128 + // ─── Main ─── 129 + 130 + console.log("Validating content files...\n"); 131 + 132 + const files = findContentFiles(CONTENT_DIR); 133 + console.log(`Found ${files.length} content files.\n`); 134 + 135 + let hasErrors = false; 136 + 137 + for (const file of files) { 138 + const content = readFileSync(file, "utf-8"); 139 + 140 + const result = validate4( 141 + validateFrontmatter(file, content), 142 + validateTitle(file, content), 143 + validateNoH1(file, content), 144 + validateInternalLinks(file, content), 145 + () => true as const, 146 + ); 147 + 148 + pipe( 149 + result, 150 + matchValidation( 151 + () => {}, 152 + (errors) => { 153 + hasErrors = true; 154 + const relPath = relative(CONTENT_DIR, file); 155 + for (const e of errors) { 156 + switch (e._tag) { 157 + case "MissingFrontmatter": 158 + console.error( 159 + ` ✗ ${relPath}: Missing frontmatter (no --- delimiter)`, 160 + ); 161 + break; 162 + case "MissingTitle": 163 + console.error( 164 + ` ✗ ${relPath}: Missing 'title' in frontmatter`, 165 + ); 166 + break; 167 + case "DuplicateH1": 168 + console.error( 169 + ` ✗ ${relPath}:${e.line}: H1 heading found (title comes from frontmatter)`, 170 + ); 171 + break; 172 + case "BrokenInternalLink": 173 + console.error( 174 + ` ✗ ${relPath}:${e.line}: Link still has .md extension: ${e.link}`, 175 + ); 176 + break; 177 + } 178 + } 179 + }, 180 + ), 181 + ); 182 + } 183 + 184 + if (hasErrors) { 185 + console.error("\n✗ Validation failed."); 186 + throw new Error("Validation failed"); 187 + } else { 188 + console.log("✓ All content files valid.\n"); 189 + }
+14
docs-site/src/components/StoryIllustration.astro
··· 1 + --- 2 + interface Props { 3 + alt: string; 4 + caption?: string; 5 + } 6 + 7 + const { alt, caption } = Astro.props; 8 + --- 9 + 10 + <div class="story-illustration"> 11 + <div class="icon">&#x1f3a8;</div> 12 + <div class="alt-text">{alt}</div> 13 + {caption && <div class="caption">{caption}</div>} 14 + </div>
+7
docs-site/src/content.config.ts
··· 1 + import { defineCollection } from "astro:content"; 2 + import { docsLoader } from "@astrojs/starlight/loaders"; 3 + import { docsSchema } from "@astrojs/starlight/schema"; 4 + 5 + export const collections = { 6 + docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), 7 + };
+317
docs-site/src/content/docs/concepts/10-recursion-over-loops.md
··· 1 + --- 2 + title: Recursion Over Loops 3 + description: Why pure functional code avoids loops and how to replace them with expressions. 4 + sidebar: 5 + order: 10 6 + --- 7 + 8 + Pure functional code avoids loops. Not as a style preference — there's a structural reason. This article explains why and shows you what to use instead. 9 + 10 + --- 11 + 12 + ## Expressions vs Statements 13 + 14 + The core insight: **loops are statements, functional operations are expressions.** 15 + 16 + A statement performs an action but produces no value. An expression always produces a value. 17 + 18 + ```typescript 19 + // Statement — produces nothing, must mutate something to be useful 20 + for (let i = 0; i < users.length; i++) { 21 + results.push(transform(users[i])) 22 + } 23 + 24 + // Expression — produces a value directly 25 + const results = users.map(transform) 26 + ``` 27 + 28 + Statements force you into mutation. You need a mutable variable to collect results, a mutable counter to track position. Expressions compose — pipe the output of one into the input of another. 29 + 30 + ```typescript 31 + // Statements: 3 mutable variables, 4 steps 32 + let filtered: User[] = [] 33 + for (const u of users) { 34 + if (u.active) filtered.push(u) 35 + } 36 + let sorted = filtered.sort(byName) 37 + let names: string[] = [] 38 + for (const u of sorted) { 39 + names.push(u.name) 40 + } 41 + 42 + // Expression: one pipeline, zero mutation 43 + const names = users 44 + .filter(u => u.active) 45 + .sort(byName) 46 + .map(u => u.name) 47 + ``` 48 + 49 + --- 50 + 51 + ## forEach Is a Lie 52 + 53 + `forEach` looks functional — it's a method on Array, it takes a callback. But look at its signature: 54 + 55 + ```typescript 56 + Array<T>.forEach(callback: (value: T, index: number) => void): void 57 + // ^^^^ ^^^^ 58 + ``` 59 + 60 + It returns `void`. Twice. The callback returns nothing. The method returns nothing. The _only_ way to do anything useful with `forEach` is through side effects — mutating variables, logging, pushing to external arrays. 61 + 62 + Compare with `map`: 63 + 64 + ```typescript 65 + Array<T>.map(callback: (value: T, index: number) => U): U[] 66 + // ^ ^^ 67 + ``` 68 + 69 + `map` returns a value. The callback returns a value. Everything flows through return values, not mutation. 70 + 71 + ### The practical problems 72 + 73 + `forEach` can't short-circuit. If you need to stop early, you're stuck — `break` doesn't work inside callbacks. You'd need `for...of` or `some`/`find`. 74 + 75 + `forEach` can't accumulate. Need to build up a result? You must declare a mutable variable outside the loop, then mutate it inside. 76 + 77 + ```typescript 78 + // forEach forces mutation 79 + const seen = new Set<string>() 80 + items.forEach(item => { 81 + seen.add(item.id) // mutating external state 82 + }) 83 + 84 + // reduce threads the accumulator 85 + const seen = items.reduce( 86 + (acc, item) => new Set([...acc, item.id]), 87 + new Set<string>() 88 + ) 89 + ``` 90 + 91 + --- 92 + 93 + ## The Functional Toolkit 94 + 95 + Four methods replace virtually all loops: 96 + 97 + ### map — transform each element 98 + 99 + ```typescript 100 + // forEach + mutation 101 + const names: string[] = [] 102 + users.forEach(u => names.push(u.name)) 103 + 104 + // map 105 + const names = users.map(u => u.name) 106 + ``` 107 + 108 + ### filter — keep elements matching a predicate 109 + 110 + ```typescript 111 + // for loop + mutation 112 + const active: User[] = [] 113 + for (const u of users) { 114 + if (u.isActive) active.push(u) 115 + } 116 + 117 + // filter 118 + const active = users.filter(u => u.isActive) 119 + ``` 120 + 121 + ### reduce — accumulate into any shape 122 + 123 + ```typescript 124 + // for loop + mutable counter 125 + let total = 0 126 + for (const item of cart) { 127 + total += item.price * item.quantity 128 + } 129 + 130 + // reduce 131 + const total = cart.reduce( 132 + (sum, item) => sum + item.price * item.quantity, 0 133 + ) 134 + ``` 135 + 136 + ### flatMap — transform and flatten 137 + 138 + ```typescript 139 + // forEach + mutation 140 + const allTags: string[] = [] 141 + posts.forEach(p => p.tags.forEach(t => allTags.push(t))) 142 + 143 + // flatMap 144 + const allTags = posts.flatMap(p => p.tags) 145 + ``` 146 + 147 + ### Display pattern: map + join 148 + 149 + A common case is rendering a list to the console. Instead of `forEach` with `console.log` inside, use `map` + `join`: 150 + 151 + ```typescript 152 + // forEach — one console.log per item 153 + errors.forEach(e => console.log(` - ${formatError(e)}`)) 154 + 155 + // map + join — single expression 156 + console.log(errors.map(e => ` - ${formatError(e)}`).join("\n")) 157 + ``` 158 + 159 + --- 160 + 161 + ## When You Need State: reduce 162 + 163 + `reduce` is the workhorse for stateful iteration. Instead of mutating variables, you thread an accumulator through each step. 164 + 165 + Here's the ballot-counting example from the [Forest Election story](/stories/forest-election/01-the-ballot-box-problem/). The old version used `forEach` with a mutable `Set`: 166 + 167 + ```typescript 168 + // Mutable accumulation 169 + const alreadyVoted = new Set<string>() 170 + ballots.forEach(({ voter, candidate }, i) => { 171 + const result = validateBallotFull(voter, candidate, alreadyVoted) 172 + matchValidation( 173 + (ballot) => { alreadyVoted.add(ballot.voter) }, 174 + (errors) => { /* ... */ } 175 + )(result) 176 + }) 177 + ``` 178 + 179 + The functional version threads the set through `reduce`: 180 + 181 + ```typescript 182 + // Immutable accumulation 183 + ballots.reduce<ReadonlySet<string>>((voted, { voter, candidate }, i) => { 184 + const result = validateBallotFull(voter, candidate, voted) 185 + return matchValidation( 186 + (ballot: Ballot) => { 187 + console.log(`Ballot ${i + 1}: Valid - ${ballot.voter} → ${ballot.candidate}`) 188 + return new Set([...voted, ballot.voter]) 189 + }, 190 + (errors: readonly BallotError[]) => { 191 + console.log(`Ballot ${i + 1}: Invalid`) 192 + console.log(errors.map(e => ` - ${formatError(e)}`).join("\n")) 193 + return voted 194 + } 195 + )(result) 196 + }, new Set<string>()) 197 + ``` 198 + 199 + Each iteration returns the next state. No mutation. The types make the flow explicit — `ReadonlySet<string>` goes in and comes out. 200 + 201 + --- 202 + 203 + ## The go() Pattern 204 + 205 + For control flow that doesn't fit `map`/`reduce` — recursion with variable step sizes, early termination on complex conditions — use a recursive inner function named `go`: 206 + 207 + ```typescript 208 + // From purus-ts: retry combinator 209 + export const retry = 210 + (times: number) => 211 + <A, E, R>(eff: Eff<A, E, R>): Eff<A, E, R> => { 212 + const go = (remaining: number): Eff<A, E, R> => 213 + remaining <= 0 214 + ? eff 215 + : pipe( 216 + eff, 217 + foldEff( 218 + (_e: E) => go(remaining - 1), 219 + (a: A) => succeed(a) as Eff<A, E, R>, 220 + ), 221 + ) 222 + 223 + return go(times) 224 + } 225 + ``` 226 + 227 + The pattern is always the same: 228 + 229 + 1. Define `go` inside the outer function 230 + 2. Base case returns a value 231 + 3. Recursive case transforms and calls `go` again 232 + 4. Call `go` with initial arguments 233 + 234 + Here's `binarySearch` from purus — a classic `go` pattern: 235 + 236 + ```typescript 237 + export const binarySearch = 238 + <T>(compare: (a: T, b: T) => number) => 239 + (target: T) => 240 + <P extends string>(xs: Arr<T, P | Sorted>): Option<number> => { 241 + const go = (lo: number, hi: number): Option<number> => 242 + lo > hi 243 + ? none 244 + : pipe(Math.floor((lo + hi) / 2), (mid) => 245 + ((cmp) => 246 + cmp === 0 247 + ? some(mid) 248 + : cmp < 0 249 + ? go(lo, mid - 1) 250 + : go(mid + 1, hi))(compare(target, xs[mid]!)), 251 + ) 252 + return go(0, xs.length - 1) 253 + } 254 + ``` 255 + 256 + A `while` loop version would need mutable `lo`, `hi`, and `result` variables. The recursive version has none. 257 + 258 + --- 259 + 260 + ## Head-Tail Decomposition 261 + 262 + Processing a list by splitting it into the first element and the rest: 263 + 264 + ```typescript 265 + const sum = (xs: readonly number[]): number => 266 + xs.length === 0 267 + ? 0 268 + : xs[0]! + sum(xs.slice(1)) 269 + ``` 270 + 271 + This is elegant but has O(n) memory from `slice`. For practical code, prefer `reduce`: 272 + 273 + ```typescript 274 + const sum = (xs: readonly number[]): number => 275 + xs.reduce((acc, x) => acc + x, 0) 276 + ``` 277 + 278 + Head-tail decomposition shines when the recursive structure mirrors the problem — tree traversals, nested data, or when you need to process elements in pairs. 279 + 280 + --- 281 + 282 + ## Cheatsheet 283 + 284 + | Imperative Pattern | Functional Replacement | 285 + |---|---| 286 + | `arr.forEach(x => console.log(x))` | `console.log(arr.map(f).join("\n"))` | 287 + | `forEach` + push to external array | `map` or `flatMap` | 288 + | `forEach` + mutable counter/set | `reduce` | 289 + | `for...of` with accumulator | `reduce` | 290 + | `for (let i = 0; i < n; i++)` | `Array.from({length: n}, (_, i) => ...)` | 291 + | `while (condition)` | Recursive `go()` | 292 + | `Object.entries(o).forEach(...)` | `Object.entries(o).map(...).join(...)` | 293 + | `for...of` in test iteration | `test.each(cases)(...)` | 294 + 295 + --- 296 + 297 + ## The Performance Question 298 + 299 + "Isn't `reduce` slower than `for`?" 300 + 301 + In practice, no. V8 and other JS engines optimize built-in array methods heavily. The overhead of a function call per iteration is negligible compared to the actual work being done. 302 + 303 + That said, in rare hot-path cases — tight inner loops processing millions of elements — a `for` loop may be appropriate. purus itself uses `for...of` inside `traverseResult` and `traverseOption` for exactly this reason. But notice: this is hidden behind a pure interface. The caller sees `traverseResult(f)(items)` — an expression that returns a value. The implementation detail stays internal. 304 + 305 + The principle: **pure interfaces, pragmatic internals**. Your application code should be expressions all the way through. If a library function needs a `for` loop for performance, that's an implementation detail behind a functional API. 306 + 307 + --- 308 + 309 + ## Key Takeaways 310 + 311 + - **Loops are statements** — they produce nothing and force mutation. 312 + - **`forEach` returns void** — it exists only for side effects. 313 + - **`map`, `filter`, `reduce`, `flatMap`** cover virtually all iteration needs. 314 + - **`reduce` replaces mutable accumulators** — thread state through the callback. 315 + - **The `go()` pattern** handles complex recursion — base case + recursive case. 316 + - **Display code** uses `map` + `join` instead of `forEach` + `console.log`. 317 + - **Performance** is not a concern in application code. Reserve `for` loops for library internals behind pure interfaces.
+481
docs-site/src/content/docs/examples/http-client.md
··· 1 + --- 2 + title: HTTP Client 3 + description: Fetching data with retry, timeout, and automatic cleanup. 4 + sidebar: 5 + order: 1 6 + --- 7 + 8 + Fetching data with retry and timeout. 9 + 10 + ## The Problem 11 + 12 + Every app fetches data from APIs. But production code needs retries, timeouts, 13 + and cleanup. In vanilla TypeScript, this means: 14 + 15 + - Manual AbortController wiring 16 + - Remembering to clearTimeout in every code path 17 + - `catch (e: unknown)` with no idea what went wrong 18 + 19 + ## Run Both Versions 20 + 21 + ```bash 22 + bun run examples/http-client/without-purus.ts 23 + bun run examples/http-client/with-purus.ts 24 + ``` 25 + 26 + ## Without purus 27 + 28 + ts` - same thing with: 29 + - `pipe(fetch, retry(3), timeout(5000))` 30 + - Cleanup function returned from async effect 31 + - Typed HttpError union 32 + - match() for exhaustive error handling 33 + 34 + ## Key Takeaways 35 + 36 + - Typed errors tell you what can fail 37 + - Cleanup is automatic when you return it from async() 38 + - retry() and timeout() are composable functions 39 + 40 + 41 + ## Full Code: without-purus.ts 42 + 43 + ```typescript 44 + /** 45 + * HTTP Client - Vanilla TypeScript (Comparison) 46 + * ============================================== 47 + * 48 + * This is the "before" version. Compare with with-purus.ts to see how purus 49 + * improves resilient HTTP fetching. 50 + * 51 + * PROBLEMS THIS APPROACH HAS: 52 + * 53 + * 1. ERRORS ARE UNKNOWN - Every catch block has `e: unknown`. You have to 54 + * instanceof check and message-sniff to figure out what happened. 55 + * What if someone changes the error message? Your handling breaks. 56 + * 57 + * 2. MANUAL CLEANUP - AbortController and setTimeout need careful wiring. 58 + * Notice how many places we call clearTimeout()? Miss one = memory leak. 59 + * The early return on line 45 doesn't clear the timeout before throwing. 60 + * 61 + * 3. RETRY IS BAKED IN - The retry logic is tangled into fetchWithRetryAndTimeout. 62 + * Want retry without timeout? Want timeout without retry? You need separate 63 + * functions or complex option objects. 64 + * 65 + * Read through and count the clearTimeout() calls. Ask yourself: did we cover 66 + * every path? (Hint: check line 45) 67 + * 68 + * Prerequisites: Promise understanding 69 + * Next: Compare with with-purus.ts to see the improvement 70 + */ 71 + 72 + type User = { 73 + id: number 74 + name: string 75 + email: string 76 + } 77 + 78 + // ============================================================================= 79 + // SECTION 1: Fetch with Retry and Timeout 80 + // ============================================================================= 81 + // 82 + // COMPLEXITY EXPLOSION: 83 + // What starts as a simple fetch() quickly becomes a maze of: 84 + // - Manual AbortController creation 85 + // - Multiple clearTimeout calls (easy to miss one!) 86 + // - Exponential backoff calculation inline 87 + // - Error message sniffing to detect timeout vs network errors 88 + // 89 + // GOTCHA #1: Line 66 - clearTimeout is in the catch block, but line 58 90 + // throws without clearing the timeout first. Memory leak potential! 91 + // 92 + // GOTCHA #2: lastError is `unknown` - we've lost all type information 93 + // about what actually went wrong. 94 + // ============================================================================= 95 + 96 + const fetchWithRetryAndTimeout = async ( 97 + url: string, 98 + options: { 99 + retries?: number 100 + timeoutMs?: number 101 + retryDelayMs?: number 102 + } = {} 103 + ): Promise<User> => { 104 + const { retries = 3, timeoutMs = 5000, retryDelayMs = 1000 } = options 105 + let lastError: unknown // <- We've lost all error type information here 106 + 107 + for (let attempt = 0; attempt <= retries; attempt++) { 108 + const controller = new AbortController() 109 + let timeoutId: ReturnType<typeof setTimeout> | undefined 110 + 111 + try { 112 + // Wire up the timeout to abort the request 113 + timeoutId = setTimeout(() => controller.abort(), timeoutMs) 114 + 115 + console.log(`[Attempt ${attempt + 1}/${retries + 1}] Fetching ${url}...`) 116 + 117 + const response = await fetch(url, { signal: controller.signal }) 118 + 119 + clearTimeout(timeoutId) // Don't forget this! 120 + 121 + if (!response.ok) { 122 + // GOTCHA: We cleared the timeout above, but if we restructure this code 123 + // and move the throw before clearTimeout, we leak the timer. 124 + // This kind of subtle ordering bug is why cleanup should be automatic. 125 + throw new Error(`HTTP ${response.status}: ${response.statusText}`) 126 + } 127 + 128 + const data = await response.json() 129 + console.log(`[Success] Got response on attempt ${attempt + 1}`) 130 + return data as User 131 + 132 + } catch (e: unknown) { 133 + // Need to clean up here too - easy to miss in a refactor 134 + if (timeoutId) clearTimeout(timeoutId) 135 + 136 + lastError = e // e is unknown - good luck figuring out what went wrong 137 + 138 + // MESSAGE SNIFFING: We detect timeout by checking error.name === "AbortError" 139 + // This is fragile - browser implementations could differ 140 + const isTimeout = e instanceof Error && e.name === "AbortError" 141 + const message = e instanceof Error ? e.message : "Unknown error" 142 + 143 + console.log(`[Attempt ${attempt + 1}] Failed: ${isTimeout ? "Timeout" : message}`) 144 + 145 + if (attempt < retries) { 146 + const delay = retryDelayMs * Math.pow(2, attempt) 147 + console.log(`[Retry] Waiting ${delay}ms before retry...`) 148 + await new Promise(resolve => setTimeout(resolve, delay)) 149 + } 150 + } 151 + } 152 + 153 + // Caller gets unknown - they have to do the same instanceof/message checking 154 + throw lastError 155 + } 156 + 157 + // ============================================================================= 158 + // SECTION 2: Demo 159 + // ============================================================================= 160 + // 161 + // NOTICE THE PATTERN: 162 + // Every test is wrapped in try/catch. Every catch block: 163 + // 1. Receives `e: unknown` 164 + // 2. Does instanceof Error check 165 + // 3. Extracts .message 166 + // 4. Has no compile-time guarantees about what errors are possible 167 + // 168 + // If fetchWithRetryAndTimeout starts throwing a new error type, these catch 169 + // blocks silently do the wrong thing. No compiler warning. 170 + // ============================================================================= 171 + 172 + const main = async () => { 173 + console.log("=== HTTP Client (without purus) ===\n") 174 + 175 + try { 176 + console.log("--- Test 1: Successful request ---") 177 + const user = await fetchWithRetryAndTimeout( 178 + "https://jsonplaceholder.typicode.com/users/1", 179 + { retries: 2, timeoutMs: 5000 } 180 + ) 181 + console.log(`Got user: ${user.name} (${user.email})\n`) 182 + 183 + } catch (e: unknown) { 184 + // WHAT DID WE CATCH? 185 + // - Network error? Timeout? 404? 500? JSON parse error? 186 + // - TypeScript has no idea. We're on our own. 187 + console.error("Failed:", e instanceof Error ? e.message : e) 188 + } 189 + 190 + try { 191 + console.log("--- Test 2: Short timeout ---") 192 + const user = await fetchWithRetryAndTimeout( 193 + "https://jsonplaceholder.typicode.com/users/1", 194 + { retries: 1, timeoutMs: 1 } 195 + ) 196 + console.log(`Got user: ${user.name}\n`) 197 + 198 + } catch (e: unknown) { 199 + // We HOPE this is a timeout error, but TypeScript can't verify 200 + console.log("Expected timeout:", e instanceof Error ? e.message : e) 201 + console.log() 202 + } 203 + 204 + try { 205 + console.log("--- Test 3: 404 Not Found ---") 206 + const user = await fetchWithRetryAndTimeout( 207 + "https://jsonplaceholder.typicode.com/users/99999", 208 + { retries: 0, timeoutMs: 5000 } 209 + ) 210 + console.log(`Got user: ${user.name}\n`) 211 + 212 + } catch (e: unknown) { 213 + // We HOPE this is a 404 error, but it could be anything 214 + console.log("Expected 404:", e instanceof Error ? e.message : e) 215 + console.log() 216 + } 217 + 218 + console.log("=== Done ===") 219 + } 220 + 221 + main() 222 + .catch(console.error) 223 + .finally(() => process.exit(0)) 224 + ``` 225 + 226 + 227 + ## Full Code: with-purus.ts 228 + 229 + ```typescript 230 + /** 231 + * HTTP Client Example - Building Resilient Data Fetching 232 + * ====================================================== 233 + * 234 + * This example teaches three core purus concepts: 235 + * 236 + * 1. TYPED ERRORS - Know exactly what can fail (no `catch (e: unknown)`) 237 + * The HttpError union has specific variants, so match() forces you to 238 + * handle each case. Try commenting out a case - the compiler stops you. 239 + * 240 + * 2. COMPOSABLE OPERATIONS - pipe(), retry(), timeout() chain cleanly 241 + * Each combinator is a pure function that transforms an Eff. You can 242 + * add/remove/reorder them without restructuring your code. 243 + * 244 + * 3. AUTOMATIC CLEANUP - Return a cleanup function, never forget to clearTimeout 245 + * The async() constructor takes a register function that returns a cleanup. 246 + * When the effect is cancelled (timeout, interrupt), cleanup runs automatically. 247 + * 248 + * Compare with without-purus.ts to see how vanilla TypeScript handles the same 249 + * problems (spoiler: lots of manual AbortController wiring and `e: unknown`). 250 + * 251 + * Prerequisites: Basic Promise understanding 252 + * Next: workflow-engine for branded types and typestate 253 + */ 254 + 255 + import { 256 + type Eff, 257 + Exit, 258 + succeed, 259 + fail, 260 + async, 261 + flatMap, 262 + catchAll, 263 + timeout, 264 + retry, 265 + pipe, 266 + match, 267 + runPromise, 268 + runPromiseExit, 269 + } from "../../src/index" 270 + 271 + // ============================================================================= 272 + // SECTION 1: Error Types 273 + // ============================================================================= 274 + // 275 + // WHY TYPED ERRORS MATTER: 276 + // In vanilla JS, errors are `unknown` in catch blocks. You end up with: 277 + // catch (e) { if (e instanceof Error && e.message.includes("timeout")) ... } 278 + // 279 + // With discriminated unions, each error variant is explicit in the type. 280 + // The match() function forces exhaustive handling - forget a case = compile error. 281 + // 282 + // PATTERN: Each error variant has a _tag (discriminant) and relevant data. 283 + // This is the standard discriminated union pattern in TypeScript. 284 + // ============================================================================= 285 + 286 + type HttpError = 287 + | { readonly _tag: "NetworkError"; readonly message: string } 288 + | { readonly _tag: "TimeoutError"; readonly ms: number } 289 + | { readonly _tag: "NotFound"; readonly url: string } 290 + | { readonly _tag: "ServerError"; readonly status: number } 291 + 292 + // Smart constructors - these ensure consistent error creation 293 + const HttpError = { 294 + network: (message: string): HttpError => ({ _tag: "NetworkError", message }), 295 + timeout: (ms: number): HttpError => ({ _tag: "TimeoutError", ms }), 296 + notFound: (url: string): HttpError => ({ _tag: "NotFound", url }), 297 + serverError: (status: number): HttpError => ({ _tag: "ServerError", status }), 298 + } 299 + 300 + type User = { 301 + id: number 302 + name: string 303 + email: string 304 + } 305 + 306 + // Type guard - validates the shape at runtime without casting 307 + const isUser = (data: unknown): data is User => 308 + typeof data === "object" && 309 + data !== null && 310 + "id" in data && 311 + "name" in data && 312 + "email" in data 313 + 314 + // ============================================================================= 315 + // SECTION 2: Fetch as an Effect 316 + // ============================================================================= 317 + // 318 + // WHY USE async() INSTEAD OF fromPromise(): 319 + // The async() constructor gives you control over cleanup. When an effect is 320 + // cancelled (via timeout, race, or manual interrupt), the cleanup function runs. 321 + // 322 + // HOW IT WORKS: 323 + // 1. async() takes a "register" function 324 + // 2. The register function receives a "resume" callback 325 + // 3. You start your async work and call resume(Exit.succeed(value)) or resume(Exit.fail(error)) 326 + // 4. Return a cleanup function - it runs on cancellation 327 + // 328 + // GOTCHA: The AbortController abort() must happen in the cleanup function. 329 + // If you forget to return cleanup, the request keeps running even after timeout! 330 + // ============================================================================= 331 + 332 + const fetchUser = (url: string): Eff<User, HttpError, unknown> => 333 + async((resume) => { 334 + // Create an AbortController - we'll abort it on cleanup 335 + const controller = new AbortController() 336 + 337 + console.log(`[Fetch] ${url}`) 338 + 339 + fetch(url, { signal: controller.signal }) 340 + .then((response) => 341 + !response.ok 342 + ? response.status === 404 343 + ? resume(Exit.fail(HttpError.notFound(url))) 344 + : resume(Exit.fail(HttpError.serverError(response.status))) 345 + : response.json().then((data: unknown) => 346 + // Use type guard instead of casting - validates at runtime 347 + isUser(data) 348 + ? resume(Exit.succeed(data)) 349 + : resume(Exit.fail(HttpError.serverError(500))), 350 + ), 351 + ) 352 + .catch((err) => { 353 + // GOTCHA: Don't resume on AbortError - the fiber is already cancelled 354 + // Resuming after cancellation would cause undefined behavior 355 + if (err.name === "AbortError") return 356 + resume(Exit.fail(HttpError.network(err.message))) 357 + }) 358 + 359 + // THE KEY INSIGHT: This cleanup function runs automatically on timeout/interrupt 360 + // No manual finally blocks, no forgetting to clearTimeout, no leaked requests 361 + return () => { 362 + console.log("[Cleanup] Aborting request") 363 + controller.abort() 364 + } 365 + }) 366 + 367 + // ============================================================================= 368 + // SECTION 3: Error Handling with match() 369 + // ============================================================================= 370 + // 371 + // WHY match() OVER switch/if-else: 372 + // - match() is EXHAUSTIVE - add a new error variant, and all match() calls 373 + // that don't handle it become compile errors 374 + // - It's an expression, not a statement, so it always returns a value 375 + // - Each handler receives the correctly narrowed type (e.g., { _tag: "TimeoutError", ms }) 376 + // 377 + // TRY THIS: Comment out one of the cases below. TypeScript will error. 378 + // ============================================================================= 379 + 380 + const handleError = (error: HttpError): string => 381 + match(error)({ 382 + NetworkError: ({ message }) => `Network failed: ${message}`, 383 + TimeoutError: ({ ms }) => `Request timed out after ${ms}ms`, 384 + NotFound: ({ url }) => `Resource not found: ${url}`, 385 + ServerError: ({ status }) => `Server error: ${status}`, 386 + }) 387 + 388 + // ============================================================================= 389 + // SECTION 4: Demo 390 + // ============================================================================= 391 + // 392 + // COMPOSABILITY IN ACTION: 393 + // Notice how each test builds up functionality using pipe(): 394 + // - fetchUser() is the base effect 395 + // - retry(n) wraps it with retry logic 396 + // - timeout(ms) adds a timeout 397 + // - catchAll() recovers from errors 398 + // 399 + // Each combinator is independent - you can add, remove, or reorder them. 400 + // Compare this to the nested try/catch/finally in without-purus.ts. 401 + // ============================================================================= 402 + 403 + const main = async () => { 404 + console.log("=== HTTP Client (with purus) ===\n") 405 + 406 + // --------------------------------------------------------------------------- 407 + // Test 1: Successful request 408 + // Shows: basic pipe with retry + timeout, happy path 409 + // --------------------------------------------------------------------------- 410 + console.log("--- Test 1: Successful request ---") 411 + 412 + const request1 = pipe( 413 + fetchUser("https://jsonplaceholder.typicode.com/users/1"), 414 + retry(2), // Retry up to 2 times on failure 415 + timeout(5000), // Cancel if not done in 5s 416 + flatMap((result) => 417 + // timeout() returns null on timeout, convert to typed error 418 + result === null 419 + ? fail(HttpError.timeout(5000)) 420 + : succeed(result) 421 + ) 422 + ) 423 + 424 + // Use runPromiseExit instead of try/catch - no type casting needed 425 + const exit1 = await runPromiseExit(request1) 426 + exit1._tag === "Success" 427 + ? console.log(`Got user: ${exit1.value.name} (${exit1.value.email})\n`) 428 + : exit1._tag === "Failure" 429 + ? console.log(`Error: ${handleError(exit1.error)}\n`) 430 + : console.log("Interrupted\n") 431 + 432 + // --------------------------------------------------------------------------- 433 + // Test 2: Short timeout - will fail, but we recover 434 + // Shows: catchAll() for error recovery, returns fallback value 435 + // --------------------------------------------------------------------------- 436 + console.log("--- Test 2: Short timeout ---") 437 + 438 + const request2 = pipe( 439 + fetchUser("https://jsonplaceholder.typicode.com/users/1"), 440 + timeout(1), // 1ms timeout = guaranteed timeout 441 + flatMap((result) => 442 + result === null 443 + ? fail(HttpError.timeout(1)) 444 + : succeed(result) 445 + ), 446 + // RECOVERY: catchAll transforms errors into success values 447 + // The error is typed, so we know exactly what we're catching 448 + catchAll((error) => ( 449 + console.log(`[Caught] ${handleError(error)}`), 450 + succeed({ id: 0, name: "Timeout Fallback", email: "" }) 451 + )) 452 + ) 453 + 454 + const fallbackUser = await runPromise(request2) 455 + console.log(`Result: ${fallbackUser.name}\n`) 456 + 457 + // --------------------------------------------------------------------------- 458 + // Test 3: 404 error - recover with default 459 + // Shows: server-side errors flow through typed error channel 460 + // --------------------------------------------------------------------------- 461 + console.log("--- Test 3: 404 Not Found ---") 462 + 463 + const request3 = pipe( 464 + fetchUser("https://jsonplaceholder.typicode.com/users/99999"), 465 + retry(0), // No retries for this test 466 + catchAll((error) => ( 467 + console.log(`[Caught] ${handleError(error)}`), 468 + succeed({ id: 0, name: "Default User", email: "" }) 469 + )) 470 + ) 471 + 472 + const user404 = await runPromise(request3) 473 + console.log(`Result: ${user404.name}\n`) 474 + 475 + console.log("=== Done ===") 476 + } 477 + 478 + main() 479 + .catch(console.error) 480 + .finally(() => process.exit(0)) 481 + ```
+58
docs-site/src/content/docs/examples/index.md
··· 1 + --- 2 + title: Examples 3 + description: Learn purus-ts through side-by-side comparisons with vanilla TypeScript. 4 + --- 5 + 6 + Learn purus-ts through side-by-side comparisons with vanilla TypeScript. 7 + 8 + Each example includes: 9 + - **README.md** - The problem and how purus helps 10 + - **without-purus.ts** - Typical vanilla implementation 11 + - **with-purus.ts** - Same thing with purus 12 + 13 + ## Examples 14 + 15 + ### [http-client](/examples/http-client/) 16 + 17 + Fetching data with retry and timeout. 18 + 19 + **Key concepts:** `pipe`, `retry`, `timeout`, typed errors, automatic cleanup 20 + 21 + ```bash 22 + bun run examples/http-client/without-purus.ts 23 + bun run examples/http-client/with-purus.ts 24 + ``` 25 + 26 + --- 27 + 28 + ### [workflow-engine](/examples/workflow-engine/) 29 + 30 + Order processing with IDs that can't be swapped and states that can't be skipped. 31 + 32 + **Key concepts:** Branded types, typestate, `match`, Result 33 + 34 + ```bash 35 + bun run examples/workflow-engine/without-purus.ts 36 + bun run examples/workflow-engine/with-purus.ts 37 + ``` 38 + 39 + --- 40 + 41 + ### [task-queue](/examples/task-queue/) 42 + 43 + Background job processing with real cancellation and dependency injection. 44 + 45 + **Key concepts:** `fork`/`join`, `catchAll`, `provide` for DI 46 + 47 + ```bash 48 + bun run examples/task-queue/without-purus.ts 49 + bun run examples/task-queue/with-purus.ts 50 + ``` 51 + 52 + --- 53 + 54 + ## Why Side-by-Side? 55 + 56 + Run both versions and compare. The vanilla code is realistic - not a strawman. 57 + Notice what changes: where the cleanup logic goes, how errors are handled, 58 + what the types tell you.
+532
docs-site/src/content/docs/examples/task-queue.md
··· 1 + --- 2 + title: Task Queue 3 + description: Background job processing with real cancellation and dependency injection. 4 + sidebar: 5 + order: 3 6 + --- 7 + 8 + Background job processing with real cancellation and easy testing. 9 + 10 + ## The Problem 11 + 12 + Background jobs need retries, timeouts, and error handling. In vanilla TypeScript: 13 + - Promise.race doesn't actually cancel the losing promise 14 + - Dependencies are hardcoded, making tests awkward 15 + - Error context gets lost after retries 16 + 17 + ## Run Both Versions 18 + 19 + ```bash 20 + bun run examples/task-queue/without-purus.ts 21 + bun run examples/task-queue/with-purus.ts 22 + ``` 23 + 24 + ## Without purus 25 + 26 + race for timeout (but the job keeps running!) 27 + - Hardcoded logger - can't mock without a DI framework 28 + - Errors are `unknown` after retry exhaustion 29 + 30 + ## With purus 31 + 32 + See `with-purus.ts`: 33 + - timeout() returns cleanup function - job actually stops 34 + - provide(env) injects dependencies - swap for tests 35 + - Typed errors preserved through the pipeline 36 + 37 + ## Key Takeaways 38 + 39 + - Return a cleanup function from async() for real cancellation 40 + - provide() makes testing easy without frameworks 41 + - catchAll() preserves error context 42 + 43 + 44 + ## Full Code: without-purus.ts 45 + 46 + ```typescript 47 + /** 48 + * Task Queue - Vanilla TypeScript (Comparison) 49 + * ============================================= 50 + * 51 + * This is the "before" version. Compare with with-purus.ts to see how purus 52 + * improves concurrency and testability. 53 + * 54 + * PROBLEMS THIS APPROACH HAS: 55 + * 56 + * 1. Promise.race DOESN'T CANCEL - It just ignores the loser 57 + * See executeWithTimeout() on line 48. When timeout wins, the job 58 + * KEEPS RUNNING in the background. We're not cancelling, just ignoring. 59 + * This wastes resources and can cause side effects after "timeout". 60 + * 61 + * 2. HARDCODED LOGGER - Awkward to mock in tests 62 + * The logger is a module-level constant (line 21-24). 63 + * To test silently, you'd need jest.mock() or process.env checks. 64 + * Compare with-purus.ts where you just swap provide(prodEnv) for provide(testEnv). 65 + * 66 + * 3. ERRORS BECOME UNKNOWN - Type information is lost 67 + * lastError is `unknown`. We throw it, catch it, and lose all structure. 68 + * In with-purus.ts, JobError flows through typed - no guessing. 69 + * 70 + * THE KEY INSIGHT: Promise.race is not cancellation. 71 + * Run both examples and watch the logs - see how purus actually stops work. 72 + * 73 + * Prerequisites: http-client and workflow-engine examples 74 + * Next: Compare with with-purus.ts 75 + */ 76 + 77 + type Job = { 78 + id: string 79 + type: "email" | "image" | "sync" 80 + payload: Record<string, unknown> 81 + } 82 + 83 + // ============================================================================= 84 + // SECTION 1: Hardcoded Logger (The Problem) 85 + // ============================================================================= 86 + // 87 + // This logger is a module-level constant. 88 + // In tests, you'd typically need: 89 + // - jest.mock() to replace it 90 + // - process.env.NODE_ENV checks to disable logging 91 + // - A mocking library 92 + // 93 + // Compare with-purus.ts where you just swap provide(prodEnv) with provide(testEnv). 94 + // ============================================================================= 95 + 96 + const logger = { 97 + info: (msg: string) => console.log(`[INFO] ${msg}`), 98 + error: (msg: string) => console.log(`[ERROR] ${msg}`), 99 + } 100 + 101 + // ============================================================================= 102 + // SECTION 2: Job Execution 103 + // ============================================================================= 104 + // 105 + // A simple async function that simulates work. 106 + // No cleanup mechanism - when this starts, it runs to completion. 107 + // ============================================================================= 108 + 109 + const executeJob = async (job: Job): Promise<void> => { 110 + const delay = Math.random() * 200 + 50 111 + await new Promise(resolve => setTimeout(resolve, delay)) 112 + 113 + // 30% chance of failure 114 + if (Math.random() < 0.3) { 115 + throw new Error(`Job ${job.id} failed: transient error`) 116 + } 117 + 118 + logger.info(`Job ${job.id} (${job.type}) completed`) 119 + } 120 + 121 + // ============================================================================= 122 + // SECTION 3: Promise.race Timeout (The Problem) 123 + // ============================================================================= 124 + // 125 + // !!! THIS IS THE KEY PROBLEM !!! 126 + // 127 + // Promise.race() returns when ONE promise settles. 128 + // BUT THE OTHER PROMISE KEEPS RUNNING! 129 + // 130 + // If executeJob takes 10 seconds and timeout is 5 seconds: 131 + // - Promise.race returns after 5 seconds with "Timeout" 132 + // - executeJob continues running for 5 more seconds 133 + // - Any side effects from executeJob still happen 134 + // - We've used resources for work we're "ignoring" 135 + // 136 + // In with-purus.ts, timeout() calls the cleanup function, which sets 137 + // cancelled=true and clearTimeout(). The job actually STOPS. 138 + // ============================================================================= 139 + 140 + const executeWithTimeout = async (job: Job, timeoutMs: number): Promise<void> => { 141 + // Promise.race: Returns when first promise settles, but... 142 + // THE LOSER KEEPS RUNNING IN THE BACKGROUND! 143 + // 144 + // If executeJob takes 10s and timeout is 5s: 145 + // - We get "Timeout" after 5s 146 + // - But executeJob runs for the full 10s anyway 147 + // - Any side effects still happen 148 + const result = await Promise.race([ 149 + executeJob(job), 150 + new Promise<never>((_, reject) => 151 + setTimeout(() => reject(new Error("Timeout")), timeoutMs) 152 + ), 153 + ]) 154 + return result 155 + } 156 + 157 + // ============================================================================= 158 + // SECTION 4: Retry Logic 159 + // ============================================================================= 160 + // 161 + // Standard retry loop with error accumulation. 162 + // 163 + // NOTICE: lastError is `unknown`. We've lost all type information about 164 + // what went wrong. The caller has to guess and instanceof-check. 165 + // ============================================================================= 166 + 167 + const executeWithRetry = async ( 168 + job: Job, 169 + maxRetries: number, 170 + timeoutMs: number 171 + ): Promise<void> => { 172 + let lastError: unknown // <- All error type information is lost here 173 + 174 + for (let attempt = 1; attempt <= maxRetries; attempt++) { 175 + try { 176 + logger.info(`[Attempt ${attempt}/${maxRetries}] Processing job ${job.id}`) 177 + await executeWithTimeout(job, timeoutMs) 178 + return 179 + } catch (e) { 180 + lastError = e // <- e is unknown, we've lost all structure 181 + logger.error(`Attempt ${attempt} failed: ${e instanceof Error ? e.message : e}`) 182 + } 183 + } 184 + 185 + throw lastError // <- Caller gets unknown, not a typed error 186 + } 187 + 188 + // ============================================================================= 189 + // SECTION 5: Queue Processing 190 + // ============================================================================= 191 + // 192 + // Processes jobs sequentially with error recovery. 193 + // Note how we track success/failure but can't distinguish error types. 194 + // ============================================================================= 195 + 196 + const processQueue = async (jobs: Job[]): Promise<void> => { 197 + const results: Array<{ job: Job; success: boolean; error?: unknown }> = [] 198 + 199 + for (const job of jobs) { 200 + try { 201 + await executeWithRetry(job, 3, 5000) 202 + results.push({ job, success: true }) 203 + } catch (e) { 204 + // e is unknown - we can't distinguish timeout vs transient vs permanent 205 + results.push({ job, success: false, error: e }) 206 + } 207 + } 208 + 209 + const succeeded = results.filter(r => r.success).length 210 + const failed = results.filter(r => !r.success).length 211 + logger.info(`Queue complete: ${succeeded} succeeded, ${failed} failed`) 212 + } 213 + 214 + // ============================================================================= 215 + // SECTION 6: Demo 216 + // ============================================================================= 217 + // 218 + // Run this and compare with with-purus.ts. 219 + // Notice how there's no way to swap out the logger for testing. 220 + // ============================================================================= 221 + 222 + const main = async () => { 223 + console.log("=== Task Queue (without purus) ===\n") 224 + 225 + const jobs: Job[] = [ 226 + { id: "job-1", type: "email", payload: { to: "user@example.com" } }, 227 + { id: "job-2", type: "image", payload: { path: "/uploads/photo.jpg" } }, 228 + { id: "job-3", type: "sync", payload: { source: "db", target: "cache" } }, 229 + ] 230 + 231 + await processQueue(jobs) 232 + 233 + console.log("\n=== Done ===") 234 + } 235 + 236 + main() 237 + .catch(console.error) 238 + .finally(() => process.exit(0)) 239 + ``` 240 + 241 + 242 + ## Full Code: with-purus.ts 243 + 244 + ```typescript 245 + /** 246 + * Task Queue Example - Concurrency and Dependency Injection 247 + * ========================================================== 248 + * 249 + * This example teaches three advanced purus concepts: 250 + * 251 + * 1. REAL CANCELLATION - When timeout() fires, the job actually stops 252 + * Promise.race just ignores the loser - it keeps running in the background. 253 + * purus calls the cleanup function, which clears the timer and sets cancelled=true. 254 + * 255 + * 2. DEPENDENCY INJECTION - provide() and accessEff() for testability 256 + * The QueueEnv type defines what dependencies the effect needs. 257 + * provide(prodEnv) supplies production logger, provide(testEnv) supplies silent one. 258 + * No DI framework needed - it's just functions and types. 259 + * 260 + * 3. TYPED ERRORS THROUGH THE PIPELINE - No error information lost 261 + * JobError is a union type. Every handler knows exactly what can fail. 262 + * Compare with without-purus.ts where errors become `unknown`. 263 + * 264 + * Compare with without-purus.ts to see: 265 + * - Promise.race that doesn't actually cancel (line 41-48) 266 + * - Hardcoded logger that's awkward to mock (line 19-22) 267 + * - Lost error types through the pipeline 268 + * 269 + * Prerequisites: http-client and workflow-engine examples 270 + * This is the most advanced example - it combines effects, DI, and concurrency. 271 + */ 272 + 273 + import { 274 + type Eff, 275 + Exit, 276 + succeed, 277 + fail, 278 + async, 279 + flatMap, 280 + mapEff, 281 + catchAll, 282 + accessEff, 283 + provide, 284 + timeout, 285 + retry, 286 + allSequential, 287 + pipe, 288 + match, 289 + runPromise, 290 + } from "../../src/index" 291 + 292 + // ============================================================================= 293 + // SECTION 1: Types 294 + // ============================================================================= 295 + // 296 + // Job: The unit of work to process 297 + // JobError: What can go wrong (typed, not `unknown`) 298 + // QueueEnv: Dependencies that the effect requires (injected at runtime) 299 + // ============================================================================= 300 + 301 + type Job = { 302 + id: string 303 + type: "email" | "image" | "sync" 304 + payload: Record<string, unknown> 305 + } 306 + 307 + // Typed errors - we know exactly what can fail 308 + type JobError = 309 + | { readonly _tag: "TransientError"; readonly jobId: string; readonly message: string } 310 + | { readonly _tag: "TimeoutError"; readonly jobId: string; readonly ms: number } 311 + 312 + const JobError = { 313 + transient: (jobId: string, message: string): JobError => 314 + ({ _tag: "TransientError", jobId, message }), 315 + timeout: (jobId: string, ms: number): JobError => 316 + ({ _tag: "TimeoutError", jobId, ms }), 317 + } 318 + 319 + // ============================================================================= 320 + // SECTION 2: Dependency Injection with Environments 321 + // ============================================================================= 322 + // 323 + // THE PROBLEM WITH HARDCODED DEPENDENCIES: 324 + // In without-purus.ts, the logger is a module-level constant. 325 + // To test silently, you'd need to mock the module or set NODE_ENV. 326 + // 327 + // THE SOLUTION - ENVIRONMENT TYPES: 328 + // 1. Define an interface (QueueEnv) that describes what the effect needs 329 + // 2. Use accessEff() to read from the environment 330 + // 3. Use provide() to supply the actual implementation 331 + // 332 + // HOW IT WORKS: 333 + // - Eff<A, E, R> has an R type parameter - the "requirements" 334 + // - processJob returns Eff<void, never, QueueEnv> - it REQUIRES QueueEnv 335 + // - provide(prodEnv) supplies QueueEnv, changing R from QueueEnv to unknown 336 + // 337 + // TESTING: Just swap provide(prodEnv) with provide(testEnv) in tests. 338 + // ============================================================================= 339 + 340 + // Environment type - what the effect requires 341 + type QueueEnv = { 342 + logger: { 343 + info: (msg: string) => void 344 + error: (msg: string) => void 345 + } 346 + } 347 + 348 + // Production environment - logs to console 349 + const prodEnv: QueueEnv = { 350 + logger: { 351 + info: (msg) => console.log(`[INFO] ${msg}`), 352 + error: (msg) => console.log(`[ERROR] ${msg}`), 353 + }, 354 + } 355 + 356 + // Test environment - silent logging for unit tests 357 + // Just swap provide(prodEnv) with provide(testEnv) in tests 358 + const _testEnv: QueueEnv = { 359 + logger: { 360 + info: () => {}, 361 + error: () => {}, 362 + }, 363 + } 364 + 365 + // ============================================================================= 366 + // SECTION 3: Real Cancellation with async() 367 + // ============================================================================= 368 + // 369 + // THE PROBLEM WITH Promise.race: 370 + // In without-purus.ts (line 41-48), Promise.race returns when one settles. 371 + // But THE LOSING PROMISE KEEPS RUNNING! If the job takes 10 seconds and 372 + // timeout is 5 seconds, the job runs for 10 seconds anyway - we just ignore it. 373 + // 374 + // THE SOLUTION - CLEANUP FUNCTIONS: 375 + // async() takes a register function that returns a cleanup function. 376 + // When timeout() fires (or manual interrupt), the cleanup function runs. 377 + // 378 + // HOW IT WORKS: 379 + // 1. Register function starts async work 380 + // 2. Returns cleanup function (clears timer, sets cancelled flag) 381 + // 3. On timeout: cleanup runs -> timer cleared -> no resume called 382 + // 4. The job actually STOPS, not just ignored 383 + // 384 + // GOTCHA: Check the cancelled flag before calling resume. 385 + // If cleanup ran, the fiber is done - calling resume causes problems. 386 + // ============================================================================= 387 + 388 + const executeJob = (job: Job): Eff<void, JobError, QueueEnv> => 389 + async((resume) => { 390 + // IMPORTANT: This flag tracks whether we've been cancelled 391 + // If true, we must NOT call resume - the fiber is already done 392 + let cancelled = false 393 + 394 + const delay = Math.random() * 200 + 50 395 + 396 + const timeoutId = setTimeout(() => { 397 + // CHECK BEFORE RESUME: If cancelled, the fiber is done 398 + // Calling resume after cancellation causes undefined behavior 399 + if (cancelled) return 400 + 401 + // 30% chance of failure for demo purposes 402 + if (Math.random() < 0.3) { 403 + resume(Exit.fail(JobError.transient(job.id, "transient error"))) 404 + } else { 405 + resume(Exit.succeed(undefined)) 406 + } 407 + }, delay) 408 + 409 + // CLEANUP FUNCTION: Called on timeout, interrupt, or race-loser 410 + // This is the key difference from Promise.race - we actually clean up 411 + return () => { 412 + cancelled = true // Signal that we're done 413 + clearTimeout(timeoutId) // Cancel the pending timer 414 + } 415 + }) 416 + 417 + // ============================================================================= 418 + // SECTION 4: Job Pipeline 419 + // ============================================================================= 420 + // 421 + // COMPOSABILITY IN ACTION: 422 + // The pipeline is built from simple, reusable combinators: 423 + // - accessEff() reads the environment for logging 424 + // - retry() wraps with retry logic 425 + // - timeout() adds cancellation timeout 426 + // - catchAll() recovers from errors 427 + // 428 + // Each piece is independent. Want more retries? Change one number. 429 + // Want a longer timeout? Change one number. No restructuring needed. 430 + // ============================================================================= 431 + 432 + const TIMEOUT_MS = 5000 433 + const MAX_RETRIES = 3 434 + 435 + const processJob = (job: Job): Eff<void, never, QueueEnv> => 436 + pipe( 437 + // accessEff gets the environment and runs an effect with it 438 + accessEff((env: QueueEnv) => 439 + pipe( 440 + succeed(undefined), 441 + flatMap(() => ( 442 + env.logger.info(`Processing job ${job.id} (${job.type})`), 443 + executeJob(job) 444 + )) 445 + ) 446 + ), 447 + 448 + // Retry up to MAX_RETRIES times on failure 449 + retry(MAX_RETRIES), 450 + 451 + // Timeout after TIMEOUT_MS - ACTUALLY CANCELS (unlike Promise.race) 452 + timeout(TIMEOUT_MS), 453 + 454 + // Convert timeout (null) to typed error 455 + flatMap((result) => 456 + result === null 457 + ? fail(JobError.timeout(job.id, TIMEOUT_MS)) 458 + : succeed(result) 459 + ), 460 + 461 + // Error recovery - log and continue 462 + // Note: error is JobError, not unknown - we know exactly what failed 463 + catchAll((error: JobError) => 464 + accessEff((env: QueueEnv) => ( 465 + env.logger.error( 466 + match(error)({ 467 + TimeoutError: ({ jobId, ms }) => `Job ${jobId} timed out after ${ms}ms`, 468 + TransientError: ({ jobId, message }) => `Job ${jobId} failed: ${message}`, 469 + }) 470 + ), 471 + succeed(undefined) 472 + )) 473 + ), 474 + 475 + // Final log 476 + flatMap(() => 477 + accessEff((env: QueueEnv) => ( 478 + env.logger.info(`Job ${job.id} completed`), 479 + succeed(undefined) 480 + )) 481 + ) 482 + ) 483 + 484 + // ============================================================================= 485 + // SECTION 5: Queue Processing 486 + // ============================================================================= 487 + // 488 + // allSequential processes jobs one at a time. 489 + // For parallel processing, use `all()` instead. 490 + // ============================================================================= 491 + 492 + const processQueue = (jobs: Job[]): Eff<void, never, QueueEnv> => 493 + pipe( 494 + allSequential(jobs.map(processJob)), 495 + mapEff(() => undefined) 496 + ) 497 + 498 + // ============================================================================= 499 + // SECTION 6: Demo 500 + // ============================================================================= 501 + // 502 + // NOTICE: 503 + // - provide(prodEnv) injects production logger into the entire pipeline 504 + // - For tests, you'd use provide(testEnv) for silent logging 505 + // - No DI framework, no mocking library - just functions and types 506 + // ============================================================================= 507 + 508 + const main = async () => { 509 + console.log("=== Task Queue (with purus) ===\n") 510 + 511 + const jobs: Job[] = [ 512 + { id: "job-1", type: "email", payload: { to: "user@example.com" } }, 513 + { id: "job-2", type: "image", payload: { path: "/uploads/photo.jpg" } }, 514 + { id: "job-3", type: "sync", payload: { source: "db", target: "cache" } }, 515 + ] 516 + 517 + // provide() injects the environment into the effect 518 + // Swap prodEnv with testEnv for unit tests - no code changes needed 519 + const program = pipe( 520 + processQueue(jobs), 521 + provide(prodEnv) // <- Change to testEnv in tests 522 + ) 523 + 524 + await runPromise(program) 525 + 526 + console.log("\n=== Done ===") 527 + } 528 + 529 + main() 530 + .catch(console.error) 531 + .finally(() => process.exit(0)) 532 + ```
+584
docs-site/src/content/docs/examples/workflow-engine.md
··· 1 + --- 2 + title: Workflow Engine 3 + description: Order processing with branded types, typestate, and exhaustive matching. 4 + sidebar: 5 + order: 2 6 + --- 7 + 8 + Order processing where IDs can't be swapped and invalid transitions don't compile. 9 + 10 + ## The Problem 11 + 12 + Business logic has: 13 + - IDs that look alike (OrderId, CustomerId - both strings) 14 + - State transitions (can't ship before paying) 15 + - Error cases you might forget to handle 16 + 17 + In vanilla TypeScript, swapping `orderId` and `customerId` compiles fine. 18 + Shipping an unpaid order throws at runtime. 19 + 20 + ## Run Both Versions 21 + 22 + ```bash 23 + bun run examples/workflow-engine/without-purus.ts 24 + bun run examples/workflow-engine/with-purus.ts 25 + ``` 26 + 27 + ## Without purus 28 + 29 + status !== 'paid')`) 30 + - String matching in error handling 31 + - There's a bug in lookupOrder - can you spot it? 32 + 33 + ## With purus 34 + 35 + See `with-purus.ts`: 36 + - Branded types: `OrderId` and `CustomerId` are incompatible 37 + - Typestate: `shipOrder(order: PaidOrder)` - can't pass a draft 38 + - match() forces you to handle every error case 39 + 40 + ## Key Takeaways 41 + 42 + - Branded types catch ID mixups at compile time 43 + - Typestate turns runtime checks into type errors 44 + - Exhaustive matching means you can't forget error cases 45 + 46 + 47 + ## Full Code: without-purus.ts 48 + 49 + ```typescript 50 + /** 51 + * Workflow Engine - Vanilla TypeScript (Comparison) 52 + * ================================================== 53 + * 54 + * This is the "before" version. Compare with with-purus.ts to see how 55 + * branded types and typestate prevent entire categories of bugs. 56 + * 57 + * PROBLEMS THIS APPROACH HAS: 58 + * 59 + * 1. TYPE ALIASES DON'T PREVENT MIX-UPS 60 + * OrderId, CustomerId, ProductId are all `string`. 61 + * Swap them accidentally? Compiles fine, fails at runtime. 62 + * See line 71 for a live example of this bug. 63 + * 64 + * 2. STATUS FIELD REQUIRES RUNTIME CHECKS 65 + * Every function that cares about state must check `order.status`. 66 + * Forget a check? Runtime error. The compiler can't help. 67 + * 68 + * 3. ERROR HANDLING IS MESSAGE-BASED 69 + * We detect errors by checking if error.message.includes("something"). 70 + * What if someone changes the error message? Silent failure. 71 + * 72 + * THE BIG BUG: Look at buggyLookup() on line 71. 73 + * Arguments are swapped, but TypeScript says nothing. Both are strings. 74 + * 75 + * Prerequisites: Basic TypeScript understanding 76 + * Next: Compare with with-purus.ts to see the fix 77 + */ 78 + 79 + // ============================================================================= 80 + // SECTION 1: Type Aliases (The Problem) 81 + // ============================================================================= 82 + // 83 + // These look like distinct types, but they're all just `string`. 84 + // TypeScript's structural typing means OrderId = CustomerId = ProductId. 85 + // 86 + // The type aliases are DOCUMENTATION ONLY - no compile-time enforcement. 87 + // ============================================================================= 88 + 89 + type OrderId = string 90 + type CustomerId = string 91 + type ProductId = string 92 + 93 + // All three are just strings, so swapping them compiles fine. 94 + // This is a major source of runtime bugs in real codebases. 95 + 96 + // ============================================================================= 97 + // SECTION 2: Status Field (The Problem) 98 + // ============================================================================= 99 + // 100 + // We encode state as a field value, not in the type system. 101 + // Every function must manually check status before proceeding. 102 + // 103 + // PROBLEMS: 104 + // - Forget a check? Runtime error. 105 + // - Add a new status? Find every place that checks status manually. 106 + // - Tests must cover every invalid state combination. 107 + // ============================================================================= 108 + 109 + type OrderStatus = "draft" | "paid" | "shipped" 110 + 111 + type Order = { 112 + id: OrderId 113 + customerId: CustomerId 114 + productId: ProductId 115 + quantity: number 116 + status: OrderStatus // <- State is a field, not part of the type 117 + } 118 + 119 + // ============================================================================= 120 + // SECTION 3: State Transitions with Runtime Checks 121 + // ============================================================================= 122 + // 123 + // NOTICE: Every function starts with a status check. 124 + // If you forget the check, invalid transitions silently succeed until runtime. 125 + // 126 + // Compare with-purus.ts where: 127 + // - payOrder() ONLY accepts DraftOrder (enforced by type system) 128 + // - shipOrder() ONLY accepts PaidOrder (enforced by type system) 129 + // - No runtime checks needed 130 + // ============================================================================= 131 + 132 + const payOrder = (order: Order): Order => { 133 + // RUNTIME CHECK: Must be draft to pay 134 + // What if we forget this? Silent corruption of order state. 135 + if (order.status !== "draft") { 136 + throw new Error(`Cannot pay order in ${order.status} status`) 137 + } 138 + console.log(`[Payment] Processing payment for order ${order.id}`) 139 + return { ...order, status: "paid" } 140 + } 141 + 142 + const shipOrder = (order: Order): Order => { 143 + // RUNTIME CHECK: Must be paid to ship 144 + // Tests must cover "ship draft order" and "ship shipped order" cases 145 + if (order.status !== "paid") { 146 + throw new Error(`Cannot ship order in ${order.status} status`) 147 + } 148 + console.log(`[Shipping] Shipping order ${order.id}`) 149 + return { ...order, status: "shipped" } 150 + } 151 + 152 + // ============================================================================= 153 + // THE SWAPPED ARGUMENTS BUG 154 + // ============================================================================= 155 + // 156 + // This is the most common bug that branded types prevent. 157 + // Both orderId and customerId are strings, so TypeScript can't tell them apart. 158 + // ============================================================================= 159 + 160 + // Both arguments are strings - can you spot the bug below? 161 + const lookupOrder = (orderId: OrderId, customerId: CustomerId): Order | null => { 162 + if (orderId === "order-1" && customerId === "cust-1") { 163 + return { 164 + id: orderId, 165 + customerId: customerId, 166 + productId: "prod-1", 167 + quantity: 2, 168 + status: "draft", 169 + } 170 + } 171 + return null 172 + } 173 + 174 + // ============================================================================= 175 + // !!! THE BUG - THIS COMPILES FINE !!! 176 + // ============================================================================= 177 + // 178 + // The arguments are BACKWARDS. customerId is passed as orderId and vice versa. 179 + // TypeScript sees string, string and says "looks good to me!" 180 + // 181 + // This returns null at runtime when you expect an order. 182 + // In a real app, this could be: wrong customer charged, order sent to wrong address, etc. 183 + // ============================================================================= 184 + 185 + const buggyLookup = () => { 186 + const customerId: CustomerId = "cust-1" 187 + const orderId: OrderId = "order-1" 188 + 189 + // ARGUMENTS ARE SWAPPED! But it compiles because both are strings. 190 + const order = lookupOrder(customerId, orderId) // Oops - wrong order! 191 + return order 192 + } 193 + 194 + // ============================================================================= 195 + // SECTION 4: Error Handling (The Problem) 196 + // ============================================================================= 197 + // 198 + // We detect error types by sniffing the error message. 199 + // 200 + // PROBLEMS: 201 + // - Someone changes "Cannot pay" to "Unable to pay"? Our handling breaks silently. 202 + // - No exhaustive checking - add a new error and we might miss handling it. 203 + // - `unknown` type means we lose all error information. 204 + // ============================================================================= 205 + 206 + const formatError = (error: unknown): string => { 207 + if (error instanceof Error) { 208 + const message = error.message 209 + 210 + // MESSAGE SNIFFING: Fragile and breaks if messages change 211 + if (message.includes("Cannot pay")) { 212 + return "Payment failed: Order is not in draft status" 213 + } 214 + if (message.includes("Cannot ship")) { 215 + return "Shipping failed: Order is not paid" 216 + } 217 + return `Unknown error: ${message}` 218 + } 219 + return "Unknown error" 220 + } 221 + 222 + // ============================================================================= 223 + // SECTION 5: Demo 224 + // ============================================================================= 225 + // 226 + // NOTICE: 227 + // - shipOrder(draftOrder) compiles but throws at runtime (Test 2) 228 + // - buggyLookup() returns null unexpectedly (Test 3) - arguments were swapped 229 + // - Error handling relies on message string matching 230 + // ============================================================================= 231 + 232 + const main = () => { 233 + console.log("=== Workflow Engine (without purus) ===\n") 234 + 235 + // --------------------------------------------------------------------------- 236 + // Test 1: Successful order flow 237 + // Shows: works when you call functions in the right order 238 + // --------------------------------------------------------------------------- 239 + console.log("--- Test 1: Successful order flow ---") 240 + try { 241 + const order: Order = { 242 + id: "order-1", 243 + customerId: "cust-1", 244 + productId: "prod-1", 245 + quantity: 2, 246 + status: "draft", 247 + } 248 + 249 + const paid = payOrder(order) 250 + const shipped = shipOrder(paid) 251 + console.log(`Order ${shipped.id} shipped successfully!\n`) 252 + } catch (e: unknown) { 253 + console.log(formatError(e)) 254 + } 255 + 256 + // --------------------------------------------------------------------------- 257 + // Test 2: Invalid state transition 258 + // Shows: calling shipOrder on a draft order COMPILES but FAILS at runtime 259 + // --------------------------------------------------------------------------- 260 + console.log("--- Test 2: Invalid state transition ---") 261 + try { 262 + const order: Order = { 263 + id: "order-2", 264 + customerId: "cust-1", 265 + productId: "prod-1", 266 + quantity: 1, 267 + status: "draft", 268 + } 269 + 270 + // THIS COMPILES! TypeScript sees Order -> Order and says "fine" 271 + // But it throws at runtime because status !== "paid" 272 + const shipped = shipOrder(order) 273 + console.log(`Shipped: ${shipped.id}\n`) 274 + } catch (e: unknown) { 275 + console.log(`Error: ${formatError(e)}\n`) 276 + } 277 + 278 + // --------------------------------------------------------------------------- 279 + // Test 3: The swapped arguments bug in action 280 + // Shows: buggyLookup returns null because arguments are backwards 281 + // --------------------------------------------------------------------------- 282 + console.log("--- Test 3: Swapped arguments bug ---") 283 + const order = buggyLookup() 284 + if (order === null) { 285 + // This happens because we passed (customerId, orderId) instead of (orderId, customerId) 286 + // In a real app, this could mean: wrong customer billed, order lost, etc. 287 + console.log("Order not found (bug: arguments were swapped!)\n") 288 + } 289 + 290 + console.log("=== Done ===") 291 + } 292 + 293 + main() 294 + process.exit(0) 295 + ``` 296 + 297 + 298 + ## Full Code: with-purus.ts 299 + 300 + ```typescript 301 + /** 302 + * Workflow Engine Example - Type-Safe Business Logic 303 + * =================================================== 304 + * 305 + * This example teaches three advanced purus concepts: 306 + * 307 + * 1. BRANDED TYPES - Create distinct types from primitives 308 + * `type OrderId = string` doesn't prevent swapping OrderId and CustomerId. 309 + * `type OrderId = Branded<string, "OrderId">` does - compiler catches mix-ups. 310 + * 311 + * 2. TYPESTATE - Encode state machines in the type system 312 + * DraftOrder, PaidOrder, ShippedOrder are different types. 313 + * shipOrder(draft) won't compile - you must pay first. 314 + * No runtime status checks needed. 315 + * 316 + * 3. EXHAUSTIVE MATCHING - Handle all error cases or don't compile 317 + * Add a new error variant? Every match() call that doesn't handle it 318 + * becomes a compile error. No silent "forgot to handle this" bugs. 319 + * 320 + * Compare with without-purus.ts to see: 321 + * - The swapped-argument bug that compiles fine (line 71 in that file) 322 + * - Runtime status checks that typestate eliminates 323 + * - Error message sniffing vs typed errors 324 + * 325 + * Prerequisites: http-client example (for basic purus patterns) 326 + * Next: task-queue for concurrency and dependency injection 327 + */ 328 + 329 + import { 330 + type Branded, 331 + type Entity, 332 + type Result, 333 + brand, 334 + entity, 335 + transition, 336 + ok, 337 + err, 338 + match, 339 + matchResult, 340 + } from "../../src/index" 341 + 342 + // ============================================================================= 343 + // SECTION 1: Branded Types 344 + // ============================================================================= 345 + // 346 + // THE PROBLEM WITH TYPE ALIASES: 347 + // type OrderId = string 348 + // type CustomerId = string 349 + // const process = (oid: OrderId, cid: CustomerId) => ... 350 + // process(customerId, orderId) // <-- Compiles! Both are just strings. 351 + // 352 + // THE SOLUTION - BRANDED TYPES: 353 + // Branded<T, B> adds a phantom "brand" that makes types incompatible. 354 + // OrderId and CustomerId are both strings at runtime, but the compiler 355 + // treats them as distinct types. 356 + // 357 + // SMART CONSTRUCTOR PATTERN: 358 + // The OrderId() function creates branded values. You could add validation 359 + // here (e.g., check format, prefix) and return Option<OrderId> for safety. 360 + // ============================================================================= 361 + 362 + type OrderId = Branded<string, "OrderId"> 363 + const OrderId = (s: string): OrderId => brand(s) 364 + 365 + type CustomerId = Branded<string, "CustomerId"> 366 + const CustomerId = (s: string): CustomerId => brand(s) 367 + 368 + type ProductId = Branded<string, "ProductId"> 369 + const ProductId = (s: string): ProductId => brand(s) 370 + 371 + // TRY THIS: Swap the arguments in a function that takes OrderId and CustomerId. 372 + // TypeScript will catch it immediately - no more runtime surprises. 373 + 374 + // ============================================================================= 375 + // SECTION 2: Typestate Pattern 376 + // ============================================================================= 377 + // 378 + // TRADITIONAL STATUS FIELD APPROACH: 379 + // type Order = { status: "draft" | "paid" | "shipped"; ... } 380 + // const ship = (order: Order) => { 381 + // if (order.status !== "paid") throw new Error("...") // Runtime check 382 + // ... 383 + // } 384 + // 385 + // THE PROBLEM: You can call ship(draftOrder) and it compiles. The error 386 + // only happens at runtime. 387 + // 388 + // TYPESTATE APPROACH: 389 + // Each state is a SEPARATE TYPE. DraftOrder, PaidOrder, ShippedOrder. 390 + // - payOrder() accepts DraftOrder, returns PaidOrder 391 + // - shipOrder() accepts PaidOrder, returns ShippedOrder 392 + // - shipOrder(draftOrder) is a COMPILE ERROR - can't even write invalid code 393 + // 394 + // HOW IT WORKS: 395 + // Entity<T, S> uses a phantom type S to track state. 396 + // transition<T, From, To>() creates a function that only accepts Entity<T, From>. 397 + // ============================================================================= 398 + 399 + type OrderData = { 400 + readonly id: OrderId 401 + readonly customerId: CustomerId 402 + readonly productId: ProductId 403 + readonly quantity: number 404 + } 405 + 406 + // Three DISTINCT types - the state IS the type, not a field 407 + type DraftOrder = Entity<OrderData, "Draft"> 408 + type PaidOrder = Entity<OrderData, "Paid"> 409 + type ShippedOrder = Entity<OrderData, "Shipped"> 410 + 411 + // Typed errors - each has specific data for debugging 412 + type OrderError = 413 + | { readonly _tag: "InvalidTransition"; readonly from: string; readonly to: string } 414 + | { readonly _tag: "OrderNotFound"; readonly orderId: OrderId } 415 + | { readonly _tag: "PaymentDeclined"; readonly reason: string } 416 + 417 + const OrderError = { 418 + invalidTransition: (from: string, to: string): OrderError => 419 + ({ _tag: "InvalidTransition", from, to }), 420 + orderNotFound: (orderId: OrderId): OrderError => 421 + ({ _tag: "OrderNotFound", orderId }), 422 + paymentDeclined: (reason: string): OrderError => 423 + ({ _tag: "PaymentDeclined", reason }), 424 + } 425 + 426 + // ============================================================================= 427 + // SECTION 3: State Transitions 428 + // ============================================================================= 429 + // 430 + // KEY INSIGHT: The function signatures ARE the state machine documentation. 431 + // payOrder: (DraftOrder) => Result<PaidOrder, OrderError> 432 + // shipOrder: (PaidOrder) => ShippedOrder 433 + // 434 + // From these signatures alone, you know: 435 + // - You can only pay a draft order 436 + // - Payment can fail (returns Result) 437 + // - You can only ship a paid order 438 + // - Shipping never fails (returns ShippedOrder directly) 439 + // 440 + // TRY THIS: Call shipOrder with a DraftOrder. TypeScript stops you: 441 + // "Argument of type 'DraftOrder' is not assignable to parameter of type 'PaidOrder'" 442 + // 443 + // No runtime status checks. No exceptions. Just types. 444 + // ============================================================================= 445 + 446 + // SIGNATURE: DraftOrder -> Result<PaidOrder, OrderError> 447 + // Only accepts DraftOrder. Try passing a ShippedOrder - won't compile. 448 + const payOrder = (order: DraftOrder): Result<PaidOrder, OrderError> => ( 449 + console.log(`[Payment] Processing payment for order ${order.id}`), 450 + ok(transition<OrderData, "Draft", "Paid">()(order)) 451 + ) 452 + 453 + // SIGNATURE: PaidOrder -> ShippedOrder 454 + // Only accepts PaidOrder. No runtime check needed. 455 + const shipOrder = (order: PaidOrder): ShippedOrder => ( 456 + console.log(`[Shipping] Shipping order ${order.id}`), 457 + transition<OrderData, "Paid", "Shipped">()(order) 458 + ) 459 + 460 + // Branded types in action - compare with without-purus.ts line 71 461 + const lookupOrder = ( 462 + orderId: OrderId, // First parameter is OrderId 463 + customerId: CustomerId // Second parameter is CustomerId 464 + ): Result<DraftOrder, OrderError> => 465 + orderId === "order-1" && customerId === "cust-1" 466 + ? ok(entity<OrderData, "Draft">({ 467 + id: orderId, 468 + customerId: customerId, 469 + productId: ProductId("prod-1"), 470 + quantity: 2, 471 + })) 472 + : err(OrderError.orderNotFound(orderId)) 473 + 474 + // ============================================================================= 475 + // THE BUG THAT BRANDED TYPES PREVENT 476 + // ============================================================================= 477 + // 478 + // In without-purus.ts, this compiles fine: 479 + // lookupOrder(customerId, orderId) // Swapped arguments! 480 + // 481 + // With branded types, TypeScript catches it: 482 + // Argument of type 'CustomerId' is not assignable to parameter of type 'OrderId' 483 + // 484 + // Uncomment the code below to see the error: 485 + // ============================================================================= 486 + 487 + // const buggyLookup = () => { 488 + // const customerId = CustomerId("cust-1") 489 + // const orderId = OrderId("order-1") 490 + // return lookupOrder(customerId, orderId) // Error! Types are swapped. 491 + // } 492 + 493 + // ============================================================================= 494 + // SECTION 4: Error Handling 495 + // ============================================================================= 496 + // 497 + // WHY TYPED ERRORS MATTER: 498 + // Without types, you end up with error.message.includes("not found") - fragile! 499 + // 500 + // With typed errors: 501 + // - match() forces you to handle every variant 502 + // - Add a new error type? All unhandled match() calls become compile errors 503 + // - Each handler receives correctly narrowed type (e.g., { orderId } for NotFound) 504 + // 505 + // TRY THIS: Comment out one case below. TypeScript will complain. 506 + // ============================================================================= 507 + 508 + const formatError = (error: OrderError): string => 509 + match(error)({ 510 + InvalidTransition: ({ from, to }) => 511 + `Cannot transition order from ${from} to ${to}`, 512 + OrderNotFound: ({ orderId }) => 513 + `Order ${orderId} not found`, 514 + PaymentDeclined: ({ reason }) => 515 + `Payment declined: ${reason}`, 516 + }) 517 + 518 + // ============================================================================= 519 + // SECTION 5: Demo 520 + // ============================================================================= 521 + // 522 + // NOTICE: 523 + // - No runtime status checks in the business logic 524 + // - matchResult handles success and error cases explicitly 525 + // - The type system guides you through valid state transitions 526 + // ============================================================================= 527 + 528 + const main = () => { 529 + console.log("=== Workflow Engine (with purus) ===\n") 530 + 531 + // --------------------------------------------------------------------------- 532 + // Test 1: Successful order flow 533 + // Shows: typestate ensures Draft -> Paid -> Shipped sequence 534 + // --------------------------------------------------------------------------- 535 + console.log("--- Test 1: Successful order flow ---") 536 + 537 + const orderId = OrderId("order-1") 538 + const customerId = CustomerId("cust-1") 539 + 540 + const result = lookupOrder(orderId, customerId) 541 + 542 + matchResult<DraftOrder, OrderError, void>( 543 + (draft) => { 544 + // draft is DraftOrder - we can call payOrder 545 + const payResult = payOrder(draft) 546 + matchResult<PaidOrder, OrderError, void>( 547 + (paid) => { 548 + // paid is PaidOrder - we can call shipOrder 549 + const shipped = shipOrder(paid) 550 + console.log(`Order ${shipped.id} shipped successfully!\n`) 551 + }, 552 + (error) => console.log(`Error: ${formatError(error)}\n`) 553 + )(payResult) 554 + }, 555 + (error) => console.log(`Error: ${formatError(error)}\n`) 556 + )(result) 557 + 558 + // --------------------------------------------------------------------------- 559 + // Test 2: Type safety demonstration 560 + // Shows: invalid state transitions are compile errors, not runtime errors 561 + // --------------------------------------------------------------------------- 562 + console.log("--- Test 2: Type safety (compile-time protection) ---") 563 + console.log("The following would be compile errors in purus:") 564 + console.log(" - shipOrder(draftOrder) // Error: DraftOrder not assignable to PaidOrder") 565 + console.log(" - lookupOrder(customerId, orderId) // Error: CustomerId not assignable to OrderId") 566 + console.log("These bugs are caught at compile time, not runtime!\n") 567 + 568 + // --------------------------------------------------------------------------- 569 + // Test 3: Error handling 570 + // Shows: typed errors flow through Result, match() handles them 571 + // --------------------------------------------------------------------------- 572 + console.log("--- Test 3: Order not found ---") 573 + const notFoundResult = lookupOrder(OrderId("invalid"), customerId) 574 + matchResult<DraftOrder, OrderError, void>( 575 + (draft) => console.log(`Found: ${draft.id}`), 576 + (error) => console.log(`Error: ${formatError(error)}\n`) 577 + )(notFoundResult) 578 + 579 + console.log("=== Done ===") 580 + } 581 + 582 + main() 583 + process.exit(0) 584 + ```
+90
docs-site/src/content/docs/index.mdx
··· 1 + --- 2 + title: purus-ts 3 + description: Pure functional programming in TypeScript — effects, fibers, branded types, and more. 4 + template: splash 5 + hero: 6 + title: purus-ts 7 + tagline: Pure functional programming in TypeScript — effects, fibers, branded types, and more. 8 + actions: 9 + - text: Start the Tutorial 10 + link: /tutorial/ 11 + icon: right-arrow 12 + variant: primary 13 + - text: Read the Stories 14 + link: /stories/ 15 + variant: minimal 16 + --- 17 + 18 + import { Card, CardGrid, LinkCard } from "@astrojs/starlight/components"; 19 + 20 + ## Choose Your Path 21 + 22 + <CardGrid> 23 + <Card title="Tutorial" icon="open-book"> 24 + Build a complete app step-by-step, learning one concept at a time. **~2-3 25 + hours.** 26 + 27 + [Start learning →](/tutorial/) 28 + </Card> 29 + 30 + <Card title="Concepts" icon="puzzle"> 31 + Deep-dives into specific topics. Each article is self-contained with 32 + real-world examples. 33 + 34 + [Browse concepts →](/concepts/) 35 + </Card> 36 + 37 + <Card title="Stories" icon="star"> 38 + Narrative-driven tutorials where animal characters discover FP concepts by 39 + building real software. 40 + 41 + [Read stories →](/stories/) 42 + </Card> 43 + 44 + <Card title="Examples" icon="laptop"> 45 + Side-by-side comparisons: vanilla TypeScript vs. purus-ts in real-world 46 + scenarios. 47 + 48 + [See examples →](/examples/) 49 + </Card> 50 + </CardGrid> 51 + 52 + --- 53 + 54 + ## Quick Reference 55 + 56 + | Problem | Solution | 57 + | ----------------------------------- | ------------------------ | 58 + | "This function can fail" | `Result<T, E>` | 59 + | "This value might not exist" | `Option<T>` | 60 + | "I need to do async work" | `Eff<A, E, R>` | 61 + | "I don't want to mix up IDs" | `Branded<T, B>` | 62 + | "I need to inject dependencies" | `provide()` / `access()` | 63 + | "I need timeout/retry/cancellation" | Effect combinators | 64 + 65 + ## Import Patterns 66 + 67 + ```typescript 68 + // Core types and functions 69 + import { 70 + type Result, ok, err, matchResult, chainResult, mapResult, 71 + traverseResult, sequenceResult, orElseResult, fromNullableResult, 72 + type Option, some, none, matchOption, 73 + traverseOption, sequenceOption, orElseOption, 74 + type Eff, succeed, fail, flatMap, pipe, 75 + type Branded, brand, 76 + } from "purus-ts"; 77 + 78 + // Effect runners 79 + import { runPromise, runPromiseExit } from "purus-ts"; 80 + 81 + // Effect combinators 82 + import { zip, zipWith, zip3, ensure, tapErr, bracket } from "purus-ts"; 83 + import { timeout, retry, race, all } from "purus-ts"; 84 + 85 + // Validation 86 + import { valid, invalid, validate2, validate3, validate4 } from "purus-ts"; 87 + 88 + // Pattern matching (custom discriminant) 89 + import { match, matchOn, matchOnOr } from "purus-ts"; 90 + ```
+66
docs-site/src/content/docs/stories/index.mdx
··· 1 + --- 2 + title: Stories 3 + description: "Narrative-driven functional programming tutorials with whimsical animal characters." 4 + --- 5 + 6 + import { Card, CardGrid } from "@astrojs/starlight/components"; 7 + 8 + **Stories** are narrative-driven functional programming tutorials. Each story is a whimsical tale where animal characters discover and build the same concepts you'll use in production code. 9 + 10 + --- 11 + 12 + ## Why Stories? 13 + 14 + Traditional tutorials show you _what_ to type. Stories show you _why_ these patterns exist — through characters who face the same problems you do and discover the same solutions. 15 + 16 + When Rabbit worries "But what if someone votes twice?", you feel the problem. When Owl scratches out a `BallotError` type, you understand why typed errors matter. The code isn't an afterthought; it's woven into the narrative as the characters write it. 17 + 18 + --- 19 + 20 + ## How to Read 21 + 22 + 1. **Follow the narrative** — Let the story carry you through the concepts 23 + 2. **Read the code** — Characters write real, runnable TypeScript 24 + 3. **Run the example** — Each story has a matching file in `examples/stories/` 25 + 4. **Try variations** — Modify the example to solidify your understanding 26 + 27 + --- 28 + 29 + ## Available Stories 30 + 31 + ### [The Forest Election](/stories/forest-election/) 32 + 33 + A trilogy about building a fair election system for the forest. 34 + 35 + | Part | Title | Concept | Status | 36 + | ---- | ----------------------------------------------------------------------------- | ---------- | --------- | 37 + | 1 | [The Ballot Box Problem](/stories/forest-election/01-the-ballot-box-problem/) | Validation | Available | 38 + | 2 | [Counting Day](/stories/forest-election/02-counting-day/) | Result | Available | 39 + | 3 | [The Announcement](/stories/forest-election/03-the-announcement/) | Effects | Available | 40 + 41 + **Tone:** Cozy and earnest (think Frog and Toad, Winnie the Pooh). The animals genuinely care about building fair software. 42 + 43 + **Characters:** Owl (methodical thinker), Beaver (eager builder), Fox (pragmatic skeptic), Rabbit (anxious but thorough), Badger (wise elder). 44 + 45 + --- 46 + 47 + ### [Beaver's Big System](/stories/beavers-big-system/) 48 + 49 + A sequel trilogy about building maintainable maintenance software. 50 + 51 + | Part | Title | Concepts | Status | 52 + | ---- | ------------------------------------------------------------------------ | ---------------------------- | --------- | 53 + | 1 | [The Mixup](/stories/beavers-big-system/01-the-mixup/) | Branded Types + Typestate | Available | 54 + | 2 | [The Categorization](/stories/beavers-big-system/02-the-categorization/) | ADTs + Pattern Matching | Available | 55 + | 3 | [The Queue](/stories/beavers-big-system/03-the-queue/) | Tracked Arrays + Refinements | Available | 56 + 57 + **Setup:** After the election, Beaver builds "The System" — a work order tracker. But strings everywhere lead to chaos: mixed-up IDs, invalid states, unsorted queues. Owl guides the rebuild. 58 + 59 + **Same characters** continue their journey, learning compile-time safety through infrastructure problems. 60 + 61 + --- 62 + 63 + ## See Also 64 + 65 + - [Tutorial](/tutorial/) — Step-by-step guide building a complete application 66 + - [Concepts](/concepts/) — Deep-dive reference articles
+20
docs-site/src/styles/custom.css
··· 1 + /* purus-ts docs — global overrides */ 2 + 3 + :root { 4 + --sl-font: "Segoe UI", system-ui, -apple-system, sans-serif; 5 + --sl-font-mono: "Fira Code", "JetBrains Mono", "Cascadia Code", ui-monospace, 6 + monospace; 7 + --sl-color-accent-low: #1a2744; 8 + --sl-color-accent: #3b82f6; 9 + --sl-color-accent-high: #c7d8f7; 10 + } 11 + 12 + /* Slightly wider content area */ 13 + .sl-container { 14 + max-width: 55rem; 15 + } 16 + 17 + /* Softer code blocks */ 18 + :root { 19 + --sl-color-gray-6: #f8fafc; 20 + }
+123
docs-site/src/styles/stories.css
··· 1 + /* Children's book styling for story chapters */ 2 + 3 + .story-chapter { 4 + --story-bg: #fdf6e3; 5 + --story-text: #3b2e1e; 6 + --story-heading: #5b3a1a; 7 + --story-accent: #8b6914; 8 + --story-border: #d4c4a0; 9 + --story-code-bg: #f5eed6; 10 + --story-blockquote-bg: #faf3dc; 11 + 12 + background: var(--story-bg); 13 + color: var(--story-text); 14 + font-size: 1.15rem; 15 + line-height: 1.85; 16 + max-width: 42rem; 17 + margin: 0 auto; 18 + padding: 2rem 1.5rem; 19 + border-radius: 0.75rem; 20 + } 21 + 22 + /* Headings */ 23 + .story-chapter h2, 24 + .story-chapter h3, 25 + .story-chapter h4 { 26 + color: var(--story-heading); 27 + font-weight: 600; 28 + } 29 + 30 + .story-chapter h2 { 31 + font-size: 1.6rem; 32 + margin-top: 2.5rem; 33 + } 34 + 35 + .story-chapter h3 { 36 + font-size: 1.3rem; 37 + margin-top: 2rem; 38 + } 39 + 40 + /* Scene breaks — decorative ~ ~ ~ */ 41 + .story-chapter hr { 42 + border: none; 43 + text-align: center; 44 + margin: 2.5rem 0; 45 + } 46 + 47 + .story-chapter hr::after { 48 + content: "~ ~ ~"; 49 + color: var(--story-accent); 50 + font-size: 1.2rem; 51 + letter-spacing: 0.5em; 52 + } 53 + 54 + /* Epigraph-style blockquotes */ 55 + .story-chapter blockquote { 56 + background: var(--story-blockquote-bg); 57 + border-left: 3px solid var(--story-accent); 58 + border-radius: 0 0.5rem 0.5rem 0; 59 + padding: 1rem 1.25rem; 60 + margin: 1.5rem 0; 61 + font-style: italic; 62 + color: var(--story-heading); 63 + } 64 + 65 + /* Softer code blocks */ 66 + .story-chapter pre { 67 + background: var(--story-code-bg) !important; 68 + border: 1px solid var(--story-border); 69 + border-radius: 0.75rem; 70 + padding: 1.25rem; 71 + } 72 + 73 + .story-chapter code { 74 + font-size: 0.92em; 75 + } 76 + 77 + .story-chapter :not(pre) > code { 78 + background: var(--story-code-bg); 79 + border: 1px solid var(--story-border); 80 + border-radius: 0.25rem; 81 + padding: 0.15em 0.35em; 82 + color: var(--story-heading); 83 + } 84 + 85 + /* Illustration placeholder styling */ 86 + .story-illustration { 87 + border: 2px dashed var(--story-border); 88 + border-radius: 0.75rem; 89 + padding: 2rem; 90 + margin: 2rem 0; 91 + text-align: center; 92 + background: var(--story-blockquote-bg); 93 + } 94 + 95 + .story-illustration .icon { 96 + font-size: 2rem; 97 + margin-bottom: 0.5rem; 98 + opacity: 0.5; 99 + } 100 + 101 + .story-illustration .alt-text { 102 + font-style: italic; 103 + color: var(--story-accent); 104 + font-size: 0.95rem; 105 + } 106 + 107 + .story-illustration .caption { 108 + font-size: 0.85rem; 109 + color: var(--story-text); 110 + opacity: 0.7; 111 + margin-top: 0.5rem; 112 + } 113 + 114 + /* Dark mode adjustments */ 115 + :root[data-theme="dark"] .story-chapter { 116 + --story-bg: #2a2318; 117 + --story-text: #e8dcc8; 118 + --story-heading: #f0d9a8; 119 + --story-accent: #c9a84c; 120 + --story-border: #4a3d28; 121 + --story-code-bg: #342b1e; 122 + --story-blockquote-bg: #302719; 123 + }
+3
docs-site/tsconfig.json
··· 1 + { 2 + "extends": "astro/tsconfigs/strict" 3 + }
-114
docs/guides/README.md
··· 1 - # purus-ts Guides 2 - 3 - Learn purus-ts through progressive tutorials and concept deep-dives. 4 - 5 - ## Learning Paths 6 - 7 - ### New to Functional TypeScript? 8 - 9 - Start with the [Tutorial](./tutorial/) - it builds a complete application step-by-step, introducing one concept at a time. By the end, you'll understand: 10 - 11 - - Why errors as values beat exceptions 12 - - How branded types prevent bugs at compile time 13 - - How the effect system makes async code composable 14 - - How dependency injection works without frameworks 15 - 16 - ### Already Know FP Basics? 17 - 18 - Jump to the [Concepts](./concepts/) section for deep-dives into specific topics. Each article is self-contained and includes real-world examples. 19 - 20 - ### Prefer Learning Through Narrative? 21 - 22 - Try the [Stories](./stories/) — whimsical tales where animal characters discover functional programming concepts by building real software. Each story is a gentle introduction that weaves code into narrative. 23 - 24 - --- 25 - 26 - ## [Tutorial](./tutorial/) 27 - 28 - Build a complete application step-by-step, learning one concept at a time. 29 - 30 - **Duration:** ~2-3 hours total 31 - **Prerequisites:** Basic TypeScript knowledge 32 - 33 - | Chapter | Topic | What You'll Learn | 34 - |---------|-----------------------------|-----------------------------------------------| 35 - | 1 | Why Functional TypeScript? | The problem with exceptions, errors as values | 36 - | 2 | Your First Effect | Creating, running, and transforming effects | 37 - | 3 | Typed Errors with Result | Ok/Err pattern, error composition | 38 - | 4 | Optional Values with Option | Some/None, avoiding null checks | 39 - | 5 | Pattern Matching | Exhaustive matching, type narrowing | 40 - | 6 | Branded Types | Distinct types from primitives | 41 - | 7 | The Effect System | Eff type, lazy evaluation, composition | 42 - | 8 | Concurrency with Fibers | Fork, join, race, cancellation | 43 - | 9 | Dependency Injection | Environments, provide, access | 44 - | 10 | Building a Complete App | Putting it all together | 45 - 46 - --- 47 - 48 - ## [Concepts](./concepts/) 49 - 50 - Deep-dives into specific topics when you need more detail. 51 - 52 - | Article | Description | 53 - |---------------------------------------------------------|--------------------------------------------------------| 54 - | [Why These Strange Names?](./concepts/00-why-these-strange-names.md) | Demystifying FP terminology | 55 - | [Why Errors as Values?](./concepts/01-errors-as-values.md) | The exception problem, railway oriented programming | 56 - | [Branded Types In Depth](./concepts/02-branded-types.md) | Phantom types, smart constructors, production patterns | 57 - | [Typestate Pattern](./concepts/05-typestate-pattern.md) | State machines in the type system | 58 - | [Effect Composition](./concepts/06-effect-composition.md) | Building complex effects from simple ones | 59 - | [Fiber Internals](./concepts/07-fiber-internals.md) | How the runtime works under the hood | 60 - | [Dependency Injection Patterns](./concepts/08-dependency-injection-patterns.md) | Testing, layered environments | 61 - | [Testing Strategies](./concepts/09-testing-strategies.md) | Unit testing effectful code | 62 - 63 - --- 64 - 65 - ## [Stories](./stories/) 66 - 67 - Learn through narrative — whimsical tales where animal characters discover FP concepts. 68 - 69 - | Story | Concept | Description | 70 - |--------------------------------------------------------------------|--------------------------|------------------------------------------------| 71 - | [The Forest Election](./stories/forest-election/) | Validation, Result, Effects | Building a fair election system for the forest | 72 - | [Beaver's Big System](./stories/beavers-big-system/) | Branded Types, Typestate, ADTs, Tracked Arrays | Building maintainable maintenance software | 73 - 74 - --- 75 - 76 - ## Quick Reference 77 - 78 - ### When to Use What 79 - 80 - | Problem | Solution | 81 - |-------------------------------------|--------------------------| 82 - | "This function can fail" | `Result<T, E>` | 83 - | "This value might not exist" | `Option<T>` | 84 - | "I need to do async work" | `Eff<A, E, R>` | 85 - | "I don't want to mix up IDs" | `Branded<T, B>` | 86 - | "I need to inject dependencies" | `provide()` / `access()` | 87 - | "I need timeout/retry/cancellation" | Effect combinators | 88 - 89 - ### Import Patterns 90 - 91 - ```typescript 92 - // Core types and functions 93 - import { 94 - type Result, ok, err, matchResult, chainResult, mapResult, 95 - traverseResult, sequenceResult, orElseResult, fromNullableResult, 96 - type Option, some, none, matchOption, 97 - traverseOption, sequenceOption, orElseOption, 98 - type Eff, succeed, fail, flatMap, pipe, 99 - type Branded, brand, 100 - } from "purus-ts" 101 - 102 - // Effect runners 103 - import { runPromise, runPromiseExit } from "purus-ts" 104 - 105 - // Effect combinators 106 - import { zip, zipWith, zip3, ensure, tapErr, bracket } from "purus-ts" 107 - import { timeout, retry, race, all } from "purus-ts" 108 - 109 - // Validation 110 - import { valid, invalid, validate2, validate3, validate4 } from "purus-ts" 111 - 112 - // Pattern matching (custom discriminant) 113 - import { match, matchOn, matchOnOr } from "purus-ts" 114 - ```
+10 -5
docs/guides/concepts/00-why-these-strange-names.md docs-site/src/content/docs/concepts/00-why-these-strange-names.md
··· 1 - # Why These Strange Names? 1 + --- 2 + title: Why These Strange Names? 3 + description: Demystifying Functor, Monad, and other FP terminology with real-world analogies. 4 + sidebar: 5 + order: 0 6 + --- 2 7 3 8 If you've encountered terms like "Monad" or "Functor" and felt intimidated, you're not alone. This article demystifies all the FP terminology used in purus-ts before you dive into the concepts. 4 9 ··· 393 398 394 399 Now that you know what the names mean, dive into the details: 395 400 396 - - [Why Errors as Values?](./01-errors-as-values.md) - The foundation: Result type 397 - - [Branded Types In Depth](./02-branded-types.md) - Compile-time type safety 398 - - [Validation and Error Accumulation](./03-validation-and-error-accumulation.md) - Applicative in action 399 - - [Type Classes in TypeScript](./04-type-classes-in-typescript.md) - Functor, Applicative, Monad patterns 401 + - [Why Errors as Values?](/concepts/01-errors-as-values/) - The foundation: Result type 402 + - [Branded Types In Depth](/concepts/02-branded-types/) - Compile-time type safety 403 + - [Validation and Error Accumulation](/concepts/03-validation-and-error-accumulation/) - Applicative in action 404 + - [Type Classes in TypeScript](/concepts/04-type-classes-in-typescript/) - Functor, Applicative, Monad patterns
+9 -4
docs/guides/concepts/01-errors-as-values.md docs-site/src/content/docs/concepts/01-errors-as-values.md
··· 1 - # Why Errors as Values? 1 + --- 2 + title: Why Errors as Values? 3 + description: The fundamental shift from exceptions to typed errors and railway-oriented programming. 4 + sidebar: 5 + order: 1 6 + --- 2 7 3 8 This article explains the fundamental shift from throwing exceptions to returning typed errors, and why this change makes your TypeScript code more reliable. 4 9 ··· 449 454 450 455 ## See Also 451 456 452 - - [Tutorial Chapter 1: Why Functional TypeScript?](../tutorial/01-why-functional-typescript.md) - Introduction with examples 453 - - [Tutorial Chapter 3: Typed Errors with Result](../tutorial/03-typed-errors-with-result.md) - Hands-on practice 454 - - [Branded Types In Depth](./02-branded-types.md) - Another compile-time safety technique 457 + - [Tutorial Chapter 1: Why Functional TypeScript?](/tutorial/01-why-functional-typescript/) - Introduction with examples 458 + - [Tutorial Chapter 3: Typed Errors with Result](/tutorial/03-typed-errors-with-result/) - Hands-on practice 459 + - [Branded Types In Depth](/concepts/02-branded-types/) - Another compile-time safety technique
+9 -4
docs/guides/concepts/02-branded-types.md docs-site/src/content/docs/concepts/02-branded-types.md
··· 1 - # Branded Types In Depth 1 + --- 2 + title: Branded Types In Depth 3 + description: Phantom types, smart constructors, and production patterns for compile-time safety. 4 + sidebar: 5 + order: 2 6 + --- 2 7 3 8 This article explains how to create distinct types from primitives, preventing an entire category of bugs at compile time. 4 9 ··· 435 440 436 441 ## See Also 437 442 438 - - [Tutorial Chapter 6: Branded Types](../tutorial/06-branded-types.md) - Hands-on introduction 439 - - [Workflow Engine Example](../../../examples/workflow-engine/) - Branded types in action 440 - - [Typestate Pattern](./05-typestate-pattern.md) - Encoding state machines with phantom types 443 + - [Tutorial Chapter 6: Branded Types](/tutorial/06-branded-types/) - Hands-on introduction 444 + - [Workflow Engine Example](/examples/workflow-engine/) - Branded types in action 445 + - [Typestate Pattern](/concepts/05-typestate-pattern/) - Encoding state machines with phantom types
+10 -5
docs/guides/concepts/03-validation-and-error-accumulation.md docs-site/src/content/docs/concepts/03-validation-and-error-accumulation.md
··· 1 - # Validation and Error Accumulation 1 + --- 2 + title: Validation and Error Accumulation 3 + description: Collecting all validation errors instead of stopping at the first one. 4 + sidebar: 5 + order: 3 6 + --- 2 7 3 8 This article explains why Result's short-circuit behavior isn't always what you want, and how the Validation type accumulates all errors instead of stopping at the first. 4 9 ··· 495 500 form => console.log("Valid registration:", form), 496 501 errors => { 497 502 console.log("Validation errors:") 498 - errors.forEach(e => console.log(` - ${formatError(e)}`)) 503 + console.log(errors.map(e => ` - ${formatError(e)}`).join("\n")) 499 504 } 500 505 )(result) 501 506 ··· 588 593 589 594 ## See Also 590 595 591 - - [Tutorial Chapter 3: Typed Errors with Result](../tutorial/03-typed-errors-with-result.md) - Result fundamentals 592 - - [Why Errors as Values?](./01-errors-as-values.md) - The philosophy behind typed errors 593 - - [Type Classes in TypeScript](./04-type-classes-in-typescript.md) - Monads vs Applicatives explained 596 + - [Tutorial Chapter 3: Typed Errors with Result](/tutorial/03-typed-errors-with-result/) - Result fundamentals 597 + - [Why Errors as Values?](/concepts/01-errors-as-values/) - The philosophy behind typed errors 598 + - [Type Classes in TypeScript](/concepts/04-type-classes-in-typescript/) - Monads vs Applicatives explained
+11 -6
docs/guides/concepts/04-type-classes-in-typescript.md docs-site/src/content/docs/concepts/04-type-classes-in-typescript.md
··· 1 - # Type Classes in TypeScript 1 + --- 2 + title: Type Classes in TypeScript 3 + description: Understanding Functor, Applicative, Monad, and Bifunctor patterns. 4 + sidebar: 5 + order: 4 6 + --- 2 7 3 8 This article explains functional programming type class patterns and how purus implements them within TypeScript's type system limitations. 4 9 ··· 120 125 121 126 **purus opts for simplicity:** explicit functions per type (`mapResult`, `mapOption`, `mapEff`) with consistent patterns. You lose some abstraction but gain immediate readability. 122 127 123 - > **New to FP terminology?** Read [Why These Strange Names?](./00-why-these-strange-names.md) first for plain-English explanations and real-world analogies. 128 + > **New to FP terminology?** Read [Why These Strange Names?](/concepts/00-why-these-strange-names/) first for plain-English explanations and real-world analogies. 124 129 125 130 --- 126 131 ··· 704 709 705 710 ## See Also 706 711 707 - - [Validation and Error Accumulation](./03-validation-and-error-accumulation.md) - Applicative in action 708 - - [Why Errors as Values?](./01-errors-as-values.md) - Result fundamentals 709 - - [Effect Composition](./06-effect-composition.md) - How effects compose internally 710 - - [Tutorial Chapter 3: Typed Errors with Result](../tutorial/03-typed-errors-with-result.md) - Hands-on Result practice 712 + - [Validation and Error Accumulation](/concepts/03-validation-and-error-accumulation/) - Applicative in action 713 + - [Why Errors as Values?](/concepts/01-errors-as-values/) - Result fundamentals 714 + - [Effect Composition](/concepts/06-effect-composition/) - How effects compose internally 715 + - [Tutorial Chapter 3: Typed Errors with Result](/tutorial/03-typed-errors-with-result/) - Hands-on Result practice
+8 -3
docs/guides/concepts/05-typestate-pattern.md docs-site/src/content/docs/concepts/05-typestate-pattern.md
··· 1 - # Typestate Pattern 1 + --- 2 + title: Typestate Pattern 3 + description: "Encoding state machines in the type system so invalid states don't compile." 4 + sidebar: 5 + order: 5 6 + --- 2 7 3 8 This article explains how to encode state machines in the type system, making invalid state transitions impossible at compile time. 4 9 ··· 319 324 320 325 ## See Also 321 326 322 - - [Branded Types In Depth](./02-branded-types.md) - Foundation for phantom types 323 - - [Workflow Engine Example](../../../examples/workflow-engine/) - Full order processing system 327 + - [Branded Types In Depth](/concepts/02-branded-types/) - Foundation for phantom types 328 + - [Workflow Engine Example](/examples/workflow-engine/) - Full order processing system
+9 -4
docs/guides/concepts/06-effect-composition.md docs-site/src/content/docs/concepts/06-effect-composition.md
··· 1 - # Effect Composition 1 + --- 2 + title: Effect Composition 3 + description: Building complex effects from simple building blocks with flatMap and pipe. 4 + sidebar: 5 + order: 6 6 + --- 2 7 3 8 This article explains how purus effects compose together, building complex programs from simple building blocks. 4 9 ··· 438 443 439 444 ## See Also 440 445 441 - - [Why Errors as Values?](./01-errors-as-values.md) - Railway oriented programming 442 - - [Fiber Internals](./07-fiber-internals.md) - How effects actually execute 443 - - [HTTP Client Example](../../../examples/http-client/) - Real-world effect composition 446 + - [Why Errors as Values?](/concepts/01-errors-as-values/) - Railway oriented programming 447 + - [Fiber Internals](/concepts/07-fiber-internals/) - How effects actually execute 448 + - [HTTP Client Example](/examples/http-client/) - Real-world effect composition
+8 -3
docs/guides/concepts/07-fiber-internals.md docs-site/src/content/docs/concepts/07-fiber-internals.md
··· 1 - # Fiber Internals 1 + --- 2 + title: Fiber Internals 3 + description: How the purus runtime executes effects with trampolining and scheduling. 4 + sidebar: 5 + order: 7 6 + --- 2 7 3 8 This article explains how purus executes effects under the hood: the trampoline pattern, stack safety, and fiber lifecycle. 4 9 ··· 337 342 338 343 ## See Also 339 344 340 - - [Effect Composition](./06-effect-composition.md) - How effects combine 341 - - [Testing Strategies](./09-testing-strategies.md) - Using Exit for assertions 345 + - [Effect Composition](/concepts/06-effect-composition/) - How effects combine 346 + - [Testing Strategies](/concepts/09-testing-strategies/) - Using Exit for assertions
+8 -3
docs/guides/concepts/08-dependency-injection-patterns.md docs-site/src/content/docs/concepts/08-dependency-injection-patterns.md
··· 1 - # Dependency Injection Patterns 1 + --- 2 + title: Dependency Injection Patterns 3 + description: Testable code without frameworks using the Reader pattern and environments. 4 + sidebar: 5 + order: 8 6 + --- 2 7 3 8 This article explains how to write testable code using purus's built-in dependency injection: `access`, `accessEff`, and `provide`. 4 9 ··· 388 393 389 394 ## See Also 390 395 391 - - [Testing Strategies](./09-testing-strategies.md) - More testing patterns 392 - - [HTTP Client Example](../../../examples/http-client/) - DI in practice 396 + - [Testing Strategies](/concepts/09-testing-strategies/) - More testing patterns 397 + - [HTTP Client Example](/examples/http-client/) - DI in practice
+15 -12
docs/guides/concepts/09-testing-strategies.md docs-site/src/content/docs/concepts/09-testing-strategies.md
··· 1 - # Testing Strategies 1 + --- 2 + title: Testing Strategies 3 + description: Unit testing effectful code with mock environments and runPromiseExit. 4 + sidebar: 5 + order: 9 6 + --- 2 7 3 8 This article covers practical patterns for testing code that uses purus: pure functions, effects, error paths, and concurrent operations. 4 9 ··· 380 385 ### 3. Test Error Types Exhaustively 381 386 382 387 ```typescript 383 - test("handles all error types", async () => { 384 - const errorTypes: FetchError["_tag"][] = ["NetworkError", "NotFound", "Unauthorized"] 388 + const errorTypes: FetchError["_tag"][] = ["NetworkError", "NotFound", "Unauthorized"] 385 389 386 - for (const errorType of errorTypes) { 387 - const env = createEnvThatProduces(errorType) 388 - const exit = await runPromiseExit(pipe(fetchUser("id"), provide(env))) 390 + test.each(errorTypes)("handles %s", async (errorType) => { 391 + const env = createEnvThatProduces(errorType) 392 + const exit = await runPromiseExit(pipe(fetchUser("id"), provide(env))) 389 393 390 - expect(exit._tag).toBe("Failure") 391 - if (exit._tag === "Failure") { 392 - expect(exit.error._tag).toBe(errorType) 393 - } 394 + expect(exit._tag).toBe("Failure") 395 + if (exit._tag === "Failure") { 396 + expect(exit.error._tag).toBe(errorType) 394 397 } 395 398 }) 396 399 ``` ··· 435 438 436 439 ## See Also 437 440 438 - - [Dependency Injection Patterns](./08-dependency-injection-patterns.md) - Creating mock environments 439 - - [Effect Composition](./06-effect-composition.md) - Understanding what you're testing 441 + - [Dependency Injection Patterns](/concepts/08-dependency-injection-patterns/) - Creating mock environments 442 + - [Effect Composition](/concepts/06-effect-composition/) - Understanding what you're testing
+34 -14
docs/guides/concepts/README.md docs-site/src/content/docs/concepts/index.md
··· 1 - # purus-ts Concepts 1 + --- 2 + title: Concepts 3 + description: Deep-dives into specific topics when you need more detail. 4 + --- 2 5 3 6 Deep-dives into specific topics when you need more detail. 4 7 ··· 8 11 9 12 ## Articles 10 13 11 - ### [00 - Why These Strange Names?](./00-why-these-strange-names.md) 14 + ### [00 - Why These Strange Names?](/concepts/00-why-these-strange-names/) 12 15 13 16 Demystifying Functor, Monad, and other FP terminology. 14 17 ··· 22 25 23 26 --- 24 27 25 - ### [01 - Why Errors as Values?](./01-errors-as-values.md) 28 + ### [01 - Why Errors as Values?](/concepts/01-errors-as-values/) 26 29 27 30 The fundamental shift from exceptions to typed errors. 28 31 ··· 37 40 38 41 --- 39 42 40 - ### [02 - Branded Types In Depth](./02-branded-types.md) 43 + ### [02 - Branded Types In Depth](/concepts/02-branded-types/) 41 44 42 45 Creating distinct types from primitives for compile-time safety. 43 46 ··· 52 55 53 56 --- 54 57 55 - ### [03 - Validation and Error Accumulation](./03-validation-and-error-accumulation.md) 58 + ### [03 - Validation and Error Accumulation](/concepts/03-validation-and-error-accumulation/) 56 59 57 60 Collecting all validation errors instead of stopping at the first. 58 61 ··· 67 70 68 71 --- 69 72 70 - ### [04 - Type Classes in TypeScript](./04-type-classes-in-typescript.md) 73 + ### [04 - Type Classes in TypeScript](/concepts/04-type-classes-in-typescript/) 71 74 72 75 Understanding Functor, Applicative, Monad, and Bifunctor patterns. 73 76 ··· 82 85 83 86 --- 84 87 85 - ### [05 - Typestate Pattern](./05-typestate-pattern.md) 88 + ### [05 - Typestate Pattern](/concepts/05-typestate-pattern/) 86 89 87 90 Encoding state machines in the type system. 88 91 ··· 97 100 98 101 --- 99 102 100 - ### [06 - Effect Composition](./06-effect-composition.md) 103 + ### [06 - Effect Composition](/concepts/06-effect-composition/) 101 104 102 105 Building complex effects from simple building blocks. 103 106 ··· 112 115 113 116 --- 114 117 115 - ### [07 - Fiber Internals](./07-fiber-internals.md) 118 + ### [07 - Fiber Internals](/concepts/07-fiber-internals/) 116 119 117 120 How the purus runtime executes effects. 118 121 ··· 127 130 128 131 --- 129 132 130 - ### [08 - Dependency Injection Patterns](./08-dependency-injection-patterns.md) 133 + ### [08 - Dependency Injection Patterns](/concepts/08-dependency-injection-patterns/) 131 134 132 135 Testable code without frameworks. 133 136 ··· 142 145 143 146 --- 144 147 145 - ### [09 - Testing Strategies](./09-testing-strategies.md) 148 + ### [09 - Testing Strategies](/concepts/09-testing-strategies/) 146 149 147 150 Unit testing code that uses purus. 148 151 ··· 157 160 158 161 --- 159 162 163 + ### [10 - Recursion Over Loops](/concepts/10-recursion-over-loops/) 164 + 165 + Why pure functional code avoids loops and how to replace them with expressions. 166 + 167 + **Topics Covered:** 168 + - Expressions vs statements 169 + - Why forEach returns void (and why that matters) 170 + - map, filter, reduce as loop replacements 171 + - The go() pattern for complex recursion 172 + - Head-tail decomposition 173 + - Performance considerations 174 + 175 + **Best For:** Understanding the no-loops philosophy, converting imperative patterns. 176 + 177 + --- 178 + 160 179 ## Quick Concept Reference 161 180 162 181 | Concept | One-Liner | ··· 170 189 | Fiber | Lightweight thread with cancellation | 171 190 | Environment | Dependencies as a type parameter | 172 191 | Type Classes | Functor, Applicative, Monad patterns | 192 + | Recursion | Loops are statements; expressions compose | 173 193 174 194 --- 175 195 176 196 ## See Also 177 197 178 - - **[Tutorial](../tutorial/)** - Learn concepts in order by building an app 179 - - **[Examples](../../../examples/)** - Real-world code patterns 180 - - **[Source](../../../src/)** - The library is ~950 lines of readable TypeScript 198 + - **[Tutorial](/tutorial/)** - Learn concepts in order by building an app 199 + - **[Examples](/examples/)** - Real-world code patterns 200 + - **[Source](https://tangled.sh/oleksify.me/purus-ts/tree/main/src/)** - The library is ~950 lines of readable TypeScript
-61
docs/guides/stories/README.md
··· 1 - # Stories 2 - 3 - **Stories** are narrative-driven functional programming tutorials. Each story is a whimsical tale where animal characters discover and build the same concepts you'll use in production code. 4 - 5 - --- 6 - 7 - ## Why Stories? 8 - 9 - Traditional tutorials show you *what* to type. Stories show you *why* these patterns exist — through characters who face the same problems you do and discover the same solutions. 10 - 11 - When Rabbit worries "But what if someone votes twice?", you feel the problem. When Owl scratches out a `BallotError` type, you understand why typed errors matter. The code isn't an afterthought; it's woven into the narrative as the characters write it. 12 - 13 - --- 14 - 15 - ## How to Read 16 - 17 - 1. **Follow the narrative** — Let the story carry you through the concepts 18 - 2. **Read the code** — Characters write real, runnable TypeScript 19 - 3. **Run the example** — Each story has a matching file in `examples/stories/` 20 - 4. **Try variations** — Modify the example to solidify your understanding 21 - 22 - --- 23 - 24 - ## Available Stories 25 - 26 - ### [The Forest Election](./forest-election/) 27 - 28 - A trilogy about building a fair election system for the forest. 29 - 30 - | Part | Title | Concept | Status | 31 - |------|--------------------------------------------------------------------------|------------|-----------| 32 - | 1 | [The Ballot Box Problem](./forest-election/01-the-ballot-box-problem.md) | Validation | Available | 33 - | 2 | [Counting Day](./forest-election/02-counting-day.md) | Result | Available | 34 - | 3 | [The Announcement](./forest-election/03-the-announcement.md) | Effects | Available | 35 - 36 - **Tone:** Cozy and earnest (think Frog and Toad, Winnie the Pooh). The animals genuinely care about building fair software. 37 - 38 - **Characters:** Owl (methodical thinker), Beaver (eager builder), Fox (pragmatic skeptic), Rabbit (anxious but thorough), Badger (wise elder). 39 - 40 - --- 41 - 42 - ### [Beaver's Big System](./beavers-big-system/) 43 - 44 - A sequel trilogy about building maintainable maintenance software. 45 - 46 - | Part | Title | Concepts | Status | 47 - |------|----------------------------------------------------------------------------|--------------------------------|-----------| 48 - | 1 | [The Mixup](./beavers-big-system/01-the-mixup.md) | Branded Types + Typestate | Available | 49 - | 2 | [The Categorization](./beavers-big-system/02-the-categorization.md) | ADTs + Pattern Matching | Available | 50 - | 3 | [The Queue](./beavers-big-system/03-the-queue.md) | Tracked Arrays + Refinements | Available | 51 - 52 - **Setup:** After the election, Beaver builds "The System" — a work order tracker. But strings everywhere lead to chaos: mixed-up IDs, invalid states, unsorted queues. Owl guides the rebuild. 53 - 54 - **Same characters** continue their journey, learning compile-time safety through infrastructure problems. 55 - 56 - --- 57 - 58 - ## See Also 59 - 60 - - [Tutorial](../tutorial/) — Step-by-step guide building a complete application 61 - - [Concepts](../concepts/) — Deep-dive reference articles
+28 -2
docs/guides/stories/beavers-big-system/01-the-mixup.md docs-site/src/content/docs/stories/beavers-big-system/01-the-mixup.mdx
··· 1 - # The Mixup 1 + --- 2 + title: The Mixup 3 + description: "Part 1 — the animals learn that not all strings are created equal." 4 + sidebar: 5 + order: 1 6 + badge: 7 + text: Part 1 8 + variant: note 9 + prev: 10 + link: /stories/beavers-big-system/ 11 + label: "Beaver's Big System" 12 + next: 13 + link: /stories/beavers-big-system/02-the-categorization/ 14 + label: "Part 2: The Categorization" 15 + --- 16 + 17 + import StoryIllustration from '../../../../components/StoryIllustration.astro'; 18 + 19 + <div class="story-chapter"> 20 + 21 + <StoryIllustration 22 + alt="Beaver standing proudly next to The System, a tangled mess of strings and logs" 23 + caption="The System — version 1.0" 24 + /> 2 25 3 26 *Part 1 of Beaver's Big System* 4 27 ··· 409 432 410 433 --- 411 434 412 - *Next: [The Categorization](./02-the-categorization.md) — where the animals learn to distinguish different kinds of work* 435 + *Next: [The Categorization](/stories/beavers-big-system/02-the-categorization/) — where the animals learn to distinguish different kinds of work* 436 + 437 + 438 + </div>
+21 -2
docs/guides/stories/beavers-big-system/02-the-categorization.md docs-site/src/content/docs/stories/beavers-big-system/02-the-categorization.mdx
··· 1 - # The Categorization 1 + --- 2 + title: The Categorization 3 + description: "Part 2 — the animals learn that different work requires different data." 4 + sidebar: 5 + order: 2 6 + badge: 7 + text: Part 2 8 + variant: note 9 + prev: 10 + link: /stories/beavers-big-system/01-the-mixup/ 11 + label: "Part 1: The Mixup" 12 + next: 13 + link: /stories/beavers-big-system/03-the-queue/ 14 + label: "Part 3: The Queue" 15 + --- 16 + 17 + <div class="story-chapter"> 2 18 3 19 *Part 2 of Beaver's Big System* 4 20 ··· 492 508 493 509 --- 494 510 495 - *Next: [The Queue](./03-the-queue.md) — where the animals learn that arrays can track their own properties* 511 + *Next: [The Queue](/stories/beavers-big-system/03-the-queue/) — where the animals learn that arrays can track their own properties* 512 + 513 + 514 + </div>
+19 -3
docs/guides/stories/beavers-big-system/03-the-queue.md docs-site/src/content/docs/stories/beavers-big-system/03-the-queue.mdx
··· 1 - # The Queue 1 + --- 2 + title: The Queue 3 + description: "Part 3 — the animals learn that arrays can remember their properties." 4 + sidebar: 5 + order: 3 6 + badge: 7 + text: Part 3 8 + variant: note 9 + prev: 10 + link: /stories/beavers-big-system/02-the-categorization/ 11 + label: "Part 2: The Categorization" 12 + --- 13 + 14 + <div class="story-chapter"> 2 15 3 16 *Part 3 of Beaver's Big System* 4 17 ··· 457 470 458 471 | Part | Story | Concepts | 459 472 |------|-------|----------| 460 - | 1 | [The Mixup](./01-the-mixup.md) | Branded Types + Typestate | 461 - | 2 | [The Categorization](./02-the-categorization.md) | ADTs + Pattern Matching | 473 + | 1 | [The Mixup](/stories/beavers-big-system/01-the-mixup/) | Branded Types + Typestate | 474 + | 2 | [The Categorization](/stories/beavers-big-system/02-the-categorization/) | ADTs + Pattern Matching | 462 475 | 3 | The Queue (this story) | Tracked Arrays + Refinements | 463 476 464 477 Together, these patterns form a toolkit for *making invalid states unrepresentable*. The type system becomes your first line of defense against bugs — catching errors at compile time instead of runtime. ··· 466 479 --- 467 480 468 481 *Return to [Beaver's Big System overview](./) or explore [The Forest Election](../forest-election/) trilogy* 482 + 483 + 484 + </div>
+8 -5
docs/guides/stories/beavers-big-system/README.md docs-site/src/content/docs/stories/beavers-big-system/index.md
··· 1 - # Beaver's Big System 1 + --- 2 + title: "Beaver's Big System" 3 + description: A trilogy about building maintainable maintenance software. 4 + --- 2 5 3 6 A story in three parts about building maintainable maintenance software. 4 7 ··· 29 32 30 33 ## Parts 31 34 32 - ### Part 1: [The Mixup](./01-the-mixup.md) 35 + ### Part 1: [The Mixup](/stories/beavers-big-system/01-the-mixup/) 33 36 34 37 Beaver's system uses plain strings for IDs and status. Fox accidentally marks `bridge-east` work as done using `dam-north`'s ID. Rabbit finds a work order with status `"in_progerss"` (typo). Badger discovers work marked `"done"` that was never `"started"`. 35 38 ··· 43 46 44 47 --- 45 48 46 - ### Part 2: [The Categorization](./02-the-categorization.md) 49 + ### Part 2: [The Categorization](/stories/beavers-big-system/02-the-categorization/) 47 50 48 51 All work orders are treated identically, but different infrastructure needs different handling. Dam repairs need water level checks. Bridge work needs weight capacity assessments. Beaver's `type: string` field leads to `"Dam"`, `"dam"`, and `"water-structure"` chaos. 49 52 ··· 57 60 58 61 --- 59 62 60 - ### Part 3: [The Queue](./03-the-queue.md) 63 + ### Part 3: [The Queue](/stories/beavers-big-system/03-the-queue/) 61 64 62 65 Work orders need prioritization. Someone calls `getNext()` on an empty queue — runtime crash. The "sorted by priority" list isn't actually sorted after appending. Negative priority values sneak in. 63 66 ··· 92 95 93 96 - [Stories overview](../) — Other available stories 94 97 - [The Forest Election](../forest-election/) — The prequel trilogy 95 - - [Branded Types and Refinements](../../concepts/01-branded-types-and-refinements.md) — Technical deep-dive 98 + - [Branded Types and Refinements](/concepts/01-branded-types-and-refinements/) — Technical deep-dive
+113 -86
docs/guides/stories/forest-election/01-the-ballot-box-problem.md docs-site/src/content/docs/stories/forest-election/01-the-ballot-box-problem.mdx
··· 1 - # The Ballot Box Problem 1 + --- 2 + title: The Ballot Box Problem 3 + description: "Part 1 — the animals learn that finding ALL problems is better than stopping at the first." 4 + sidebar: 5 + order: 1 6 + badge: 7 + text: Part 1 8 + variant: note 9 + prev: 10 + link: /stories/forest-election/ 11 + label: The Forest Election 12 + next: 13 + link: /stories/forest-election/02-counting-day/ 14 + label: "Part 2: Counting Day" 15 + --- 2 16 3 - *Part 1 of The Forest Election* 17 + import StoryIllustration from "../../../../components/StoryIllustration.astro"; 18 + 19 + <div class="story-chapter"> 20 + 21 + <StoryIllustration 22 + alt="The animals gathered around the hollow log ballot box" 23 + caption="Election day in the forest" 24 + /> 25 + 26 + _Part 1 of The Forest Election_ 4 27 5 28 > In which the animals learn that finding ALL the problems 6 29 > is better than stopping at the first one. ··· 37 60 38 61 More leaves spilled out. Owl's pile of problems grew. 39 62 40 - "Here's one from Rabbit, voting for Deer. And here's *another* one from Rabbit, also voting for Deer." 63 + "Here's one from Rabbit, voting for Deer. And here's _another_ one from Rabbit, also voting for Deer." 41 64 42 65 "I was nervous!" Rabbit squeaked. "I couldn't remember if I'd already voted!" 43 66 ··· 45 68 46 69 Fox pushed off from his tree. "Simple solution. Throw out the bad ones, count what's left." 47 70 48 - "But then we don't know what went wrong!" Rabbit wrung her paws. "What if someone's vote was supposed to count but we rejected it? What if we could have *fixed* it?" 71 + "But then we don't know what went wrong!" Rabbit wrung her paws. "What if someone's vote was supposed to count but we rejected it? What if we could have _fixed_ it?" 49 72 50 - "Rabbit has a point," said Owl. "If we only stop at the first problem, we might miss others. Someone fixes their blank name, submits again, and *then* we tell them their candidate doesn't exist?" 73 + "Rabbit has a point," said Owl. "If we only stop at the first problem, we might miss others. Someone fixes their blank name, submits again, and _then_ we tell them their candidate doesn't exist?" 51 74 52 75 "So what do you suggest?" Fox asked. 53 76 ··· 55 78 56 79 --- 57 80 58 - "A ballot," said Owl, scratching careful marks, "has structure. It's not just any leaf — it's a leaf with *requirements*." 81 + "A ballot," said Owl, scratching careful marks, "has structure. It's not just any leaf — it's a leaf with _requirements_." 59 82 60 83 ```typescript 61 - type Candidate = "Deer" | "Squirrel" | "Heron" 84 + type Candidate = "Deer" | "Squirrel" | "Heron"; 62 85 63 86 type Ballot = { 64 - readonly voter: string 65 - readonly candidate: Candidate 66 - } 87 + readonly voter: string; 88 + readonly candidate: Candidate; 89 + }; 67 90 ``` 68 91 69 92 "A voter name," she continued. "And a candidate that must be one of our three." 70 93 71 94 Rabbit peered at the scratches. "But what happens when something's wrong?" 72 95 73 - Owl made a new leaf. "Then we need to describe what's wrong. And there can be *multiple* things wrong with a single ballot." 96 + Owl made a new leaf. "Then we need to describe what's wrong. And there can be _multiple_ things wrong with a single ballot." 74 97 75 98 ```typescript 76 99 type BallotError = 77 100 | { readonly _tag: "MissingVoter" } 78 101 | { readonly _tag: "MissingCandidate" } 79 102 | { readonly _tag: "InvalidCandidate"; readonly name: string } 80 - | { readonly _tag: "DuplicateVoter"; readonly voter: string } 103 + | { readonly _tag: "DuplicateVoter"; readonly voter: string }; 81 104 ``` 82 105 83 106 "Missing voter," Rabbit said, reading along. "Missing candidate. Invalid candidate — that's the Wolfe one. And... duplicate voter." She looked sheepish. ··· 95 118 (error) (error) 96 119 ``` 97 120 98 - "If the voter is missing, we stop. We never check the candidate. The submitter fixes the voter, resubmits, *then* discovers the candidate problem." 121 + "If the voter is missing, we stop. We never check the candidate. The submitter fixes the voter, resubmits, _then_ discovers the candidate problem." 99 122 100 123 "That's what Fox said," Beaver observed. 101 124 ··· 128 151 import { 129 152 type Validation, 130 153 valid, 154 + invalid, 131 155 invalidOne, 132 156 apValidation, 133 157 matchValidation, 158 + match, 134 159 pipe, 135 - } from "purus-ts" 160 + } from "purus-ts"; 136 161 ``` 137 162 138 - "A `Validation` is either valid, containing a value, or invalid, containing a *list* of errors. Not one error — a list." 163 + "A `Validation` is either valid, containing a value, or invalid, containing a _list_ of errors. Not one error — a list." 139 164 140 165 "Show us how it works," said Beaver. 141 166 ··· 145 170 146 171 ```typescript 147 172 const validateVoter = ( 148 - voter: string | undefined 173 + voter: string | undefined, 149 174 ): Validation<string, BallotError> => 150 175 voter && voter.trim().length > 0 151 176 ? valid(voter.trim()) 152 - : invalidOne({ _tag: "MissingVoter" }) 177 + : invalidOne({ _tag: "MissingVoter" }); 153 178 ``` 154 179 155 180 "If the voter exists and isn't blank, it's `valid`. Otherwise, `invalidOne` creates an invalid result with that single error." ··· 161 186 Beaver took a leaf: 162 187 163 188 ```typescript 164 - const VALID_CANDIDATES: readonly Candidate[] = ["Deer", "Squirrel", "Heron"] 189 + const VALID_CANDIDATES: readonly Candidate[] = ["Deer", "Squirrel", "Heron"]; 165 190 166 191 const validateCandidate = ( 167 - candidate: string | undefined 192 + candidate: string | undefined, 168 193 ): Validation<Candidate, BallotError> => 169 194 !candidate || candidate.trim().length === 0 170 195 ? invalidOne({ _tag: "MissingCandidate" }) 171 - : VALID_CANDIDATES.includes(candidate as Candidate) 172 - ? valid(candidate as Candidate) 173 - : invalidOne({ _tag: "InvalidCandidate", name: candidate }) 196 + : pipe( 197 + VALID_CANDIDATES.find((c) => c === candidate.trim()), 198 + (found) => 199 + found !== undefined 200 + ? valid(found) 201 + : invalidOne({ _tag: "InvalidCandidate", name: candidate }), 202 + ); 174 203 ``` 175 204 176 205 "Good," said Owl. "If blank, missing. If not a valid candidate name, invalid. Otherwise, valid." ··· 179 208 180 209 "But we have two validators," said Fox. "How do they become one?" 181 210 182 - "This is where it gets interesting." Owl cleared a fresh patch of dirt. "We need to *combine* them. And the combining is what accumulates errors." 211 + "This is where it gets interesting." Owl cleared a fresh patch of dirt. "We need to _combine_ them. And the combining is what accumulates errors." 183 212 184 213 She wrote: 185 214 ··· 189 218 (candidate: Candidate): Ballot => ({ 190 219 voter, 191 220 candidate, 192 - }) 221 + }); 193 222 194 223 const validateBallot = ( 195 224 voter: string | undefined, 196 - candidate: string | undefined 225 + candidate: string | undefined, 197 226 ): Validation<Ballot, BallotError> => 198 227 pipe( 199 228 valid(makeBallot), 200 229 apValidation(validateVoter(voter)), 201 - apValidation(validateCandidate(candidate)) 202 - ) 230 + apValidation(validateCandidate(candidate)), 231 + ); 203 232 ``` 204 233 205 - "Start with `valid(makeBallot)` — a validation containing a function that *builds* a ballot. Then `apValidation` applies each validator in turn." 234 + "Start with `valid(makeBallot)` — a validation containing a function that _builds_ a ballot. Then `apValidation` applies each validator in turn." 206 235 207 236 "The magic," she tapped the leaf, "is what happens when things go wrong." 208 237 ··· 232 261 233 262 "In case 3, both errors are collected! That's accumulation." 234 263 235 - Rabbit's eyes widened. "So we know *everything* that's wrong. All at once." 264 + Rabbit's eyes widened. "So we know _everything_ that's wrong. All at once." 236 265 237 266 "Exactly." 238 267 ··· 245 274 ```typescript 246 275 const validateNoDuplicate = ( 247 276 voter: string, 248 - alreadyVoted: ReadonlySet<string> 277 + alreadyVoted: ReadonlySet<string>, 249 278 ): Validation<string, BallotError> => 250 279 alreadyVoted.has(voter) 251 280 ? invalidOne({ _tag: "DuplicateVoter", voter }) 252 - : valid(voter) 281 + : valid(voter); 282 + ``` 283 + 284 + "And we can combine this with the others." She started writing, then paused and separated the voter checks: 285 + 286 + ```typescript 287 + const validateVoterFull = ( 288 + voter: string | undefined, 289 + alreadyVoted: ReadonlySet<string>, 290 + ): Validation<string, BallotError> => 291 + matchValidation( 292 + (v: string) => validateNoDuplicate(v, alreadyVoted), 293 + (errors: readonly BallotError[]) => invalid<BallotError>(errors), 294 + )(validateVoter(voter)); 253 295 ``` 254 296 255 - "And we can combine this with the others." She rewrote the full validator: 297 + "First check if the voter exists. If valid, check for duplicates. If already invalid, pass the errors through." 298 + 299 + Then the full validator became clean: 256 300 257 301 ```typescript 258 302 const validateBallotFull = ( 259 303 voter: string | undefined, 260 304 candidate: string | undefined, 261 - alreadyVoted: ReadonlySet<string> 305 + alreadyVoted: ReadonlySet<string>, 262 306 ): Validation<Ballot, BallotError> => 263 307 pipe( 264 308 valid(makeBallot), 265 - apValidation( 266 - pipe( 267 - validateVoter(voter), 268 - // Only check duplicate if voter exists 269 - (v) => 270 - v._tag === "Valid" 271 - ? pipe( 272 - validateNoDuplicate(v.value, alreadyVoted), 273 - // Keep the voter value if no duplicate 274 - (dup) => (dup._tag === "Valid" ? v : dup) 275 - ) 276 - : v 277 - ) 278 - ), 279 - apValidation(validateCandidate(candidate)) 280 - ) 309 + apValidation(validateVoterFull(voter, alreadyVoted)), 310 + apValidation(validateCandidate(candidate)), 311 + ); 281 312 ``` 282 313 283 - "Hmm," said Beaver. "That duplicate check is a bit tangled." 284 - 285 - "It is," Owl admitted. "There's an alternative — run all validators independently and combine errors manually. For now, this works." 314 + "Each line is one concern," said Owl. "Voter checks run in sequence — existence, then duplicates. But voter and candidate run in parallel, accumulating errors from both." 286 315 287 316 --- 288 317 ··· 291 320 Owl gathered her leaves: 292 321 293 322 ```typescript 294 - const formatError = (error: BallotError): string => { 295 - switch (error._tag) { 296 - case "MissingVoter": 297 - return "Ballot is missing a voter name" 298 - case "MissingCandidate": 299 - return "Ballot is missing a candidate" 300 - case "InvalidCandidate": 301 - return `"${error.name}" is not a valid candidate` 302 - case "DuplicateVoter": 303 - return `${error.voter} has already voted` 304 - } 305 - } 323 + const formatError = (error: BallotError): string => 324 + match(error)({ 325 + MissingVoter: () => "Ballot is missing a voter name", 326 + MissingCandidate: () => "Ballot is missing a candidate", 327 + InvalidCandidate: ({ name }) => `"${name}" is not a valid candidate`, 328 + DuplicateVoter: ({ voter }) => `${voter} has already voted`, 329 + }); 306 330 307 331 // Test ballots 308 332 const ballots = [ 309 - { voter: "Rabbit", candidate: "Deer" }, // Valid 333 + { voter: "Rabbit", candidate: "Deer" }, // Valid 310 334 { voter: undefined, candidate: "Squirrel" }, // Missing voter 311 - { voter: "Fox", candidate: undefined }, // Missing candidate 312 - { voter: "Beaver", candidate: "Wolfe" }, // Invalid candidate 313 - { voter: undefined, candidate: undefined }, // Both missing! 314 - { voter: "Rabbit", candidate: "Heron" }, // Duplicate voter 315 - ] 335 + { voter: "Fox", candidate: undefined }, // Missing candidate 336 + { voter: "Beaver", candidate: "Wolfe" }, // Invalid candidate 337 + { voter: undefined, candidate: undefined }, // Both missing! 338 + { voter: "Rabbit", candidate: "Heron" }, // Duplicate voter 339 + ]; 316 340 317 - const alreadyVoted = new Set<string>() 341 + ballots.reduce<ReadonlySet<string>>((voted, { voter, candidate }, i) => { 342 + const result = validateBallotFull(voter, candidate, voted); 318 343 319 - ballots.forEach(({ voter, candidate }, i) => { 320 - const result = validateBallotFull(voter, candidate, alreadyVoted) 321 - 322 - matchValidation( 323 - (ballot) => { 324 - console.log(`Ballot ${i + 1}: Valid - ${ballot.voter} votes for ${ballot.candidate}`) 325 - alreadyVoted.add(ballot.voter) 344 + return matchValidation( 345 + (ballot: Ballot) => { 346 + console.log( 347 + `Ballot ${i + 1}: Valid - ${ballot.voter} votes for ${ballot.candidate}`, 348 + ); 349 + return new Set([...voted, ballot.voter]); 350 + }, 351 + (errors: readonly BallotError[]) => { 352 + console.log(`Ballot ${i + 1}: Invalid`); 353 + console.log(errors.map((e) => ` - ${formatError(e)}`).join("\n")); 354 + return voted; 326 355 }, 327 - (errors) => { 328 - console.log(`Ballot ${i + 1}: Invalid`) 329 - errors.forEach((e) => console.log(` - ${formatError(e)}`)) 330 - } 331 - )(result) 332 - }) 356 + )(result); 357 + }, new Set<string>()); 333 358 ``` 334 359 335 360 Owl ran through the leaves. The results appeared in the dirt: ··· 363 388 364 389 Badger had been quiet, watching. Now he spoke. "We have valid ballots and invalid ones. The invalid ones are clearly marked. We can set them aside, ask those voters to try again with corrections." 365 390 366 - "And the valid ones are *truly* valid," added Rabbit. "We know they have a real voter, a real candidate, and no duplicates." 391 + "And the valid ones are _truly_ valid," added Rabbit. "We know they have a real voter, a real candidate, and no duplicates." 367 392 368 393 The sun was lower now. The pile of leaves had been sorted — valid on one side, invalid on the other with their errors carefully noted. 369 394 ··· 401 426 402 427 --- 403 428 404 - *Next: [Counting Day](./02-counting-day.md) (coming soon)* 429 + _Next: [Counting Day](/stories/forest-election/02-counting-day/) (coming soon)_ 430 + 431 + </div>
+30 -12
docs/guides/stories/forest-election/02-counting-day.md docs-site/src/content/docs/stories/forest-election/02-counting-day.mdx
··· 1 - # Counting Day 1 + --- 2 + title: Counting Day 3 + description: "Part 2 — the animals learn that some operations must stop at the first sign of trouble." 4 + sidebar: 5 + order: 2 6 + badge: 7 + text: Part 2 8 + variant: note 9 + prev: 10 + link: /stories/forest-election/01-the-ballot-box-problem/ 11 + label: "Part 1: The Ballot Box Problem" 12 + next: 13 + link: /stories/forest-election/03-the-announcement/ 14 + label: "Part 3: The Announcement" 15 + --- 16 + 17 + <div class="story-chapter"> 2 18 3 19 *Part 2 of The Forest Election* 4 20 ··· 53 69 chainResult, 54 70 mapResult, 55 71 matchResult, 72 + match, 56 73 pipe, 57 74 } from "purus-ts" 58 75 ``` ··· 253 270 She wrote out test cases: 254 271 255 272 ```typescript 256 - const formatError = (error: CountError): string => { 257 - switch (error._tag) { 258 - case "EmptyBallotBox": 259 - return "No ballots in the box" 260 - case "TotalMismatch": 261 - return `Count mismatch: expected ${error.expected}, got ${error.actual}` 262 - case "TieDetected": 263 - return `Tie between ${error.candidates.join(" and ")} with ${error.votes} votes each` 264 - } 265 - } 273 + const formatError = (error: CountError): string => 274 + match(error)({ 275 + EmptyBallotBox: () => "No ballots in the box", 276 + TotalMismatch: ({ expected, actual }) => 277 + `Count mismatch: expected ${expected}, got ${actual}`, 278 + TieDetected: ({ candidates, votes }) => 279 + `Tie between ${candidates.join(" and ")} with ${votes} votes each`, 280 + }) 266 281 267 282 // Test 1: Normal election 268 283 const normalBallots: readonly Ballot[] = [ ··· 373 388 374 389 --- 375 390 376 - *Next: [The Announcement](./03-the-announcement.md)* 391 + *Next: [The Announcement](/stories/forest-election/03-the-announcement/)* 392 + 393 + 394 + </div>
+25 -9
docs/guides/stories/forest-election/03-the-announcement.md docs-site/src/content/docs/stories/forest-election/03-the-announcement.mdx
··· 1 - # The Announcement 1 + --- 2 + title: The Announcement 3 + description: "Part 3 — the animals learn that some things take time, can fail, and need to be tried again." 4 + sidebar: 5 + order: 3 6 + badge: 7 + text: Part 3 8 + variant: note 9 + prev: 10 + link: /stories/forest-election/02-counting-day/ 11 + label: "Part 2: Counting Day" 12 + --- 13 + 14 + <div class="story-chapter"> 2 15 3 16 *Part 3 of The Forest Election* 4 17 ··· 294 307 ) 295 308 296 309 console.log(`Delivered: ${delivered.length}/4`) 297 - delivered.forEach((d) => 298 - console.log(` ✓ ${d.result.region} via ${d.result.deliveredBy}`) 310 + console.log( 311 + delivered.map((d) => ` ✓ ${d.result.region} via ${d.result.deliveredBy}`).join("\n") 299 312 ) 300 313 301 314 console.log(`Failed: ${failed.length}/4`) 302 - failed.forEach((f) => 303 - console.log(` ✗ ${f.region}: ${formatError(f.error)}`) 315 + console.log( 316 + failed.map((f) => ` ✗ ${f.region}: ${formatError(f.error)}`).join("\n") 304 317 ) 305 318 } 306 319 ``` ··· 387 400 388 401 To dive deeper into any of these concepts, see: 389 402 390 - - [Validation and Error Accumulation](../../concepts/03-validation-and-error-accumulation.md) 391 - - [Why Errors as Values?](../../concepts/01-errors-as-values.md) 392 - - [Effect Composition](../../concepts/06-effect-composition.md) 393 - - [Fiber Internals](../../concepts/07-fiber-internals.md) 403 + - [Validation and Error Accumulation](/concepts/03-validation-and-error-accumulation/) 404 + - [Why Errors as Values?](/concepts/01-errors-as-values/) 405 + - [Effect Composition](/concepts/06-effect-composition/) 406 + - [Fiber Internals](/concepts/07-fiber-internals/) 407 + 408 + 409 + </div>
+8 -5
docs/guides/stories/forest-election/README.md docs-site/src/content/docs/stories/forest-election/index.md
··· 1 - # The Forest Election 1 + --- 2 + title: The Forest Election 3 + description: A trilogy about building a fair election system for the forest. 4 + --- 2 5 3 6 A story in three parts about building a fair election system. 4 7 ··· 26 29 27 30 ## Parts 28 31 29 - ### Part 1: [The Ballot Box Problem](./01-the-ballot-box-problem.md) 32 + ### Part 1: [The Ballot Box Problem](/stories/forest-election/01-the-ballot-box-problem/) 30 33 31 34 The animals gather to elect a president, but the old way — leaves in a box — leads to chaos. Blank ballots, invalid candidates, duplicate votes. Fox wants to throw out bad ballots; Rabbit insists they need to know *all* the problems. Owl introduces the Validation type. 32 35 ··· 40 43 41 44 --- 42 45 43 - ### Part 2: [Counting Day](./02-counting-day.md) 46 + ### Part 2: [Counting Day](/stories/forest-election/02-counting-day/) 44 47 45 48 The ballots are validated, but counting brings new challenges. What if the count doesn't match? What if there's a tie? The animals discover that some operations must short-circuit — and `Result` is the right tool. 46 49 ··· 54 57 55 58 --- 56 59 57 - ### Part 3: [The Announcement](./03-the-announcement.md) 60 + ### Part 3: [The Announcement](/stories/forest-election/03-the-announcement/) 58 61 59 62 The winner must be announced to every corner of the forest. But the messenger birds are unreliable — some might not return. The animals need effects that can be retried, raced, and cancelled. 60 63 ··· 89 92 ## See Also 90 93 91 94 - [Stories overview](../) — Other available stories 92 - - [Validation and Error Accumulation](../../concepts/03-validation-and-error-accumulation.md) — Technical deep-dive 95 + - [Validation and Error Accumulation](/concepts/03-validation-and-error-accumulation/) — Technical deep-dive
+7 -2
docs/guides/tutorial/01-why-functional-typescript.md docs-site/src/content/docs/tutorial/01-why-functional-typescript.md
··· 1 - # Chapter 1: Why Functional TypeScript? 1 + --- 2 + title: Why Functional TypeScript? 3 + description: "Learn why exceptions break TypeScript's type safety and how purus-ts fixes it." 4 + sidebar: 5 + order: 1 6 + --- 2 7 3 8 TypeScript gives you a powerful type system. You define interfaces, catch type errors at compile time, and get autocomplete in your editor. It feels like you're in control. 4 9 ··· 244 249 245 250 In the next chapter, you'll learn about the Effect type - purus's core abstraction for async operations. Effects take the ideas from this chapter and make them work with async code, cancellation, and dependency injection. 246 251 247 - [Continue to Chapter 2: Your First Effect →](./02-your-first-effect.md) 252 + [Continue to Chapter 2: Your First Effect →](/tutorial/02-your-first-effect/)
+7 -2
docs/guides/tutorial/02-your-first-effect.md docs-site/src/content/docs/tutorial/02-your-first-effect.md
··· 1 - # Chapter 2: Your First Effect 1 + --- 2 + title: Your First Effect 3 + description: Creating, running, and transforming effects with the Eff type. 4 + sidebar: 5 + order: 2 6 + --- 2 7 3 8 In the previous chapter, we saw how `Result<T, E>` makes errors visible in the type system. But Result is synchronous - it holds a value that already exists. 4 9 ··· 423 428 424 429 In the next chapter, we'll dive deeper into `Result<T, E>` for synchronous error handling - the building block that makes typed errors possible. 425 430 426 - [Continue to Chapter 3: Typed Errors with Result →](./03-typed-errors-with-result.md) 431 + [Continue to Chapter 3: Typed Errors with Result →](/tutorial/03-typed-errors-with-result/)
+7 -2
docs/guides/tutorial/03-typed-errors-with-result.md docs-site/src/content/docs/tutorial/03-typed-errors-with-result.md
··· 1 - # Chapter 3: Typed Errors with Result 1 + --- 2 + title: Typed Errors with Result 3 + description: The Result type for synchronous error handling with Ok and Err. 4 + sidebar: 5 + order: 3 6 + --- 2 7 3 8 In Chapter 1, we saw why exceptions break TypeScript's type safety. In Chapter 2, we used Effects for async operations. But what about synchronous code that can fail? 4 9 ··· 266 271 267 272 Result handles synchronous errors. But what about values that might not exist at all? In the next chapter, we'll explore `Option<T>` for nullable value handling. 268 273 269 - [Continue to Chapter 4: Optional Values with Option →](./04-optional-values-with-option.md) 274 + [Continue to Chapter 4: Optional Values with Option →](/tutorial/04-optional-values-with-option/)
+7 -2
docs/guides/tutorial/04-optional-values-with-option.md docs-site/src/content/docs/tutorial/04-optional-values-with-option.md
··· 1 - # Chapter 4: Optional Values with Option 1 + --- 2 + title: Optional Values with Option 3 + description: The Option type for handling nullable values with Some and None. 4 + sidebar: 5 + order: 4 6 + --- 2 7 3 8 In the previous chapter, we used `Result<T, E>` for operations that can fail with an error. But sometimes there's no error - a value simply might not exist. 4 9 ··· 260 265 261 266 We've been using the `_tag` field to distinguish variants. In the next chapter, we'll explore pattern matching - the elegant way to handle discriminated unions exhaustively. 262 267 263 - [Continue to Chapter 5: Pattern Matching →](./05-pattern-matching.md) 268 + [Continue to Chapter 5: Pattern Matching →](/tutorial/05-pattern-matching/)
+7 -2
docs/guides/tutorial/05-pattern-matching.md docs-site/src/content/docs/tutorial/05-pattern-matching.md
··· 1 - # Chapter 5: Pattern Matching 1 + --- 2 + title: Pattern Matching 3 + description: Exhaustive type-safe matching with match(), when(), and guards. 4 + sidebar: 5 + order: 5 6 + --- 2 7 3 8 Throughout this tutorial, we've used the `_tag` field to distinguish between variants. This chapter shows you the elegant way to handle these discriminated unions with exhaustive pattern matching. 4 9 ··· 250 255 251 256 We've been creating distinct types with `_tag`. But what about primitives? How do you prevent swapping `UserId` and `OrderId` when both are strings? The next chapter introduces branded types. 252 257 253 - [Continue to Chapter 6: Branded Types →](./06-branded-types.md) 258 + [Continue to Chapter 6: Branded Types →](/tutorial/06-branded-types/)
+7 -2
docs/guides/tutorial/06-branded-types.md docs-site/src/content/docs/tutorial/06-branded-types.md
··· 1 - # Chapter 6: Branded Types 1 + --- 2 + title: Branded Types 3 + description: Creating distinct types from primitives to prevent mix-ups at compile time. 4 + sidebar: 5 + order: 6 6 + --- 2 7 3 8 We've used discriminated unions to distinguish between variants of a type. But what about primitives? A `UserId` and an `OrderId` are both strings - how do you prevent mixing them up? 4 9 ··· 280 285 281 286 We've covered the foundational types: Result, Option, and Branded. Now it's time to go deeper into the Effect system - how to compose async operations with typed errors and dependencies. 282 287 283 - [Continue to Chapter 7: The Effect System →](./07-the-effect-system.md) 288 + [Continue to Chapter 7: The Effect System →](/tutorial/07-the-effect-system/)
+7 -2
docs/guides/tutorial/07-the-effect-system.md docs-site/src/content/docs/tutorial/07-the-effect-system.md
··· 1 - # Chapter 7: The Effect System 1 + --- 2 + title: The Effect System 3 + description: Deep dive into Eff, lazy evaluation, and effect composition with pipe(). 4 + sidebar: 5 + order: 7 6 + --- 2 7 3 8 In Chapter 2, we introduced Effects as "descriptions of work." This chapter goes deeper into the `Eff<A, E, R>` type and how to compose complex async operations. 4 9 ··· 331 336 332 337 So far, our effects run one at a time. What about running multiple effects in parallel? Racing them? Cancelling long-running work? The next chapter covers concurrency with Fibers. 333 338 334 - [Continue to Chapter 8: Concurrency with Fibers →](./08-concurrency-with-fibers.md) 339 + [Continue to Chapter 8: Concurrency with Fibers →](/tutorial/08-concurrency-with-fibers/)
+7 -2
docs/guides/tutorial/08-concurrency-with-fibers.md docs-site/src/content/docs/tutorial/08-concurrency-with-fibers.md
··· 1 - # Chapter 8: Concurrency with Fibers 1 + --- 2 + title: Concurrency with Fibers 3 + description: Fork, join, race, and cancellation with lightweight fibers. 4 + sidebar: 5 + order: 8 6 + --- 2 7 3 8 Promises give you basic concurrency with `Promise.all` and `Promise.race`. But they have limitations: `Promise.race` doesn't cancel the loser, `Promise.all` doesn't handle partial failures well, and there's no way to interrupt running work. 4 9 ··· 316 321 317 322 We've seen effects that require certain dependencies (the `R` type parameter). The next chapter shows how to use dependency injection to provide these requirements - without any DI framework. 318 323 319 - [Continue to Chapter 9: Dependency Injection →](./09-dependency-injection.md) 324 + [Continue to Chapter 9: Dependency Injection →](/tutorial/09-dependency-injection/)
+7 -2
docs/guides/tutorial/09-dependency-injection.md docs-site/src/content/docs/tutorial/09-dependency-injection.md
··· 1 - # Chapter 9: Dependency Injection 1 + --- 2 + title: Dependency Injection 3 + description: Environment types, provide(), and access() for testable code. 4 + sidebar: 5 + order: 9 6 + --- 2 7 3 8 Testing is hard when your code has hardcoded dependencies. You end up mocking modules, setting NODE_ENV, or building elaborate test fixtures. 4 9 ··· 347 352 348 353 You've learned all the core concepts! The final chapter puts everything together in a complete application. 349 354 350 - [Continue to Chapter 10: Building a Complete App →](./10-building-a-complete-app.md) 355 + [Continue to Chapter 10: Building a Complete App →](/tutorial/10-building-a-complete-app/)
+6 -1
docs/guides/tutorial/10-building-a-complete-app.md docs-site/src/content/docs/tutorial/10-building-a-complete-app.md
··· 1 - # Chapter 10: Building a Complete App 1 + --- 2 + title: Building a Complete App 3 + description: Putting it all together with a CLI task manager. 4 + sidebar: 5 + order: 10 6 + --- 2 7 3 8 Time to put everything together. We'll build a CLI task manager that uses every concept from this tutorial: 4 9
+16 -19
docs/guides/tutorial/README.md docs-site/src/content/docs/tutorial/index.md
··· 1 - # purus-ts Tutorial 1 + --- 2 + title: Tutorial 3 + description: Learn purus-ts by building a complete application step-by-step. 4 + --- 2 5 3 6 Learn purus-ts by building a complete application step-by-step. 4 7 ··· 30 33 - The "errors as values" philosophy 31 34 - How purus makes TypeScript's type system work for errors too 32 35 33 - [Read Chapter 1 →](./01-why-functional-typescript.md) 36 + [Read Chapter 1 →](/tutorial/01-why-functional-typescript/) 34 37 35 38 --- 36 39 ··· 44 47 - Running effects with `runPromise`, `runPromiseExit` 45 48 - Transforming effects with `mapEff`, `flatMap` 46 49 47 - [Read Chapter 2 →](./02-your-first-effect.md) 50 + [Read Chapter 2 →](/tutorial/02-your-first-effect/) 48 51 49 52 --- 50 53 ··· 58 61 - Transforming with `mapResult`, `chainResult` 59 62 - Unwrapping with `unwrapOr`, `matchResult` 60 63 61 - [Read Chapter 3 →](./03-typed-errors-with-result.md) 64 + [Read Chapter 3 →](/tutorial/03-typed-errors-with-result/) 62 65 63 66 --- 64 67 ··· 72 75 - Transforming with `mapOption`, `flatMapOption` 73 76 - Unwrapping with `getOrElse`, `matchOption` 74 77 75 - [Read Chapter 4 →](./04-optional-values-with-option.md) 78 + [Read Chapter 4 →](/tutorial/04-optional-values-with-option/) 76 79 77 80 --- 78 81 ··· 86 89 - Guard-based matching with `when()` 87 90 - Why `_tag` is the standard discriminant 88 91 89 - [Read Chapter 5 →](./05-pattern-matching.md) 92 + [Read Chapter 5 →](/tutorial/05-pattern-matching/) 90 93 91 94 --- 92 95 ··· 100 103 - Smart constructors for validation 101 104 - Real-world examples: IDs, emails, validated numbers 102 105 103 - [Read Chapter 6 →](./06-branded-types.md) 106 + [Read Chapter 6 →](/tutorial/06-branded-types/) 104 107 105 108 --- 106 109 ··· 114 117 - Building complex effects from simple ones 115 118 - The `pipe()` function for composition 116 119 117 - [Read Chapter 7 →](./07-the-effect-system.md) 120 + [Read Chapter 7 →](/tutorial/07-the-effect-system/) 118 121 119 122 --- 120 123 ··· 129 132 - Parallel execution with `all()` 130 133 - Cleanup functions and interruption 131 134 132 - [Read Chapter 8 →](./08-concurrency-with-fibers.md) 135 + [Read Chapter 8 →](/tutorial/08-concurrency-with-fibers/) 133 136 134 137 --- 135 138 ··· 143 146 - Providing dependencies with `provide()` 144 147 - Layered environments for testing 145 148 146 - [Read Chapter 9 →](./09-dependency-injection.md) 149 + [Read Chapter 9 →](/tutorial/09-dependency-injection/) 147 150 148 151 --- 149 152 ··· 157 160 - Dependency injection for testability 158 161 - Concurrent operations with proper cancellation 159 162 160 - [Read Chapter 10 →](./10-building-a-complete-app.md) 163 + [Read Chapter 10 →](/tutorial/10-building-a-complete-app/) 161 164 162 165 --- 163 166 ··· 165 168 166 169 After completing the tutorial: 167 170 168 - 1. **Explore the Examples** - See `examples/` for real-world patterns 169 - 2. **Read the Concepts** - Deep-dive into specific topics in `../concepts/` 171 + 1. **Explore the [Examples](/examples/)** - See real-world patterns 172 + 2. **Read the [Concepts](/concepts/)** - Deep-dive into specific topics 170 173 3. **Build Something** - The best way to learn is to use it 171 - 172 - ## Getting Help 173 - 174 - - Check the examples in `examples/` directory 175 - - Read the source - purus is ~950 lines of well-commented TypeScript 176 - - Open an issue on GitHub for questions
+1 -3
examples/stories/beavers-big-system/01-branded-typestate.ts
··· 229 229 workOrders.push(dam2) 230 230 231 231 console.log("All work orders:\n") 232 - workOrders.forEach((work) => { 233 - console.log(` ${describeWorkOrder(work)}`) 234 - }) 232 + console.log(workOrders.map((work) => ` ${describeWorkOrder(work)}`).join("\n")) 235 233 236 234 // Summary by state using pattern matching 237 235 console.log("\n" + "-".repeat(40))
+34 -24
examples/stories/beavers-big-system/02-adt-matching.ts
··· 215 215 console.log("Work Orders:\n") 216 216 console.log("-".repeat(70)) 217 217 218 - workOrders.forEach((order) => { 219 - const { details } = order 220 - console.log(`ID: ${order.id}`) 221 - console.log(` Type: ${details._tag}`) 222 - console.log(` Description: ${describeWork(details)}`) 223 - console.log(` Estimated hours: ${estimateHours(details)}`) 224 - console.log(` Priority: ${getPriority(details)}`) 225 - console.log(` Requires equipment: ${requiresEquipment(details) ? "Yes" : "No"}`) 226 - console.log(` Water info: ${getWaterInfo(details)}`) 227 - console.log(` Rabbit relevant: ${isRabbitRelevant(details) ? "Yes" : "No"}`) 228 - console.log("-".repeat(70)) 229 - }) 218 + console.log( 219 + workOrders 220 + .map((order) => { 221 + const { details } = order 222 + return [ 223 + `ID: ${order.id}`, 224 + ` Type: ${details._tag}`, 225 + ` Description: ${describeWork(details)}`, 226 + ` Estimated hours: ${estimateHours(details)}`, 227 + ` Priority: ${getPriority(details)}`, 228 + ` Requires equipment: ${requiresEquipment(details) ? "Yes" : "No"}`, 229 + ` Water info: ${getWaterInfo(details)}`, 230 + ` Rabbit relevant: ${isRabbitRelevant(details) ? "Yes" : "No"}`, 231 + "-".repeat(70), 232 + ].join("\n") 233 + }) 234 + .join("\n"), 235 + ) 230 236 231 237 // ============================================================================= 232 238 // Type Safety Demonstrations ··· 283 289 284 290 const grouped = groupByType(workOrders) 285 291 286 - Object.entries(grouped).forEach(([type, orders]) => { 287 - console.log(`${type}: ${orders.length} order(s)`) 288 - orders.forEach((o) => console.log(` - ${o.id}: ${describeWork(o.details)}`)) 289 - }) 292 + console.log( 293 + Object.entries(grouped) 294 + .map(([type, orders]) => 295 + `${type}: ${orders.length} order(s)\n${orders.map((o) => ` - ${o.id}: ${describeWork(o.details)}`).join("\n")}`, 296 + ) 297 + .join("\n"), 298 + ) 290 299 291 300 // ============================================================================= 292 301 // Total Hours by Priority ··· 304 313 {} as Record<string, number>, 305 314 ) 306 315 307 - Object.entries(hoursByPriority) 308 - .sort(([a], [b]) => { 309 - const order = ["critical", "high", "normal", "low"] 310 - return order.indexOf(a) - order.indexOf(b) 311 - }) 312 - .forEach(([priority, hours]) => { 313 - console.log(` ${priority}: ${hours} hours`) 314 - }) 316 + console.log( 317 + Object.entries(hoursByPriority) 318 + .sort(([a], [b]) => { 319 + const order = ["critical", "high", "normal", "low"] 320 + return order.indexOf(a) - order.indexOf(b) 321 + }) 322 + .map(([priority, hours]) => ` ${priority}: ${hours} hours`) 323 + .join("\n"), 324 + ) 315 325 316 326 console.log("\n=== End of ADTs + Pattern Matching Demo ===")
+27 -17
examples/stories/beavers-big-system/03-tracked-arrays.ts
··· 148 148 149 149 const testPriorities = [1, 5, 3, 0, -1, 2.5, 100] 150 150 151 - testPriorities.forEach((n) => { 152 - const result = priority(n) 153 - match(result)({ 154 - Some: ({ value }) => console.log(` priority(${n}) = Valid: ${value}`), 155 - None: () => console.log(` priority(${n}) = Invalid (must be non-negative integer)`), 156 - }) 157 - }) 151 + console.log( 152 + testPriorities 153 + .map((n) => { 154 + const result = priority(n) 155 + return match(result)({ 156 + Some: ({ value }) => ` priority(${n}) = Valid: ${value}`, 157 + None: () => ` priority(${n}) = Invalid (must be non-negative integer)`, 158 + }) 159 + }) 160 + .join("\n"), 161 + ) 158 162 159 163 // ============================================================================= 160 164 // Part 2: Building Work Orders with Valid Priorities ··· 183 187 ] 184 188 185 189 console.log("Attempting to create work orders:") 186 - maybeOrders.forEach((result, i) => { 187 - match(result)({ 188 - Some: ({ value }) => 189 - console.log(` Order ${i + 1}: Created "${value.description}" (priority ${value.priority})`), 190 - None: () => console.log(` Order ${i + 1}: Rejected (invalid priority)`), 191 - }) 192 - }) 190 + console.log( 191 + maybeOrders 192 + .map((result, i) => 193 + match(result)({ 194 + Some: ({ value }) => 195 + ` Order ${i + 1}: Created "${value.description}" (priority ${value.priority})`, 196 + None: () => ` Order ${i + 1}: Rejected (invalid priority)`, 197 + }), 198 + ) 199 + .join("\n"), 200 + ) 193 201 194 202 // Collect only valid orders 195 203 const validOrders: WorkOrder[] = maybeOrders.flatMap((opt) => ··· 217 225 const fullQueue = addToQueue(queue3, validOrders[3]!) 218 226 219 227 console.log("Queue after adding all valid work orders (sorted by priority):") 220 - fullQueue.forEach((work, i) => { 221 - console.log(` ${i + 1}. [Priority ${work.priority}] ${work.description}`) 222 - }) 228 + console.log( 229 + [...fullQueue] 230 + .map((work, i) => ` ${i + 1}. [Priority ${work.priority}] ${work.description}`) 231 + .join("\n"), 232 + ) 223 233 224 234 // ============================================================================= 225 235 // Part 4: Safe Queue Access
+81 -55
examples/stories/forest-election/01-validation.ts
··· 15 15 import { 16 16 type Validation, 17 17 valid, 18 + invalid, 18 19 invalidOne, 19 20 apValidation, 20 21 matchValidation, 22 + match, 23 + isDefined, 21 24 pipe, 22 25 } from "../../../src/index" 23 26 ··· 62 65 ): Validation<Candidate, BallotError> => 63 66 !candidate || candidate.trim().length === 0 64 67 ? invalidOne({ _tag: "MissingCandidate" }) 65 - : VALID_CANDIDATES.includes(candidate as Candidate) 66 - ? valid(candidate as Candidate) 67 - : invalidOne({ _tag: "InvalidCandidate", name: candidate }) 68 + : pipe( 69 + VALID_CANDIDATES.find((c) => c === candidate.trim()), 70 + (found) => 71 + isDefined(found) 72 + ? valid(found) 73 + : invalidOne({ _tag: "InvalidCandidate", name: candidate }), 74 + ) 68 75 69 76 /** 70 77 * Validate that a voter hasn't already voted. ··· 82 89 // ============================================================================= 83 90 84 91 /** 92 + * Validate voter exists AND hasn't already voted. 93 + * Sequential: duplicate check only runs if voter is present. 94 + */ 95 + const validateVoterFull = ( 96 + voter: string | undefined, 97 + alreadyVoted: ReadonlySet<string>, 98 + ): Validation<string, BallotError> => 99 + matchValidation( 100 + (v: string) => validateNoDuplicate(v, alreadyVoted), 101 + (errors: readonly BallotError[]) => invalid<BallotError>(errors), 102 + )(validateVoter(voter)) 103 + 104 + /** 85 105 * Curried ballot constructor for use with apValidation. 86 106 */ 87 107 const makeBallot = ··· 93 113 94 114 /** 95 115 * Validate a complete ballot, checking: 96 - * - Voter is present 97 - * - Voter hasn't voted before 116 + * - Voter is present and not a duplicate 98 117 * - Candidate is present and valid 99 118 * 100 119 * All errors are accumulated — you get the complete list of problems. ··· 106 125 ): Validation<Ballot, BallotError> => 107 126 pipe( 108 127 valid(makeBallot), 109 - apValidation( 110 - pipe(validateVoter(voter), (voterResult) => 111 - voterResult._tag === "Valid" 112 - ? pipe( 113 - validateNoDuplicate(voterResult.value, alreadyVoted), 114 - (dupResult) => (dupResult._tag === "Valid" ? voterResult : dupResult), 115 - ) 116 - : voterResult, 117 - ), 118 - ), 128 + apValidation(validateVoterFull(voter, alreadyVoted)), 119 129 apValidation(validateCandidate(candidate)), 120 130 ) 121 131 ··· 123 133 // Error Formatting 124 134 // ============================================================================= 125 135 126 - const formatError = (error: BallotError): string => { 127 - switch (error._tag) { 128 - case "MissingVoter": 129 - return "Ballot is missing a voter name" 130 - case "MissingCandidate": 131 - return "Ballot is missing a candidate" 132 - case "InvalidCandidate": 133 - return `"${error.name}" is not a valid candidate` 134 - case "DuplicateVoter": 135 - return `${error.voter} has already voted` 136 - } 137 - } 136 + const formatError = (error: BallotError): string => 137 + match(error)({ 138 + MissingVoter: () => "Ballot is missing a voter name", 139 + MissingCandidate: () => "Ballot is missing a candidate", 140 + InvalidCandidate: ({ name }) => `"${name}" is not a valid candidate`, 141 + DuplicateVoter: ({ voter }) => `${voter} has already voted`, 142 + }) 138 143 139 144 // ============================================================================= 140 145 // Demo ··· 157 162 { voter: "Owl", candidate: "Eagle" }, // Invalid candidate 158 163 ] 159 164 160 - const alreadyVoted = new Set<string>() 161 - const validBallots: Ballot[] = [] 162 - const invalidBallots: Array<{ 163 - index: number 164 - errors: readonly BallotError[] 165 - }> = [] 165 + // Step 1: Validate all ballots (sequential — each needs the updated voted set) 166 + const validated = testBallots.reduce<{ 167 + voted: Set<string> 168 + results: Validation<Ballot, BallotError>[] 169 + }>( 170 + ({ voted, results }, { voter, candidate }) => { 171 + const result = validateBallot(voter, candidate, voted) 172 + return { 173 + voted: matchValidation( 174 + (b: Ballot) => new Set([...voted, b.voter]), 175 + () => voted, 176 + )(result), 177 + results: [...results, result], 178 + } 179 + }, 180 + { voted: new Set(), results: [] }, 181 + ).results 166 182 183 + // Step 2: Display each result 167 184 console.log("Processing ballots...\n") 185 + console.log( 186 + validated 187 + .map((result, i) => 188 + matchValidation( 189 + (b: Ballot) => 190 + `Ballot ${i + 1}: Valid - ${b.voter} votes for ${b.candidate}`, 191 + (errors: readonly BallotError[]) => 192 + `Ballot ${i + 1}: Invalid\n${errors.map((e) => ` - ${formatError(e)}`).join("\n")}`, 193 + )(result), 194 + ) 195 + .join("\n"), 196 + ) 168 197 169 - testBallots.forEach(({ voter, candidate }, i) => { 170 - const result = validateBallot(voter, candidate, alreadyVoted) 198 + // Step 3: Partition for summary 199 + const validBallots = validated.flatMap((r) => 200 + matchValidation( 201 + (b: Ballot) => [b], 202 + () => [] as Ballot[], 203 + )(r), 204 + ) 171 205 206 + const invalidBallots = validated.flatMap((r, i) => 172 207 matchValidation( 173 - (ballot: Ballot) => { 174 - console.log( 175 - `Ballot ${i + 1}: Valid - ${ballot.voter} votes for ${ballot.candidate}`, 176 - ) 177 - alreadyVoted.add(ballot.voter) 178 - validBallots.push(ballot) 179 - }, 180 - (errors: readonly BallotError[]) => { 181 - console.log(`Ballot ${i + 1}: Invalid`) 182 - errors.forEach((e) => console.log(` - ${formatError(e)}`)) 183 - invalidBallots.push({ index: i + 1, errors }) 184 - }, 185 - )(result) 186 - }) 208 + () => [] as { index: number; errors: readonly BallotError[] }[], 209 + (errors: readonly BallotError[]) => [{ index: i + 1, errors }], 210 + )(r), 211 + ) 187 212 188 213 // Summary 189 214 console.log("\n" + "=".repeat(50)) ··· 193 218 console.log(`Invalid ballots: ${invalidBallots.length}`) 194 219 195 220 console.log("\nVoters who cast valid ballots:") 196 - validBallots.forEach((b) => console.log(` - ${b.voter} → ${b.candidate}`)) 221 + console.log(validBallots.map((b) => ` - ${b.voter} → ${b.candidate}`).join("\n")) 197 222 198 223 console.log("\nBallots with multiple errors:") 199 - invalidBallots 200 - .filter((b) => b.errors.length > 1) 201 - .forEach((b) => { 202 - console.log(` Ballot ${b.index}: ${b.errors.length} errors`) 203 - }) 224 + console.log( 225 + invalidBallots 226 + .filter((b) => b.errors.length > 1) 227 + .map((b) => ` Ballot ${b.index}: ${b.errors.length} errors`) 228 + .join("\n"), 229 + ) 204 230 205 231 console.log("\n=== End of validation phase ===") 206 232 console.log("Tomorrow, we count. And counting can also go wrong...")
+9 -10
examples/stories/forest-election/02-result.ts
··· 19 19 chainResult, 20 20 mapResult, 21 21 matchResult, 22 + match, 22 23 pipe, 23 24 } from "../../../src/index" 24 25 ··· 162 163 // Error Formatting 163 164 // ============================================================================= 164 165 165 - const formatError = (error: CountError): string => { 166 - switch (error._tag) { 167 - case "EmptyBallotBox": 168 - return "No ballots in the box" 169 - case "TotalMismatch": 170 - return `Count mismatch: expected ${error.expected}, got ${error.actual}` 171 - case "TieDetected": 172 - return `Tie between ${error.candidates.join(" and ")} with ${error.votes} votes each` 173 - } 174 - } 166 + const formatError = (error: CountError): string => 167 + match(error)({ 168 + EmptyBallotBox: () => "No ballots in the box", 169 + TotalMismatch: ({ expected, actual }) => 170 + `Count mismatch: expected ${expected}, got ${actual}`, 171 + TieDetected: ({ candidates, votes }) => 172 + `Tie between ${candidates.join(" and ")} with ${votes} votes each`, 173 + }) 175 174 176 175 // ============================================================================= 177 176 // Demo
+20 -19
examples/stories/forest-election/03-effects.ts
··· 24 24 runPromise, 25 25 retry, 26 26 all, 27 + match, 27 28 } from "../../../src/index" 28 29 29 30 // ============================================================================= ··· 169 170 // Error Formatting 170 171 // ============================================================================= 171 172 172 - const formatError = (error: MessageError): string => { 173 - switch (error._tag) { 174 - case "BirdDistracted": 175 - return `${error.bird} got distracted by ${error.reason}` 176 - case "BirdLost": 177 - return `${error.bird} got lost` 178 - case "Timeout": 179 - return `Message to ${error.region} timed out` 180 - } 181 - } 173 + const formatError = (error: MessageError): string => 174 + match(error)({ 175 + BirdDistracted: ({ bird, reason }) => `${bird} got distracted by ${reason}`, 176 + BirdLost: ({ bird }) => `${bird} got lost`, 177 + Timeout: ({ region }) => `Message to ${region} timed out`, 178 + }) 182 179 183 180 // ============================================================================= 184 181 // Demo ··· 211 208 console.log("─".repeat(50)) 212 209 213 210 console.log(`\nDelivered: ${delivered.length}/4`) 214 - delivered.forEach((d) => { 215 - const flightTime = d.result.confirmationTime - startTime 216 - console.log(` ✓ ${d.result.region}`) 217 - console.log(` via ${d.result.deliveredBy} (${flightTime}ms)`) 218 - }) 211 + console.log( 212 + delivered 213 + .map((d) => { 214 + const flightTime = d.result.confirmationTime - startTime 215 + return ` ✓ ${d.result.region}\n via ${d.result.deliveredBy} (${flightTime}ms)` 216 + }) 217 + .join("\n"), 218 + ) 219 219 220 220 if (failed.length > 0) { 221 221 console.log(`\nFailed: ${failed.length}/4`) 222 - failed.forEach((f) => { 223 - console.log(` ✗ ${f.region}`) 224 - console.log(` ${formatError(f.error)}`) 225 - }) 222 + console.log( 223 + failed 224 + .map((f) => ` ✗ ${f.region}\n ${formatError(f.error)}`) 225 + .join("\n"), 226 + ) 226 227 } 227 228 228 229 console.log(`\nTotal time: ${elapsed}ms`)
+4 -4
examples/user-registration/with-purus.ts
··· 282 282 (_data) => console.log("Unexpectedly valid!"), 283 283 (errors) => { 284 284 console.log("Validation errors:") 285 - errors.forEach((err) => console.log(` - ${formatValidationError(err)}`)) 285 + console.log(errors.map((err) => ` - ${formatValidationError(err)}`).join("\n")) 286 286 }, 287 287 )(result1) 288 288 console.log() ··· 336 336 337 337 console.log(`\nResults:`) 338 338 console.log(` Successful: ${batchResult.successful.length}`) 339 - batchResult.successful.forEach((u) => console.log(` - ${u.name} (${u.id})`)) 339 + console.log(batchResult.successful.map((u) => ` - ${u.name} (${u.id})`).join("\n")) 340 340 341 341 console.log(` Failed: ${batchResult.failed.length}`) 342 - batchResult.failed.forEach((f) => 343 - console.log(` - ${f.input.name}: ${formatApiError(f.error)}`), 342 + console.log( 343 + batchResult.failed.map((f) => ` - ${f.input.name}: ${formatApiError(f.error)}`).join("\n"), 344 344 ) 345 345 346 346 console.log("\n=== Done ===")
+26 -6
src/data/guards.ts
··· 7 7 // ============================================================================= 8 8 // Primitive Type Guards 9 9 // ============================================================================= 10 + // TS 5.5+ infers type predicates from simple narrowing expressions. 11 + // These need no explicit `: x is T` — the compiler sees `typeof x === "string"` 12 + // and infers the return type as `x is string` automatically. 10 13 11 14 /** Type guard for string values */ 12 - export const isString = (x: unknown): x is string => typeof x === "string" 15 + export const isString = (x: unknown) => typeof x === "string" 13 16 14 17 /** Type guard for number values */ 15 - export const isNumber = (x: unknown): x is number => typeof x === "number" 18 + export const isNumber = (x: unknown) => typeof x === "number" 16 19 17 20 /** Type guard for boolean values */ 18 - export const isBoolean = (x: unknown): x is boolean => typeof x === "boolean" 21 + export const isBoolean = (x: unknown) => typeof x === "boolean" 19 22 20 23 /** Type guard for non-null object values */ 21 - export const isObject = (x: unknown): x is object => 22 - typeof x === "object" && x !== null 24 + export const isObject = (x: unknown) => typeof x === "object" && x !== null 23 25 24 26 /** Type guard for array values */ 25 - export const isArray = (x: unknown): x is unknown[] => Array.isArray(x) 27 + export const isArray = (x: unknown) => Array.isArray(x) 26 28 27 29 // ============================================================================= 28 30 // Number Property Guards 29 31 // ============================================================================= 32 + // Compound guards that chain other guards (e.g. isNumber(x) && x > 0) 33 + // cannot be inferred — TS only infers single-step narrowings. 34 + // These still need explicit `: x is number`. 30 35 31 36 /** Type guard for positive numbers (x > 0) */ 32 37 export const isPositive = (x: unknown): x is number => isNumber(x) && x > 0 ··· 41 46 /** Type guard for finite numbers (excludes Infinity and NaN) */ 42 47 export const isFiniteNumber = (x: unknown): x is number => 43 48 isNumber(x) && Number.isFinite(x) 49 + 50 + // ============================================================================= 51 + // Nullability Guards 52 + // ============================================================================= 53 + // TS 5.5+ infers these — `x !== undefined` is a single-step narrowing. 54 + 55 + /** Narrows `T | undefined` to `T`. Use with `.filter(isDefined)` or in pipes. */ 56 + export const isDefined = <T>(x: T | undefined) => x !== undefined 57 + 58 + /** Narrows `T | null` to `T`. */ 59 + export const isNotNull = <T>(x: T | null) => x !== null 60 + 61 + /** Narrows `T | null | undefined` to `T`. */ 62 + export const isNotNullish = <T>(x: T | null | undefined) => 63 + x !== null && x !== undefined 44 64 45 65 // ============================================================================= 46 66 // Guard Combinators
+4 -5
src/effect/combinators.ts
··· 43 43 * ) 44 44 * ``` 45 45 */ 46 - export const ensure = <A, E2>( 47 - predicate: (a: A) => boolean, 48 - onFalse: (a: A) => E2, 49 - ) => 46 + export const ensure = 47 + <A, E2>(predicate: (a: A) => boolean, onFalse: (a: A) => E2) => 50 48 <E, R>(eff: Eff<A, E, R>): Eff<A, E | E2, R> => 51 49 pipe( 52 50 eff, ··· 72 70 * ) 73 71 * ``` 74 72 */ 75 - export const tapErr = <E>(f: (e: E) => void) => 73 + export const tapErr = 74 + <E>(f: (e: E) => void) => 76 75 <A, R>(eff: Eff<A, E, R>): Eff<A, E, R> => 77 76 pipe( 78 77 eff,
+6 -4
src/prelude/match.ts
··· 102 102 <D extends string>(discriminant: D) => 103 103 <T extends Record<D, string>>(value: T) => 104 104 <R>(cases: { [K in T[D] & string]: (v: Extract<T, Record<D, K>>) => R }): R => 105 - (cases as unknown as Record<string, (v: T) => R>)[value[discriminant]]!(value) 105 + (cases as unknown as Record<string, (v: T) => R>)[value[discriminant]]!( 106 + value, 107 + ) 106 108 107 109 /** 108 110 * Pattern matching with a custom discriminant and a default case. ··· 129 131 (cases as unknown as Record<string, ((v: T) => R) | undefined>)[ 130 132 value[discriminant] 131 133 ] !== undefined 132 - ? (cases as unknown as Record<string, (v: T) => R>)[ 133 - value[discriminant] 134 - ]!(value) 134 + ? (cases as unknown as Record<string, (v: T) => R>)[value[discriminant]]!( 135 + value, 136 + ) 135 137 : defaultValue 136 138 137 139 /**
+1 -2
src/prelude/option.ts
··· 210 210 */ 211 211 export const sequenceOption = <A>( 212 212 options: readonly Option<A>[], 213 - ): Option<readonly A[]> => 214 - traverseOption<Option<A>, A>((o) => o)(options) 213 + ): Option<readonly A[]> => traverseOption<Option<A>, A>((o) => o)(options) 215 214 216 215 /** 217 216 * Try an alternative when an Option is None.
+5 -7
tests/effect-basic.test.ts
··· 5 5 bracket, 6 6 catchAll, 7 7 type Eff, 8 - ensure, 9 8 Exit, 9 + ensure, 10 10 fail, 11 11 flatMap, 12 12 foldEff, ··· 105 105 106 106 const result = pipe( 107 107 dbEffect, 108 - flatMap((n): Eff<string, NotFound, unknown> => 109 - n > 0 ? succeed(String(n)) : fail({ _tag: "NotFound" }), 108 + flatMap( 109 + (n): Eff<string, NotFound, unknown> => 110 + n > 0 ? succeed(String(n)) : fail({ _tag: "NotFound" }), 110 111 ), 111 112 ) 112 113 ··· 125 126 ) 126 127 127 128 const value = await runPromise( 128 - pipe( 129 - result, 130 - provide({ db: "pg", logger: "console" } as Db & Logger), 131 - ), 129 + pipe(result, provide({ db: "pg", logger: "console" } as Db & Logger)), 132 130 ) 133 131 expect(value).toBe("console1") 134 132 })
+8 -2
tests/effect-concurrency.test.ts
··· 271 271 272 272 it("runs effects concurrently", async () => { 273 273 const start = Date.now() 274 - const eff1 = pipe(sleep(50), mapEff(() => "a")) 275 - const eff2 = pipe(sleep(50), mapEff(() => "b")) 274 + const eff1 = pipe( 275 + sleep(50), 276 + mapEff(() => "a"), 277 + ) 278 + const eff2 = pipe( 279 + sleep(50), 280 + mapEff(() => "b"), 281 + ) 276 282 const result = await runPromise(zip(eff1, eff2)) 277 283 const elapsed = Date.now() - start 278 284 expect(result).toEqual(["a", "b"])
+34
tests/guards.test.ts
··· 2 2 import { 3 3 and, 4 4 isBoolean, 5 + isDefined, 6 + isNotNull, 7 + isNotNullish, 5 8 isNumber, 6 9 isPositive, 7 10 isString, ··· 23 26 it("isBoolean checks for booleans", () => { 24 27 expect(isBoolean(true)).toBe(true) 25 28 expect(isBoolean(0)).toBe(false) 29 + }) 30 + }) 31 + 32 + describe("nullability guards", () => { 33 + it("isDefined narrows T | undefined to T", () => { 34 + expect(isDefined("hello")).toBe(true) 35 + expect(isDefined(0)).toBe(true) 36 + expect(isDefined(false)).toBe(true) 37 + expect(isDefined(null)).toBe(true) 38 + expect(isDefined(undefined)).toBe(false) 39 + }) 40 + 41 + it("isNotNull narrows T | null to T", () => { 42 + expect(isNotNull("hello")).toBe(true) 43 + expect(isNotNull(0)).toBe(true) 44 + expect(isNotNull(undefined)).toBe(true) 45 + expect(isNotNull(null)).toBe(false) 46 + }) 47 + 48 + it("isNotNullish narrows T | null | undefined to T", () => { 49 + expect(isNotNullish("hello")).toBe(true) 50 + expect(isNotNullish(0)).toBe(true) 51 + expect(isNotNullish(false)).toBe(true) 52 + expect(isNotNullish(null)).toBe(false) 53 + expect(isNotNullish(undefined)).toBe(false) 54 + }) 55 + 56 + it("isDefined works with filter", () => { 57 + const xs: (number | undefined)[] = [1, undefined, 3, undefined, 5] 58 + const result: number[] = xs.filter(isDefined) 59 + expect(result).toEqual([1, 3, 5]) 26 60 }) 27 61 }) 28 62
+10 -8
tests/pattern-matching.test.ts
··· 68 68 }) 69 69 70 70 it("matches on custom discriminant field", () => { 71 - expect(describeError({ type: "TOO_LONG", maxLength: 100, actualLength: 200 })) 72 - .toBe("Too long: 200/100") 71 + expect( 72 + describeError({ type: "TOO_LONG", maxLength: 100, actualLength: 200 }), 73 + ).toBe("Too long: 200/100") 73 74 }) 74 75 75 76 it("narrows type in handlers", () => { 76 - expect(describeError({ type: "EMPTY", message: "Cannot be empty" })) 77 - .toBe("Empty: Cannot be empty") 77 + expect(describeError({ type: "EMPTY", message: "Cannot be empty" })).toBe( 78 + "Empty: Cannot be empty", 79 + ) 78 80 }) 79 81 }) 80 82 ··· 90 92 }) 91 93 92 94 it("returns handler result for matched case", () => { 93 - expect(describeStatus({ kind: "banned", reason: "spam" })) 94 - .toBe("Banned: spam") 95 + expect(describeStatus({ kind: "banned", reason: "spam" })).toBe( 96 + "Banned: spam", 97 + ) 95 98 }) 96 99 97 100 it("returns default for unmatched case", () => { 98 - expect(describeStatus({ kind: "active" })) 99 - .toBe("unknown") 101 + expect(describeStatus({ kind: "active" })).toBe("unknown") 100 102 }) 101 103 }) 102 104
+4 -5
tests/validation.test.ts
··· 210 210 211 211 describe("validate2", () => { 212 212 it("combines two valid values", () => { 213 - const result = validate2( 214 - valid("Alice"), 215 - valid(30), 216 - (name, age) => ({ name, age }), 217 - ) 213 + const result = validate2(valid("Alice"), valid(30), (name, age) => ({ 214 + name, 215 + age, 216 + })) 218 217 expect(result).toEqual(valid({ name: "Alice", age: 30 })) 219 218 }) 220 219
+2
tsconfig.json
··· 14 14 "skipLibCheck": true, 15 15 "noFallthroughCasesInSwitch": true, 16 16 "noUncheckedIndexedAccess": true, 17 + "exactOptionalPropertyTypes": true, 18 + "noPropertyAccessFromIndexSignature": true, 17 19 "noImplicitOverride": true, 18 20 "noUnusedLocals": true, 19 21 "noUnusedParameters": true,