diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db55b91..9b3e043 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: [main, develop] pull_request: branches: [main, develop] + workflow_dispatch: workflow_call: inputs: prepare-publish: @@ -16,6 +17,23 @@ permissions: contents: read jobs: + sites: + name: Frontend checks + uses: ./.github/workflows/sites.yml + + deploy-sites: + name: Deploy websites + needs: [build, base-install, lint, types, tests, sites] + if: >- + github.repository == 'UlloaSP/mlbundle' && + github.ref == 'refs/heads/main' && + !inputs.prepare-publish && + (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + uses: ./.github/workflows/deploy-sites.yml + secrets: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + build: name: Build distributions runs-on: ubuntu-latest diff --git a/.github/workflows/deploy-sites.yml b/.github/workflows/deploy-sites.yml new file mode 100644 index 0000000..aed28bd --- /dev/null +++ b/.github/workflows/deploy-sites.yml @@ -0,0 +1,71 @@ +name: Deploy websites + +on: + workflow_call: + secrets: + CLOUDFLARE_API_TOKEN: + required: true + CLOUDFLARE_ACCOUNT_ID: + required: true + +permissions: + contents: read + +jobs: + deploy: + name: Deploy ${{ matrix.site }} + if: github.repository == 'UlloaSP/mlbundle' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + include: + - site: landing + url: https://mlbundle.eshkra.dev + title: MLbundle + - site: docs + url: https://docs.mlbundle.eshkra.dev + title: mlbundle documentation + - site: playground + url: https://playground.mlbundle.eshkra.dev + title: MLbundle Playground + environment: + name: ${{ matrix.site }} + url: ${{ matrix.url }} + concurrency: + group: mlbundle-${{ matrix.site }}-production + cancel-in-progress: false + steps: + - uses: actions/checkout@v4 + + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@v1.21.2 + with: + node-version: '24.14.1' + cache: true + + - name: Install locked deployment tooling + run: vp install --frozen-lockfile + + - name: Download tested site + uses: actions/download-artifact@v4 + with: + name: ${{ matrix.site }}-build + path: ${{ matrix.site }}/dist/ + + - name: Deploy static assets and custom domain + run: vp exec wrangler deploy --config "$SITE/wrangler.jsonc" + env: + SITE: ${{ matrix.site }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + - name: Check the deployed homepage + env: + SITE_URL: ${{ matrix.url }} + EXPECTED_TITLE: ${{ matrix.title }} + run: | + curl --fail --silent --show-error --location --retry 10 --retry-delay 15 \ + --retry-all-errors --max-time 20 "$SITE_URL/" -o "$RUNNER_TEMP/site.html" + grep -Fq "
+
+
Your trained model, schema and verified structure. One file.
+ + + +MLbundle packages a trained model with its manifest, locally inferred input schema +and extractable structure in one compressed `.mlbundle` file. The same library +writes the bundle and validates it when loaded, preserving feature order, class +labels and output meaning across the training/runtime boundary. + +Use it to hand a model to [MLSuite](https://github.com/UlloaSP/mlsuite), inspect +its metadata or build your own consumer around the validated native model. + +## Why bundle a model? + +A model file alone leaves the next application to recover its inputs, labels, +environment and supporting metadata. Separate descriptions can drift from the +model they are supposed to describe. + +MLbundle keeps them together and checks model-derived claims against the loaded +model. Schema inference runs locally through [MLschema](https://github.com/UlloaSP/mlschema); +real training rows are included only when explicitly requested. Native formats +preserve the supported model without conversion. + +## Installation + +MLbundle **0.2.0** requires Python **3.14** for export with the schema extra. +The installation command for this release is: + +```bash +python -m pip install "mlbundle[sklearn,schema]==0.2.0" +``` -- **Pickle code execution:** closed adapters use skops, native XGBoost UBJSON, - CatBoost CBM or an existing ONNX file, without pickle/joblib/cloudpickle. -- **Missing training versions:** the manifest records Python, OS, architecture - and installed package versions alongside model and feature metadata. -- **Training data leaving the machine:** schema inference runs locally; - training rows are included only when explicitly requested as samples. +To use a local checkout, run `python -m pip install -e ".[sklearn,schema]"` +from its root. The base library requires Python >=3.14; schema inference uses +mlschema `>=0.3.0,<0.4` on Python 3.14 only. -**Pre-alpha v0.1.0a3, not yet published.** Version 0.1.0a2 is on PyPI. Format version 1 is -not frozen; bundles may need re-export. The security design has not had an -independent review: do not accept bundles from untrusted parties. -Requires **Python >=3.14**; tested on Windows/Linux. Schema requires Python 3.14. +Choose `xgboost`, `catboost` or `onnx` instead of `sklearn`, or install `[all]`. +Export always needs `schema`. Metadata-only inspection needs only the base +package; load and CLI verification need the matching model extra. -## Install and try scikit-learn +## Quick start -Install the pre-release with -`python -m pip install "mlbundle[sklearn,schema]==0.1.0a3"` once published. -Replace `sklearn` with `xgboost`, `catboost` or `onnx`, or use `[all]` for all dependencies. -Export needs the `schema` extra; reading does not. Omit the brackets for metadata-only inspection; load/verify needs the matching model extra. +Train a model, export its schema without including training rows, then inspect +and load the bundle: ```python +import pandas as pd from sklearn.tree import DecisionTreeClassifier -from mlbundle import export, inspect, load - -rows = [[0.0, 1.0], [1.0, 0.0], [0.1, 0.9], [0.9, 0.1]] -model = DecisionTreeClassifier(random_state=7).fit(rows, ["cold", "warm"] * 2) -export(model, "trained.mlbundle", feature_names=["temperature", "humidity"]) -bundle = load("trained.mlbundle") -assert isinstance(bundle.native_model, DecisionTreeClassifier) -assert bundle.native_model.predict(rows).tolist() == model.predict(rows).tolist() -assert bundle.class_labels == [["cold", "warm"]] -print(bundle.predict_proba([[0.0, 1.0]])) # [[1.0, 0.0]] -assert inspect("trained.mlbundle").manifest.model.library == "sklearn" -``` - -`export` returns metadata and requires `overwrite=True` for existing destinations. -`load` accepts paths, bytes or streams. Classifiers use `bundle.predict_proba`; -regressors use `bundle.predict`. Results are plain lists: output-major classifier -labels/probabilities, row-major regression values. `native_model` supports native -inference/explanations; mutating it invalidates the metadata verification. - -Native names win; otherwise origin is `declared`, `dataframe`, or `generated` (`feature_0` through `feature_{N-1}`). -For models without native names, pass a 2-D NumPy array or nested numeric rows in manifest feature order; declared names do not add name-based prediction. -`target_names=` and `metadata=` remain author declarations. -## Runtime records - -Load with the matching model extra, then use the bundle's own record mapping: +from mlbundle import export, inspect, load -```python -from mlbundle import load +training = pd.DataFrame( + { + "temperature": [0.0, 1.0, 0.1, 0.9], + "humidity": [1.0, 0.0, 0.9, 0.1], + "target": ["cold", "warm", "cold", "warm"], + } +) +model = DecisionTreeClassifier(random_state=7).fit( + training.drop(columns="target"), training["target"] +) -bundle = load("trained.mlbundle") -record = {"0": "0.0", "1": "true"} -print(bundle.predict_record(record)[0].probabilities) # (1.0, 0.0) -assert bundle.row_from_record(record) == [0.0, 1.0] # Also usable for explanations. -``` +export( + model, + "trained.mlbundle", + training_data=training, + target_names=["target"], +) -Model-origin keys are native feature names. Other origins use zero-based positions -as integers or canonical decimal strings; supplying both forms is an error. -Every feature is required as a key. Extra keys are ignored without iterating the -record, so work depends on feature count rather than record size. -Values accept finite Python numbers, booleans and decimal or true/false strings. -Strings strip ASCII whitespace and are limited to 64 characters; decimal forms -include `.5`, `1.` and exponents. `None` becomes NaN only when the schema field -permits missing values, using the group's requirement for one-hot options. -`PredictionInputError` names invalid keys without echoing values. Array/DataFrame -prediction retains its existing rules. Results are frozen `OutputPrediction` -records in output order, with classifier labels/probabilities or a regression value. -`predicted_label` is the report string for the native model's own class decision, -which may differ from the largest probability. It is `None` for regressors and -ONNX classifiers without a graph label output; mlbundle never substitutes argmax. -Name-selecting sklearn pipelines use a DataFrame and require pandas (included in -`schema`); other models use a numeric row, preserving native categorical handling. -`EnvironmentMismatchError` diagnoses a failed skops load with differing sklearn -versions. `InvalidManifestError` explains safe, different writer versions on a -failed manifest check. Neither diagnostic adds a compatibility gate. - -## XGBoost - -Install the `xgboost` extra. Train on encoded indices and supply the original -labels in probability-column order, nested even for one output: +metadata = inspect("trained.mlbundle") # No model deserialization. +print(metadata.manifest.features.names) # ["temperature", "humidity"] -```python -from xgboost import XGBClassifier -from mlbundle import export, load - -rows = [[0.0, 1.0], [1.0, 0.0], [0.1, 0.9], [0.9, 0.1]] -model = XGBClassifier(n_estimators=3, max_depth=2, n_jobs=1).fit(rows, [0, 1, 0, 1]) -export(model, "xgboost.mlbundle", class_labels=[["cold", "warm"]]) -bundle = load("xgboost.mlbundle") -assert isinstance(bundle.native_model, XGBClassifier) -assert bundle.native_model.predict(rows).tolist() == model.predict(rows).tolist() -assert bundle.predict_proba(rows) == model.predict_proba(rows).tolist() -assert bundle.class_labels == [["cold", "warm"]] +bundle = load("trained.mlbundle") # Recomputes native claims and structure. +result = bundle.predict_record({"temperature": 0.0, "humidity": 1.0})[0] +print(result.predicted_label) # cold +print(result.probabilities) # (1.0, 0.0) ``` -Labels/settings bind to a native **copy**. Load verifies consistency, not history. -Omitted labels use encoded indices/an existing binding; native predict stays encoded. -Standalone sklearn, CatBoost and regressors reject `class_labels=`; composites -delegate labels to the final native adapter. +The target column is excluded from the input schema. Add `sample_rows=N` only +when you intend to include the first N real feature rows. Without +`training_data=`, export infers a default schema from synthetic values. -Only recoverable native configuration is recorded; learning rate, max depth, -seed and thread count do not survive native XGBoost I/O and are not reconstructed. +Existing destinations require `overwrite=True`. Native feature names take +precedence; otherwise mappings use zero-based positions. `load` and `inspect` +accept paths, bytes or streams. The native model remains available as +`bundle.native_model`; mutating it invalidates the verified metadata. -## CatBoost +## Choose your operation -Install the `catboost` extra (>=1.2.10; includes matplotlib/plotly). Numeric, -single-target symmetric classifiers/regressors use native CBM and native labels: +| Operation | Purpose | +| --- | --- | +| `export(model, destination, ...)` | Validate a native round trip, infer the schema locally and write a bundle. | +| `inspect(source)` | Validate the container, manifest, hashes and declarations without loading a model. | +| `load(source)` | Load through a closed adapter and verify model-derived claims and structure. | +| `bundle.predict_proba(data)` | Classifier probabilities as plain lists. | +| `bundle.predict(data)` | Regressor predictions as plain lists. | +| `bundle.predict_record(record)` | Map one record through the schema and return frozen output-major results. | +| `bundle.row_from_record(record)` | Validate and order record values for native inference or explanations. | -```python -from catboost import CatBoostClassifier -from mlbundle import export, load - -rows = [[0.0, 1.0], [1.0, 0.0], [0.1, 0.9], [0.9, 0.1]] -model = CatBoostClassifier( - iterations=3, depth=2, verbose=False, allow_writing_files=False -).fit(rows, ["cold", "warm"] * 2) -export(model, "catboost.mlbundle") -bundle = load("catboost.mlbundle") -assert bundle.class_labels == [["cold", "warm"]] -assert bundle.predict_proba(rows) == model.predict_proba(rows).tolist() -``` - -`class_labels=`, categorical/text/embedding features, -Depthwise/Lossguide, custom Python objectives/metrics and other losses are rejected. -Classifier losses: Logloss, CrossEntropy, MultiClass. Regressors: RMSE, MAE, -Quantile, Huber, Poisson, MAPE. Complete trees/borders/NaN handling/importances -are verified on load. [The CatBoost contract](https://github.com/UlloaSP/mlbundle/blob/main/docs/format-catboost.md) details -wrapper recovery, native-parser limits and the private in-memory serializer. +The CLI exposes metadata inspection and full verification: -## Composite pipelines - -Install `[sklearn,xgboost]` or `[sklearn,catboost]`. Export the fitted Pipeline -as it is; transformers use skops and the final estimator uses its native adapter: - -```python -from sklearn.pipeline import make_pipeline -from sklearn.preprocessing import StandardScaler -from xgboost import XGBClassifier -from mlbundle import export, load - -rows = [[0.0, 1.0], [1.0, 0.0], [0.1, 0.9], [0.9, 0.1]] -model = make_pipeline(StandardScaler(), XGBClassifier(n_estimators=3, n_jobs=1)) -model.fit(rows, [0, 1, 0, 1]) -export(model, "pipeline.mlbundle", class_labels=[["cold", "warm"]]) -bundle = load("pipeline.mlbundle") -assert bundle.predict_proba(rows) == model.predict_proba(rows).tolist() -assert bundle.native_model.predict(rows).tolist() == model.predict(rows).tolist() -assert bundle.class_labels == [["cold", "warm"]] +```shell +mlbundle inspect trained.mlbundle +mlbundle inspect trained.mlbundle --json +mlbundle verify trained.mlbundle ``` -Load rebuilds a real Pipeline after verifying both parts. Bundle probability labels -use XGBoost's bound originals; native classes/predict stay encoded. CatBoost keeps -native labels. Prefixes include scalers, PCA, SimpleImputer (indicators/empty columns), -KBinsDiscretizer, PolynomialFeatures, univariate selectors, OneHotEncoder/OrdinalEncoder, -identity/closed NumPy FunctionTransformer, nested native-free Pipeline and ColumnTransformer. -Each needs a fitted, data-free width proof. Unknown widths, FeatureUnion/KNNImputer, -slice/callable/scalar selectors, caching, metadata routing and other native placements -remain rejected. Step names/outer options are declarations; trees address transformed -features, without original-feature attribution. Schema/dtypes/samples describe original -inputs; six members permit schema plus explicit samples. See [the pipeline contract](https://github.com/UlloaSP/mlbundle/blob/main/docs/format-pipeline.md). - -## Existing ONNX files - -Install the `onnx` extra. This example assumes your existing `trained.onnx` is -a self-contained tabular classifier with two numeric features and two probability -columns. Labels on a label-free graph are count-checked declarations: - -```python -from pathlib import Path -from mlbundle import export, load - -original = Path("trained.onnx").read_bytes() -export("trained.onnx", "onnx.mlbundle", class_labels=[["cold", "warm"]]) -bundle = load("onnx.mlbundle") -assert bundle.native_model.data == original -assert bundle.native_model.session.get_providers() == ["CPUExecutionProvider"] -assert bundle.class_labels == [["cold", "warm"]] -print(bundle.predict_proba([[0.0, 0.0]])) +Inspection does not prove that metadata describes the model. Full verification +loads the model and compares its claims. Neither operation establishes authorship +or training history. + +## Supported models + +| Library | Storage | Support boundary | +| --- | --- | --- | +| scikit-learn | skops | Closed fitted classifier/regressor families and supported transformers. | +| XGBoost | Native UBJSON | Exact single-target tree wrappers and closed objectives. | +| CatBoost | Native CBM | Exact numeric, single-target symmetric classifiers/regressors. | +| ONNX | Original ONNX bytes | Self-contained tabular models with supported signatures; no conversion. | +| Composite pipelines | skops + UBJSON or CBM | Supported sklearn preprocessing with an XGBoost/CatBoost final estimator. | + +Custom types fail at export. Sklearn and CatBoost own their class labels; +XGBoost can bind declared labels to a native copy. ONNX has no visualization +structure, and all-sklearn Pipeline structure is null. The +[adapter contracts](./docs/format-adapters.md) define the exact support limits. + +## Some notes + +Package versions and wire-format versions are independent. Format 1 is not frozen; +incompatible schema shapes require re-export. The security design has not had an +independent review; do not accept bundles from untrusted parties. + +The reader bounds ZIP/JSON data, verifies integrity and uses closed native loaders. +Hashes detect corruption, not deliberate tampering. Native parsers and inference +still need process isolation and application-owned resource limits. +Read [SECURITY.md](./SECURITY.md) for guarantees, limits and private reporting. + +Training rows stay local by default, but feature names, categories, labels and +learned structure can reveal information. Schemas and environment records remain +author declarations. Model training, hosting, signing and conversion are outside +the library's scope. + +## Documentation + +The [documentation site](./docs/) includes installation, adapter guides, record +prediction and API reference. The normative contracts remain in this repository: + +- [Bundle format and verification](./docs/format.md) +- [Schema, mappings and explicit samples](./docs/format-schema.md) +- [Supported adapters and structure](./docs/format-adapters.md) +- [Sklearn state and trusted types](./docs/format-state.md) +- [Composite pipelines](./docs/format-pipeline.md) +- [ONNX layouts and bounds](./docs/format-onnx.md) + +The [playground](./playground/) provides editable examples and inspection of the +generated `.mlbundle` file. It runs scikit-learn examples in the browser and +inspects uploaded files without loading their models. + +## Contributing + +The Python library uses [uv](https://docs.astral.sh/uv/) and Python 3.14: + +```bash +uv python install 3.14 +uv sync --locked --all-extras +uv run pre-commit install +uv run ruff check . +uv run ruff format --check . +uv run pyright +uv run pytest +uv build ``` -Paths and bytes are preserved unchanged; a ModelProto is serialized once. -There is no conversion. `native_model` contains the original `data` and CPU -`session`. Native graph labels are verified; supplied labels must agree with -them. Without native/supplied labels, encoded indices are generated. ONNX has -no visualization structure. See [the exact ONNX contract](https://github.com/UlloaSP/mlbundle/blob/main/docs/format-onnx.md). - -## Local schema and explicit samples +Tests use the installed package and enforce 80% branch-inclusive coverage. +CI validates the built wheel on Windows and Linux. Read [AGENTS.md](./AGENTS.md) +for contribution rules and [publishing](./docs/publishing.md) for the owner-approved +release workflow. -Install a model extra plus `schema` on Python 3.14. Supply a nonempty pandas -DataFrame with string feature names in model input order. `target_names` has one -unique name per output; every target column must exist and is excluded. Without it the frame contains features only. +The landing, docs and playground use [Vite+](https://viteplus.dev/guide/) and a +pnpm workspace. With `vp` installed, run from the repository root: -```python -import pandas as pd -from sklearn.tree import DecisionTreeClassifier -from mlbundle import export, inspect, load - -training = pd.DataFrame( - [[0.0, 1.0, "cold"], [1.0, 0.0, "warm"]] * 2, - columns=["temperature", "humidity", "target"], -) -features = training.drop(columns="target") -model = DecisionTreeClassifier(random_state=7).fit(features, training["target"]) -export(model, "schema.mlbundle", training_data=training, target_names=["target"]) -view = inspect("schema.mlbundle") -assert view.manifest.features.dtypes == ["float64", "float64"] -assert view.schema["reports"][0]["labels"] == ["cold", "warm"] -assert view.samples is None -assert load("schema.mlbundle").schema == view.schema -# Samples are real data: explicitly include the first two feature rows. -export(model, "sampled.mlbundle", training_data=features, sample_rows=2) -assert inspect("sampled.mlbundle").samples == features.head(2).to_dict("records") -``` - -`schema.json` is required and uses schema version `2`, with `source` equal to -`training_data` or `default`. Without a frame, mlschema infers fields from one -synthetic row of float `1.0` values; training dtypes remain null. `onehot_separator=` -defaults to `__` and is stored in the schema (1–16 characters). `color__red`/`color__blue` -binary columns can become one category field. Native-name mappings are strings; all other origins map by zero-based feature position. Reports map by output position, -including `0` for one output, with string display labels; manifest class labels retain -their original types. Frame names must match native names in order; `feature_names=` cannot rename/reorder a supplied frame. -Training dtypes are declarations, separate from verified model dtypes. Inspect/load -validate schema mappings, reports and sample keys without pandas or mlschema. -Bundles written by 0.1.0a1 without schema, or with schema version `1`, must be re-exported. - -**Data leaving the machine:** names/dtypes, field kinds/nullability, dtype-based -steps, categories (including unused), one-hot suffixes, pair-axis metadata and -class labels. No inferred ranges, means, counts or arbitrary text/example rows. -Names/categories and learned models/structure can reveal training information. -Explicit `sample_rows=N` includes the first N feature rows, without target/index; -N must be positive and within the frame. See [schema and samples](https://github.com/UlloaSP/mlbundle/blob/main/docs/format-schema.md). - -## Inspect, load and the CLI - -`inspect` validates archive layout, limits, manifest, hashes and schema/sample -declarations **without importing any model library**. It cannot attest that -metadata or structure matches the model. `load` additionally deserializes through -the closed adapter and recomputes native claims and structure before returning -the model and predictor. Export checks the same native round trip before writing. - -```shell -mlbundle inspect trained.mlbundle -mlbundle inspect trained.mlbundle --json -mlbundle verify trained.mlbundle +```bash +vp install +vp run check +vp run build ``` -Inspection prints metadata, members/hashes and declarations. Human values escape -terminal controls and truncate at 512 characters; `--json` is lossless ASCII-escaped -manifest JSON. `verify` requires the model extra. Errors are one bounded stderr -line without bundle content/traceback. Exit codes: **0** success, **1** bundle or -dependency rejection (`MLBundleError`), **2** usage/I/O failure. - -## Supported models and limits - -- **Sklearn:** fitted classifiers exposing `predict_proba` and regressors with recoverable feature/output counts. Tested families include trees/ensembles, linear/regularized/robust/GLM models, SVMs, PLSRegression, naive Bayes, LDA/QDA, dense brute-force neighbors, NearestCentroid, MLP and MultiOutput/RegressorChain. SimpleImputer/KNNImputer, scalers, KBinsDiscretizer, categorical encoders, univariate selectors and closed NumPy FunctionTransformer functions round-trip, including nested ColumnTransformer pipelines; see [the exact lists](https://github.com/UlloaSP/mlbundle/blob/main/docs/format-state.md). All-sklearn Pipeline structure stays null; composite trees expose the inner boundary. See - [tested configurations](https://github.com/UlloaSP/mlbundle/blob/main/tests/test_sklearn_families.py) and [core round trips/rejections](https://github.com/UlloaSP/mlbundle/blob/main/tests/test_sklearn.py). -- **Sklearn rejections:** custom estimators/subclasses, functions/types outside the closed lists (including lambdas and user functions), compound fitted dtypes, classifiers without probabilities, multilabel/ClassifierChain layouts, unsupported native placements, and untrusted nested storage such as fitted sparse neighbor arrays. HistGradientBoosting uses untrusted `TreePredictor` storage and is rejected. -- **Sklearn versions:** the closed class/attribute catalog is audited/tested against **1.9.1 with skops 0.16.0**, not every version allowed by `>=1.6`. Unknown state fails; matching export/load sklearn versions are recommended. Configurations must satisfy the catalog and round trip; see [state ownership](https://github.com/UlloaSP/mlbundle/blob/main/docs/format-state.md). -- **XGBoost:** exact XGBClassifier/XGBRegressor, one target, native gbtree - (including XGBoost 3.4.1 dart normalized to gbtree). Closed probability/regression objectives; raw boosters, RF wrappers, subclasses, ranking/survival/custom objectives and unsupported storage are rejected. At most 100,000 trees/depth 14, with complete state/dumps; see [the mapping](https://github.com/UlloaSP/mlbundle/blob/main/docs/format-adapters.md#implemented-xgboost-mapping). -- **ONNX:** fixed-width rank-two numeric inputs; one regression/probability target, optional native labels and dynamic/one-row batches. Unsupported layouts, external data/custom domains/functions and runtime/training configuration fail before CPU sessions. Tested with ONNX 1.23.0/Runtime 1.30.0; see [ONNX bounds](https://github.com/UlloaSP/mlbundle/blob/main/docs/format-onnx.md). - -Archive ceilings: **6 members**, **512 MiB** archive/compressed/member bytes, -**1 GiB** aggregate uncompressed bytes, ratio **200**, JSON depth **32**. Manifest: **8 MiB**; structure: **256 MiB**; schema/samples: **16 MiB each**. Features/classes: at most **100,000**; outputs: **1,024** (XGBoost/CatBoost/ONNX: one). Callers may only lower `ArchiveLimits`. Native [state](https://github.com/UlloaSP/mlbundle/blob/main/docs/format-state.md)/[ONNX](https://github.com/UlloaSP/mlbundle/blob/main/docs/format-onnx.md) bounds do not cap process memory, prediction rows or CPU. - -## Bundle contents and security - -Every bundle contains `manifest.json`, `schema.json` and one native member, or the declared -`model.skops` + `model.ubj`/`model.cbm` composite pair. Single-step composites use only the leaf. Optional members are `structure.json` -(required when the adapter extracts structure) and explicitly requested -`samples.json`. No training DataFrame or arbitrary files are included. -The manifest records identity/time/writer, model parameters/counts/state, -ordered features/dtypes/origin, target labels/names, environment, member -SHA-256/sizes, references and free-form user metadata. The normative -[format specification](https://github.com/UlloaSP/mlbundle/blob/main/docs/format.md) owns these records and verification rules. - -Treat bundles as untrusted. The reader validates ZIP layout, allowed members, -limits, strict JSON/manifest and hashes; adapters use closed trust/loading paths -and recompute claims. **Hashes detect corruption, not deliberate tampering**; -there is no signing or proof of authorship, training history, quality or fairness. -Schema, samples and training-environment declarations are not authenticated. -Native parsers/inference require process isolation, memory/CPU/row limits and -maintained dependencies. See [SECURITY.md](https://github.com/UlloaSP/mlbundle/blob/main/SECURITY.md) for guarantees and reporting. - -## Development - -From the root: `uv python install 3.14`, `uv sync --locked --all-extras`, `uv run pre-commit install`. Default dev installs every extra; base: `uv sync --no-dev`. Pydantic v2 is the sole core dependency; model/schema imports are lazy. Schema uses mlschema `>=0.2.1,<0.3` on 3.14 only (0.2.1 excludes 3.15). `uv.lock` fixes tested versions; dependency edits require `uv sync` and committing the updated lockfile. - -Run `uv run ruff check .`, `uv run ruff format --check .`, `uv run pyright`, `uv run pytest` and `uv build`. Tests execute every fenced README example and write terminal/XML coverage with an 80% branch-inclusive minimum. CI checks lint/types and tests the same built wheel on Ubuntu/Windows, including clean base installs. The owner, or an agent on the owner's explicit request for that version, runs manual [publishing](https://github.com/UlloaSP/mlbundle/blob/main/docs/publishing.md) from `main` with `pypi_env` approval. Read [AGENTS.md](https://github.com/UlloaSP/mlbundle/blob/main/AGENTS.md). Agents never run or approve it on their own initiative or add publish paths, tokens or secrets. Releases, version tags, conversions, per-model environments, signing and additional model libraries remain outside scope. The project is MIT licensed. +Start a site with `vp run dev:landing`, `vp run dev:docs` or +`vp run dev:playground`. Their local ports are 4340, 4341 and 10021 respectively. +Each site also accepts `vp run dev`, `vp run build` and `vp run preview` from its +directory. Astro sites run their framework CLI through `vp exec`; the playground +uses Vite+ directly. `vp run check` runs frontend lint, landing formatting, all +three sites' type checks, and the docs and playground unit tests. Runtime and +browser tests live in `playground/`. + +Actions runs Python and frontend checks on pushes and PRs to `main` and `develop`. +After all checks pass on `main`, it deploys the three tested site builds to +Cloudflare. Manual **CI Pipeline** runs on `main` use the same gates and can +redeploy without publishing Python. A change to `project.version` on `main` +automatically starts the PyPI workflow after its version and branch preflight. +See [website deployment](./docs/deployment.md) and [publishing](./docs/publishing.md). + +## License + +MIT. The design takes ideas from MLflow's versioned manifest and native storage, +not its code. diff --git a/SECURITY.md b/SECURITY.md index f91dc70..5f1ed08 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security policy -The v0.1.0a3 development pre-alpha has not had an independent security review; +MLbundle 0.2.0 has not had an independent security review; do not accept bundles from untrusted parties. Format version 1 is not frozen. It implements strict manifest validation, secure container reading/writing, metadata-only `inspect`, export/load, sklearn, XGBoost, CatBoost and ONNX adapters, local schema inference, @@ -291,5 +291,4 @@ behavior, and the impact. Share malicious bundles or sensitive details through that private report rather than a public issue or pull request. Remove personal or confidential training data from any reproducer you submit. -Version 0.1.0a2 is published; 0.1.0a3 is not yet published. Report the affected -version and commit you tested. +Report the affected version and commit you tested. diff --git a/brand/README.md b/brand/README.md new file mode 100644 index 0000000..4b83083 --- /dev/null +++ b/brand/README.md @@ -0,0 +1,14 @@ +# MLbundle identity + +Original mark copied unchanged from `eshkra/src/assets/marks/mlbundle.png` +(708 × 960). The PNG retains its original resolution and embedded provenance. + +Outlined wordmarks use Mona Sans from the family's self-hosted Latin subset: ML at +weight 800 and width 125%, bundle at weight 300 and width 100%, matching MLform and +MLschema glyph for glyph. Font license: SIL OFL 1.1, included alongside these assets. +Wordmarks are SVG paths so GitHub and PyPI need no fonts. `wordmark-light.svg` is +graphite `#0b0b10` for light backgrounds; `wordmark-dark.svg` is `#f4f4f6` for dark ones. + +Use orange #ff6a1a with graphite and neutral backgrounds. Use MLbundle for the brand +lockup; keep `mlbundle` lowercase in package names, imports, commands, URLs and the +`.mlbundle` file extension. diff --git a/brand/mlbundle.png b/brand/mlbundle.png new file mode 100644 index 0000000..5ea5bb8 Binary files /dev/null and b/brand/mlbundle.png differ diff --git a/brand/mona-sans-LICENSE.txt b/brand/mona-sans-LICENSE.txt new file mode 100644 index 0000000..fd6728b --- /dev/null +++ b/brand/mona-sans-LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Mona Sans Project Authors (https://github.com/github/mona-sans), with Reserved Font Name "Mona" MonaSans-Italic[wdth,wght].ttf: Copyright 2022 The Mona Sans Project Authors (https://github.com/github/mona-sans), with Reserved Font Name "Mona" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/brand/wordmark-dark.svg b/brand/wordmark-dark.svg new file mode 100644 index 0000000..689867c --- /dev/null +++ b/brand/wordmark-dark.svg @@ -0,0 +1,14 @@ + diff --git a/brand/wordmark-light.svg b/brand/wordmark-light.svg new file mode 100644 index 0000000..6e250e3 --- /dev/null +++ b/brand/wordmark-light.svg @@ -0,0 +1,14 @@ + diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..ddce69b --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.astro/ diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..7265737 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,35 @@ +# mlbundle documentation site + +The Astro Starlight site for `docs.mlbundle.eshkra.dev`, package `mlbundle-docs` in the repository's +pnpm workspace. Run every command from the repository root with Vite+ (`vp`) and Node.js 22.18+, +24.11+ or 26+: + +```shell +vp install --frozen-lockfile +vp run mlbundle-docs#dev # http://127.0.0.1:4341/ +vp run mlbundle-docs#build # astro check, then a static build in docs/dist +``` + +In development the header links point to the local landing (`4340`) and playground (`10021`); +built output links to the production hosts. + +From `docs/`, use `vp run dev`, `vp run build` or `vp run preview`. These scripts +invoke Astro through `vp exec`; bare `vp dev` and `vp build` run Vite's own +commands and do not build an Astro site. + +## Single-sourced contract + +The Markdown files beside this README (`format*.md`) and the root `SECURITY.md` are the normative +contract. The site renders them in place through the `spec` collection in `src/content.config.ts`, +mapped to routes in `src/spec.ts`; relative links become site routes or GitHub links. Edit those +files, never a copy. `publishing.md` is maintainer guidance and is not rendered. + +Guides live in `src/content/docs/`. Their Python fences are checked by `uv run ruff format --check`. +The link resolver has a self-test: + +```shell +vp run mlbundle-docs#test +``` + +The brand mark is read from the repository's `brand/` directory. Actions builds, +checks and deploys this site with the other frontends; see [website deployment](deployment.md). diff --git a/docs/astro.config.ts b/docs/astro.config.ts new file mode 100644 index 0000000..6284329 --- /dev/null +++ b/docs/astro.config.ts @@ -0,0 +1,73 @@ +import { defineConfig } from "astro/config"; +import starlight from "@astrojs/starlight"; +import { fileURLToPath } from "node:url"; +import { sidebar } from "./src/navigation"; + +const repo = (path: string) => + fileURLToPath(new URL(`../${path}`, import.meta.url)).replaceAll("\\", "/"); + +export default defineConfig({ + site: "https://docs.mlbundle.eshkra.dev", + trailingSlash: "always", + devToolbar: { enabled: false }, + vite: { + // The docs read the shared brand assets at the repository root. + server: { + fs: { + allow: [fileURLToPath(new URL(".", import.meta.url)), repo("brand")], + }, + }, + }, + integrations: [ + starlight({ + title: "mlbundle", + description: + "Pickle-free, verifiable model bundles with a manifest, a local schema and model structure.", + components: { + Head: "./src/components/BrandHead.astro", + SiteTitle: "./src/components/BrandTitle.astro", + SocialIcons: "./src/components/SocialIcons.astro", + Footer: "./src/components/Footer.astro", + }, + social: [ + { + icon: "github", + label: "GitHub", + href: "https://github.com/UlloaSP/mlbundle", + }, + ], + editLink: { + baseUrl: "https://github.com/UlloaSP/mlbundle/edit/main/docs/", + }, + expressiveCode: { + // The landing's code themes, switched with the documentation theme. + themes: ["github-dark-default", "github-light-default"], + styleOverrides: { + borderRadius: "12px", + borderColor: "var(--mb-border)", + codeFontFamily: "var(--brand-font-mono)", + uiFontFamily: "var(--brand-font-sans)", + codeFontSize: "0.8125rem", + codeLineHeight: "1.7", + frames: { + shadowColor: "transparent", + editorTabBarBackground: "var(--mb-subtle)", + editorActiveTabBackground: "var(--mb-code-bg)", + editorActiveTabIndicatorTopColor: "var(--mb-ember)", + editorActiveTabIndicatorBottomColor: "transparent", + editorBackground: "var(--mb-code-bg)", + terminalTitlebarBackground: "var(--mb-subtle)", + terminalBackground: "var(--mb-code-bg)", + }, + }, + }, + customCss: [ + "./src/styles/typography.css", + "./src/styles/theme.css", + "./src/styles/content.css", + "./src/styles/home.css", + ], + sidebar, + }), + ], +}); diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..41487f1 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,85 @@ +# Website deployment + +The three websites deploy as Cloudflare Workers Static Assets. They need no +server-side Python: the playground runs Python in the visitor's browser. + +| Site | Worker | Domain | +| --- | --- | --- | +| Landing | `mlbundle-landing` | `mlbundle.eshkra.dev` | +| Documentation | `mlbundle-docs` | `docs.mlbundle.eshkra.dev` | +| Playground | `mlbundle-playground` | `playground.mlbundle.eshkra.dev` | + +Each site's `wrangler.jsonc` owns its worker name, custom domain, asset directory +and routing. Astro sites use trailing slashes and their generated 404 pages. +Cloudflare manages the custom domains and certificates during deployment. + +## Triggers and checks + +`.github/workflows/ci.yml` runs on pushes and pull requests targeting `main` or +`develop`, and supports **Run workflow** in GitHub Actions. + +- Python gates build wheel/sdist, validate metadata, lint, check types and test + the built wheel and base install on Ubuntu and Windows. +- The reusable `sites.yml` runs frontend lint, landing formatting, Astro/TypeScript + checks, docs link tests, playground unit tests and real Pyodide example tests. + It builds all three sites, validates Wrangler configuration without uploading, + and runs Chromium tests on desktop and mobile against the built playground. +- Only successful frontend runs upload `landing-build`, `docs-build` and + `playground-build`. Browser failures retain their traces for seven days. +- After **all** Python and frontend jobs pass, pushes to `main` and manual CI + runs on `main` call `deploy-sites.yml`. PRs, forks and `develop` never deploy. + +Deployment downloads each tested artifact from the same run; it does not rebuild. +It uses the locked Wrangler dependency through `vp exec`, deploys each site to +its own GitHub environment, and checks the resulting homepage title over HTTPS. +Deployments for the same site are serialized and are not cancelled midway. + +`publish.yml` runs reuse these checks, but skip website deployment. PyPI +publication starts automatically when a push to `main` changes +`project.version`, runs only from `main` through `pypi_env`, and is independent +of website deployments. No tags or GitHub releases are created; see +[publishing](publishing.md). + +## GitHub and Cloudflare setup + +The repository's Actions secrets must contain `CLOUDFLARE_API_TOKEN` and +`CLOUDFLARE_ACCOUNT_ID`. The token must authorize Workers deployments and custom +domain management for `eshkra.dev`. The zone must belong to that account and be +active in Cloudflare. Keep credentials in Actions; +never put them in a Wrangler file or commit them. + +The `landing`, `docs` and `playground` GitHub environments allow deployment only +from the `main` branch. They have no manual review gate; passing CI permits the +automatic website deployment. `pypi_env` is also limited to `main` and has no +manual review gate. + +Secrets being present does not prove their permissions or domain ownership. +The first successful Actions deployment verifies that external configuration. + +## Local validation and redeployment + +From the repository root: + +```sh +vp install --frozen-lockfile +vp run check +vp run mlbundle-playground#test:runtime +vp run build +vp run check:deploy +vp run mlbundle-playground#test:e2e:built +``` + +Install Chromium first with `vp exec playwright install chromium` from +`playground/`. `check:deploy` uses `wrangler deploy --dry-run`; it never uploads. +Node 24.14.1 and the Vite+ version pinned in `package.json` are used in Actions. + +To redeploy current `main`, select **CI Pipeline → Run workflow → main**. This +reruns every gate and does not publish to PyPI. For a rollback, revert the website +change through the usual review process and merge to `main`; the new CI run +deploys the reverted build. The repository files must be committed and pushed +before GitHub can execute a changed workflow. + +The setup follows the official [Vite+ action](https://github.com/voidzero-dev/setup-vp), +[reusable workflow](https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows) +and [Wrangler configuration](https://developers.cloudflare.com/workers/wrangler/configuration/) +documentation. diff --git a/docs/format-pipeline.md b/docs/format-pipeline.md index 74e20b8..b2e3ac2 100644 --- a/docs/format-pipeline.md +++ b/docs/format-pipeline.md @@ -1,6 +1,6 @@ # Version 1 composite pipelines -Normative section of [the format contract](format.md). This pre-release format +Normative section of [the format contract](format.md). This format extension preserves reading of standalone bundles. Older readers reject composites. ## Representation and declarations diff --git a/docs/format-schema.md b/docs/format-schema.md index 0e319e5..dae7067 100644 --- a/docs/format-schema.md +++ b/docs/format-schema.md @@ -1,7 +1,7 @@ # Format 1 schema v2 and samples Normative section of [the format contract](format.md). Local inference uses -mlschema >=0.2.1,<0.3, tested with 0.2.1. Fields, source and separator are untrusted +mlschema >=0.3.0,<0.4, validated against local 0.3.0. Fields, source and separator are untrusted declarations; integrity cannot authenticate the training data or its history. ## Export inputs and defaults @@ -35,8 +35,8 @@ without builders, overrides or custom kinds. The separator is stored and read from the schema; consumers must not assume the default. Model-origin fields retain name mappings. Dataframe/declared inference preserves -named columns for grouping, then rewrites parent/one-hot `mappedTo` values -to their zero-based feature position. Series columns have no mappings. Generated +named columns for grouping, then calls `mlschema.with_positional_mappings(fields, names)` to convert +parent/one-hot targets to their zero-based feature position. Series columns have no mappings. Generated origin is inferred on positional columns directly. Labels remain feature names. For models without native names, prediction accepts 2-D NumPy arrays or nested numeric rows in manifest feature order. Adapter-supported sparse arrays and @@ -76,8 +76,8 @@ All five keys are required and unknown keys are forbidden. `source` is exactly and unknown versions raise bounded `InvalidSchemaError` requiring re-export. A missing schema member raises bounded `InvalidContainerError` saying the bundle predates the required schema and must be re-exported, through shared member rules. -Thus 0.1.0a1 bundles without schema or with schema v1 are incompatible with 0.1.0a2. -Format version remains `1`, whose pre-release wire contract is not frozen. +Thus 0.1.0a1 bundles without schema or with schema v1 require re-export for 0.2.0. +Format version remains `1`, whose wire contract is not frozen. Arrays preserve order. `fields` has 1–100,000 entries and `reports` 1–1,024. Typed records use strict types, finite numbers and no arbitrary extensibility. @@ -99,12 +99,12 @@ a top-level ordinary label must equal the feature name at that index. | `series` | `columns`: exactly two scalar subfields, with `id` equal to `field1` then `field2`, and no `mappedTo`; no nested series/one-hot groups. Labels describe pair axes. | | `onehot-category` | Special shape below. | -In 0.1.0a3 the `series` shape changed from `field1`/`field2` to MLForm's +In 0.2.0 the `series` shape changed from `field1`/`field2` to MLForm's `columns` while format 1 is not frozen. Schema version stays `"2"`: no bundle written before this change contains a series field, so a version bump would invalidate every 0.1.0a2 bundle for a shape none uses. Readers reject the old shape. -The exporter adapts mlschema's pairs in one small function until mlschema emits -this shape. Parent `label`, `required` and `mappedTo` remain unchanged: +MLschema emits this shape directly. Parent `label`, `required` and `mappedTo` +remain unchanged: ```json {"kind":"series","label":"pair","mappedTo":0,"required":true, diff --git a/docs/format-state.md b/docs/format-state.md index 977ec78..3675dca 100644 --- a/docs/format-state.md +++ b/docs/format-state.md @@ -1,7 +1,7 @@ # Version 1 prediction state and bounded native work -Normative section of [the format contract](format.md). Version 1 is pre-release -and not yet frozen. Bundles lacking the required prediction-state records +Normative section of [the format contract](format.md). Version 1 is not frozen. +Bundles lacking the required prediction-state records fail full claim comparison. ## Sklearn state ownership diff --git a/docs/format.md b/docs/format.md index 7a5d1ad..0bd99bc 100644 --- a/docs/format.md +++ b/docs/format.md @@ -2,10 +2,10 @@ This document defines the wire contract for `format_name = "mlbundle"` and `format_version = "1"`. Format versions are independent of package versions. -"Must" denotes a compatibility or security requirement. This pre-release +"Must" denotes a compatibility or security requirement. This contract is implemented by the manifest, secure container, inspect/export/load, sklearn/XGBoost/CatBoost/ONNX adapters, local schema, samples and prediction-state checks. -Version 1 is not frozen. Releases 0.1.0a2/a3 require schema v2; incompatible manifest/schema shapes require re-export. Valid a2 bundles without series remain compatible. The inspect/verify CLI uses these public APIs. +Version 1 is not frozen. MLbundle 0.2.0 requires schema v2; incompatible manifest/schema shapes require re-export. Valid 0.1.0a2 bundles without series remain compatible. The inspect/verify CLI uses these public APIs. The normative container rules are in [Container and resource limits](format-container.md). The normative structure, adapter and result rules are in [Adapters and structure](format-adapters.md). diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..bfa00ff --- /dev/null +++ b/docs/package.json @@ -0,0 +1,27 @@ +{ + "name": "mlbundle-docs", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vp exec astro dev --host 127.0.0.1 --port 4341", + "start": "vp run dev", + "build": "vp run typecheck && vp exec astro build", + "check": "vp run typecheck", + "preview": "vp exec astro preview --host 127.0.0.1 --port 4341", + "typecheck": "vp exec astro check", + "test": "vp exec node --test src/spec.test.ts" + }, + "dependencies": { + "@astrojs/starlight": "0.42.1", + "astro": "7.3.2", + "sharp": "0.35.5" + }, + "devDependencies": { + "@astrojs/check": "0.9.10", + "typescript": "6.0.3" + }, + "engines": { + "node": "^22.18.0 || ^24.11.0 || >=26.0.0" + } +} diff --git a/docs/publishing.md b/docs/publishing.md index 6a8071a..ef847aa 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -1,61 +1,48 @@ -# Manual pre-alpha publishing +# Publishing to PyPI -Version `0.1.0a3` is the next unpublished pre-alpha; `0.1.0a2` is on PyPI. -Format version 1 is not frozen; incompatible manifest/schema shapes require -re-export. Valid a2 bundles remain compatible, with the series exception documented -in [the schema contract](format-schema.md). The security design has not had an +Version `0.2.0` is prepared by this checkout. Merging a version change into +`main` automatically starts the PyPI workflow; no one needs to start a release +run manually. Format version 1 is not frozen, so incompatible manifest/schema +shapes require re-export. Valid a2 bundles remain compatible, as documented in +[the schema contract](format-schema.md). The security design has not had an independent review; do not accept bundles from untrusted parties. -The owner, or an agent on the owner's explicit request for that specific version, -runs this workflow. Agents never run or approve it on their own initiative. -There is no API token, repository secret, tag, GitHub release or other publish path. - -## Publishing steps - -The owner, or an agent on the owner's explicit request, follows these steps: - -1. Review and merge the change into `develop`, then bring it into `main` through - the normal reviewed integration process. The workflow refuses any other ref. -2. Confirm the registered PyPI pending publisher at - [account publishing](https://pypi.org/manage/account/publishing/) has these - exact values: project `mlbundle`, owner `UlloaSP`, repository `mlbundle`, - workflow `publish.yml`, environment `pypi_env`. Confirm the GitHub `pypi_env` - environment still requires `UlloaSP` approval and permits only `main`. -3. Open [Publish to PyPI](https://github.com/UlloaSP/mlbundle/actions/workflows/publish.yml), - choose **Run workflow**, select **main**, and run it once. -4. Wait for the build/CI jobs to succeed. Review the run's commit and downloaded - `distributions` artifact, containing the `0.1.0a3` wheel and source archive. - Approve the `pypi_env` deployment when satisfied. This approval permits upload. -5. Check [mlbundle on PyPI](https://pypi.org/project/mlbundle/0.1.0a3/) and install - with `python -m pip install "mlbundle[sklearn,schema]==0.1.0a3"` on Python 3.14. - -A pending publisher does not reserve a name until a successful upload. The -[PyPI pending-publisher documentation](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/) -explains this first-upload step. If publishing fails or only one artifact uploads, -inspect the PyPI state before taking further action; the preflight rejects an -existing version and the workflow does not skip existing files. - -## What the workflow checks - -`publish.yml` has two jobs. Its `build` job calls `ci.yml` from the same commit, -with publishing preflight enabled. That preflight checks `refs/heads/main`, reads -the version from `pyproject.toml`, and requires HTTP 404 from the exact PyPI -version endpoint. An existing version, other HTTP errors or a network failure -stop the run. One concurrency group prevents overlapping publish runs. - -Reusable CI builds one wheel and source archive with Python 3.14, uv 0.12.19 and -`uv_build`. Strict Twine checks validate both artifacts' metadata and README -rendering. Both OSes check artifact contents, matching wheel/sdist package bytes, -and an isolated base-wheel install. Ubuntu and Windows install that exact wheel -and run the full test suite; lint and type checks must also succeed. Tests run -from the runner's temporary directory so CatBoost writes stay outside the checkout. - -The artifact is immutable within the run. `publish` waits for all reusable CI -jobs and `pypi_env` approval, downloads `distributions` and uploads those files without -rebuilding or checking out source. Only this job has `id-token: write`; the -workflow otherwise permits only `contents: read`. Trusted Publishing exchanges -OIDC identity for short-lived PyPI credentials through the documented -[PyPA publish action](https://github.com/pypa/gh-action-pypi-publish). - -Publication and OIDC authorization cannot be tested locally. The first authorized -upload is the verification of the registered publisher and approval gate. +## Release flow + +1. Set `project.version` in `pyproject.toml` to a new version in a reviewed + change, then merge it into `main` through the normal integration process. +2. `publish.yml` checks that the push is to `main`, that the version changed + from the previous commit, and that this exact version is not already on + PyPI. Unchanged versions skip publication; an existing version, network + failure or unexpected PyPI response fails the workflow. +3. The workflow runs the full reusable CI: Python packaging, lint, type checks, + tests on Ubuntu and Windows, and all frontend checks, tests and builds. +4. If every check passes, Actions uploads the exact CI-built wheel and source + archive to [mlbundle on PyPI](https://pypi.org/project/mlbundle/). + Install with `python -m pip install "mlbundle[sklearn,schema]==0.2.0"` on + Python 3.14, replacing the example version as needed. + +The GitHub `pypi_env` environment contains the project-scoped +`MLBUNDLE_API_TOKEN` Actions secret and allows deployments from `main` only. +The workflow uses it as the PyPI token; the token value must never appear in +the repository or logs. There is no manual approval gate, tag, GitHub release +or alternate publish workflow. + +## Checks and recovery + +`publish.yml` uses a read-only preflight before calling `ci.yml`. It skips when +the version has not changed and rejects versions already on PyPI. The reusable +CI checks the same version again before building, so a release that appears +during the run blocks publication. One concurrency group prevents overlapping +publish runs. + +The CI artifact is immutable within the run. `publish` waits for all reusable +CI jobs, downloads the `distributions` artifact and uploads those files without +rebuilding or checking out source. The workflow has `contents: read`; only the +publish job receives the environment-scoped token. Website deployment is +skipped here and follows its own automatic path after successful CI on `main`, +described in [website deployment](deployment.md). + +If upload fails after PyPI accepted one artifact, inspect the version on PyPI +before retrying. The preflight refuses any existing version, and the publish +action does not skip duplicate files. diff --git a/docs/src/components/BrandHead.astro b/docs/src/components/BrandHead.astro new file mode 100644 index 0000000..2d4b9f8 --- /dev/null +++ b/docs/src/components/BrandHead.astro @@ -0,0 +1,19 @@ +--- +import { getImage } from "astro:assets"; +import mark from "../../../brand/mlbundle.png"; +import monaLicense from "../fonts/mona-sans-LICENSE.txt?url"; +import monoLicense from "../fonts/jetbrains-mono-LICENSE.txt?url"; + +const favicon = await getImage({ src: mark, width: 64, format: "png" }); +const { head } = Astro.locals.starlightRoute; +const tags = head.filter( + ({ tag, attrs }) => !(tag === "link" && String(attrs?.rel ?? "").split(/\s+/).includes("icon")), +); +--- + +{tags.map(({ tag: Tag, attrs, content }) =>
+ Normative source: {file}. This page renders that file unchanged; the guides explain it, this text defines it.
+
overwrite.
+
+
+ Keep data local
+ What the schema carries, and why samples need sample_rows=N.
+
+
+ Read a bundle
+ inspect for metadata, load for a verified model.
+
+
+ Predict from a form
+ Records keyed by the schema, frozen per-output results.
+
+
+ Check your model
+ scikit-learn, XGBoost, CatBoost, ONNX and composite pipelines.
+
++ A closed adapter inside MLbundle loads each format. Nothing is imported from names written + in the bundle, and there is no pickle fallback. A model MLbundle cannot load safely is + rejected when you export it, on your machine, not after you share the file. +
+ + Supported models + +| Library | +Stored as | +Extra | +
|---|---|---|
| {name} | ++ {member} + {note} + | +{extra} | +
+ MLbundle packs a scikit-learn, XGBoost, CatBoost or ONNX model with its manifest and input + schema. Loading recomputes what the file claims about the model from the model itself. No + pickle anywhere. +
+ +{install}
+
+
+
+ examples/train.py with MLbundle {example.version}.
+
+ A .mlbundle is a ZIP with a closed list of files. Anything else
+ inside it is rejected before the model is read.
+
{text}
+
+ Models with readable internals, such as trees and linear coefficients, also carry that
+ structure for review, checked against the loaded model. Training rows are added only when you
+ ask for them with sample_rows=N.
+
+ Format specification
+
+
+ Export infers the input schema where the data already is, and keeps only the description.
+ Want example rows in the bundle? Pass sample_rows=N: they are real
+ data, so they are never added by default.
+
Export a model you already have, then load it the way your application will.
+{command} +{output}+
+ inspect validates the archive, manifest and hashes without
+ importing a model library. load also deserializes the model,
+ recomputes the claims derived from it, such as hyperparameters, counts, class labels and
+ tree structure, and rejects the file when they differ.
+
max_depth changed from{" "}
+ null to 3 in the manifest.
+ + Hashes catch corrupted files, not deliberate edits, and bundles are not signed. The training + environment, schema and samples are validated but not authenticated. Treat a bundle from + someone else as untrusted input. + + What MLbundle checks + +
+404
+Head back to MLbundle, or find what you need in the documentation.
+ +. */
+.example .code:focus-visible {
+ outline-offset: -3px;
+}
diff --git a/landing/src/styles/typography.css b/landing/src/styles/typography.css
new file mode 100644
index 0000000..1f4547c
--- /dev/null
+++ b/landing/src/styles/typography.css
@@ -0,0 +1,82 @@
+@font-face {
+ font-family: "Mona Sans Variable";
+ font-style: normal;
+ font-display: swap;
+ font-weight: 200 900;
+ font-stretch: 75% 125%;
+ src: url("../fonts/mona-sans-latin-standard-normal.woff2") format("woff2");
+ unicode-range:
+ U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
+ U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: "Mona Sans Variable";
+ font-style: normal;
+ font-display: swap;
+ font-weight: 200 900;
+ font-stretch: 75% 125%;
+ src: url("../fonts/mona-sans-latin-ext-standard-normal.woff2") format("woff2");
+ unicode-range:
+ U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
+ U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
+ U+A720-A7FF;
+}
+
+@font-face {
+ font-family: "JetBrains Mono Variable";
+ font-style: normal;
+ font-display: swap;
+ font-weight: 100 800;
+ src: url("../fonts/jetbrains-mono-latin-wght-normal.woff2") format("woff2");
+ unicode-range:
+ U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
+ U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: "JetBrains Mono Variable";
+ font-style: normal;
+ font-display: swap;
+ font-weight: 100 800;
+ src: url("../fonts/jetbrains-mono-latin-ext-wght-normal.woff2") format("woff2");
+ unicode-range:
+ U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
+ U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
+ U+A720-A7FF;
+}
+
+:root {
+ --brand-font-sans: "Mona Sans Variable", "Mona Sans", system-ui, sans-serif;
+ --brand-font-mono: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, monospace;
+}
+
+.mlbundle-brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.65rem;
+ color: inherit;
+ text-decoration: none;
+}
+
+.mlbundle-brand img {
+ width: 2.25rem;
+ height: 2.5rem;
+ object-fit: contain;
+}
+
+.wordmark {
+ font-family: var(--brand-font-sans);
+ white-space: nowrap;
+}
+
+.wordmark .ml {
+ font-weight: 800;
+ font-stretch: 125%;
+ letter-spacing: -0.01em;
+}
+
+.wordmark .rest {
+ font-weight: 300;
+ font-stretch: 100%;
+}
diff --git a/landing/tsconfig.json b/landing/tsconfig.json
new file mode 100644
index 0000000..8bf91d3
--- /dev/null
+++ b/landing/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/landing/wrangler.jsonc b/landing/wrangler.jsonc
new file mode 100644
index 0000000..c7f24cc
--- /dev/null
+++ b/landing/wrangler.jsonc
@@ -0,0 +1,12 @@
+{
+ "name": "mlbundle-landing",
+ "compatibility_date": "2026-10-19",
+ "workers_dev": false,
+ "preview_urls": false,
+ "assets": {
+ "directory": "./dist",
+ "html_handling": "force-trailing-slash",
+ "not_found_handling": "404-page",
+ },
+ "routes": [{ "pattern": "mlbundle.eshkra.dev", "custom_domain": true }],
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9b85236
--- /dev/null
+++ b/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "mlbundle-sites",
+ "private": true,
+ "type": "module",
+ "packageManager": "pnpm@12.14.0",
+ "engines": { "node": "^22.18.0 || ^24.11.0 || >=26.0.0" },
+ "scripts": {
+ "build": "vp run --filter './landing' --filter './docs' --filter './playground' build",
+ "typecheck": "vp run --filter './landing' --filter './docs' --filter './playground' typecheck",
+ "test": "vp run mlbundle-docs#test && vp run mlbundle-playground#test:unit",
+ "lint": "vp lint landing/src docs/src playground/src playground/tests --deny-warnings",
+ "check": "vp run lint && vp run mlbundle-landing#check:format && vp run typecheck && vp run test",
+ "check:deploy": "vp exec wrangler deploy --dry-run --config landing/wrangler.jsonc && vp exec wrangler deploy --dry-run --config docs/wrangler.jsonc && vp exec wrangler deploy --dry-run --config playground/wrangler.jsonc",
+ "dev:landing": "vp run mlbundle-landing#dev",
+ "dev:docs": "vp run mlbundle-docs#dev",
+ "dev:playground": "vp run mlbundle-playground#dev"
+ },
+ "devDependencies": {
+ "vite-plus": "1.3.0",
+ "wrangler": "4.151.1"
+ }
+}
diff --git a/playground/README.md b/playground/README.md
new file mode 100644
index 0000000..66acc0f
--- /dev/null
+++ b/playground/README.md
@@ -0,0 +1,80 @@
+# MLbundle playground
+
+Write Python on the left, export a real `.mlbundle`, and inspect it on the right: manifest, schema, structure, members and samples. Part of [eshkra](https://eshkra.dev).
+
+Intended host: . Documentation: .
+
+## How it runs
+
+Python runs in your browser on [Pyodide](https://pyodide.org/), inside a dedicated Web Worker. There is no server that executes code, and neither your code nor your files are uploaded anywhere.
+
+- The build bundles this repository's `src/mlbundle` (every `.py` file plus the `state_fields.json` package data) into the worker, with distribution metadata whose version is read from `pyproject.toml`. The page always runs the mlbundle source it was built from, at version 0.2.0 today.
+- Each run executes your code as written, in a fresh namespace and an empty working directory. Every `*.mlbundle` the code writes into that directory (at most 8 per run) is passed to `mlbundle.inspect`, then to `mlbundle.load`, which deserializes it through the closed sklearn adapter and recomputes model claims and structure. Only then is it marked **Verified**. Loading is done automatically only for bundles your own code just wrote.
+- **Download .mlbundle** saves the exact bytes mlbundle wrote.
+- **Inspect a .mlbundle file** (the upload icon) reads a local file in the browser and runs `mlbundle.inspect` on it. It never calls `load` and never deserializes the model, so uploaded files show **Inspected only** and their structure is withheld: structure is only trustworthy once `load` has recomputed it. Files that mlbundle rejects show **Rejected** with mlbundle's error.
+- Inspection and verification use `mlbundle.ArchiveLimits` lowered for a browser tab: 64 MiB archive, compressed and per-member bytes, 128 MiB total uncompressed bytes; the other ceilings keep mlbundle's defaults. Files above 64 MiB are refused before they are read into memory.
+- A run that exceeds 60 seconds, or that you stop, terminates the worker; a fresh Python starts. **Restart Python** does the same on request. `print()` output and stderr appear under **Console**, capped at 100,000 characters per run, together with the last expression and any traceback.
+- Tables show at most 200 rows and tree outlines at most 511 nodes; each says what it omits. Raw `schema.json`/`structure.json` views are capped at 200,000 characters.
+- Bundle contents are rendered as text only, never as HTML.
+
+The playground is a local, in-browser tool for trying mlbundle with your own trusted code. It is not a sandbox for untrusted bundles and gives no guarantee beyond what `inspect` and `load` themselves provide; see [SECURITY.md](../SECURITY.md).
+
+## Interface
+
+The page follows the MLschema and MLForm playgrounds: the same header, floating options menu (design system, style, appearance, viewport size), split editor and icon controls with tooltips and accessible names. Narrow windows start with the code above the inspector; **Code position** offers every layout, and the separator resizes with the pointer or the arrow keys.
+
+The inspector has one tab per view. Arrow keys, Home and End move between tabs.
+
+- **Overview**: verification status, model facts, features with training dtypes, class labels, training environment and author metadata.
+- **Manifest**: the canonical manifest JSON.
+- **Schema**: the fields and reports mlschema inferred locally, plus raw `schema.json`.
+- **Structure**: decision trees as expandable outlines read from sklearn's native arrays, a selectable tree for ensembles, coefficient and importance bars, plus raw `structure.json`.
+- **Members**: archive members with the sizes and SHA-256 hashes the manifest records.
+- **Samples**: rows included only through an explicit `sample_rows=N`.
+- **Console**: errors, the last expression and printed output.
+
+Ctrl/⌘ + Enter runs the code. Tab indents; press Escape, then Tab, to leave the editor. The example, your code and the layout are saved in this browser; **Reset example** restores the example.
+
+## Runtime
+
+Only the scikit-learn adapter runs here. XGBoost (Pyodide ships 2.1.4, below mlbundle's 3.4.1 floor), CatBoost and ONNX Runtime have no builds for this runtime, so those adapters are not offered.
+
+| Component | Version | Source |
+| --- | --- | --- |
+| Pyodide | 314.0.7 (Python 3.14.2) | jsDelivr CDN |
+| scikit-learn, scipy, numpy, pandas | 1.8.0, 1.18.0, 2.4.6, 3.0.2 | Pyodide distribution |
+| pydantic, pydantic-core | 2.14.1, 2.50.1 | PyPI |
+| skops, mlschema, prettytable, wcwidth, packaging | 0.16.0, 0.3.0, 3.18.0, 0.9.2, 26.3 | PyPI |
+| typing-extensions, typing-inspection, annotated-types | 4.16.0, 0.4.4, 0.8.0 | PyPI |
+
+PyPI pins match `uv.lock` (a unit test checks them). scikit-learn 1.8.0 satisfies mlbundle's `>=1.6`, but mlbundle's sklearn catalog is audited against 1.9.1, which has no build for this runtime. pandas 3.0.2 is likewise the newest Pyodide build. Bundles written here record the versions that actually ran, with `Emscripten`/`wasm32` as the platform; loading them with a different scikit-learn version may produce mlbundle's version diagnostics.
+
+Pyodide's `os` module has no `link`. The harness defines one that raises `ENOSYS`, so mlbundle takes its documented publication path for file systems without hard links.
+
+The first visit downloads Python and the wheels from `cdn.jsdelivr.net`, `pypi.org` and `files.pythonhosted.org`; later visits use the browser cache. If those hosts are unreachable, the page reports that Python could not start and offers **Restart Python**.
+
+To move to a new Pyodide release, update `pyodide` in `package.json` and `PYODIDE_VERSION` in `src/runtime/versions.ts` together (a unit test checks they match), then review `PYPI_PINS`.
+
+## Examples
+
+`src/examples/*.py` are ordinary Python files: a decision tree classifier with a training-frame schema, a random forest regressor, a logistic regression with one-hot columns and explicit samples, an all-sklearn Pipeline (verified, with no structure in format version 1) and rejected models. `test:runtime` runs every example against the real package.
+
+## Commands
+
+From this directory, after `vp install --frozen-lockfile` at the repository root:
+
+```bash
+vp run dev # http://127.0.0.1:10021
+vp run build
+vp run preview
+vp run typecheck
+vp run test:unit # runtime client, upload requests, pins, version and saved state
+vp run test:runtime # real Pyodide in Node: examples, verification, uploads, rejections
+vp run test:e2e # production build on port 10022, desktop and mobile Chromium
+vp run test:e2e:built # same browser tests against an already built dist/
+```
+
+`test:runtime` and `test:e2e` download Pyodide and wheels, so they need network access. Before the first `test:e2e`, run `vp exec playwright install chromium`. The Python examples and harness are linted with the repository's Ruff configuration.
+
+Actions builds once, runs `test:e2e:built`, then uploads that tested build for
+Cloudflare deployment. See [website deployment](../docs/deployment.md).
diff --git a/playground/index.html b/playground/index.html
new file mode 100644
index 0000000..ea34316
--- /dev/null
+++ b/playground/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+ MLbundle Playground
+
+
+
+
+
+
+
diff --git a/playground/package.json b/playground/package.json
new file mode 100644
index 0000000..4aa9ea0
--- /dev/null
+++ b/playground/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "mlbundle-playground",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vp dev --host 127.0.0.1 --port 10021 --strictPort",
+ "build": "vp build",
+ "preview": "vp preview --host 127.0.0.1 --port 10021 --strictPort",
+ "typecheck": "vp exec tsc -p tsconfig.json --noEmit",
+ "test:unit": "vp exec node --test tests/unit/*.test.ts",
+ "test:runtime": "vp exec node --test tests/runtime/*.test.ts",
+ "test:e2e": "vp run build && vp run test:e2e:built",
+ "test:e2e:built": "vp exec playwright test"
+ },
+ "dependencies": {
+ "@codemirror/commands": "6.11.1",
+ "@codemirror/lang-json": "6.0.2",
+ "@codemirror/lang-python": "6.2.1",
+ "@codemirror/language": "6.13.1",
+ "@codemirror/state": "6.7.6",
+ "@codemirror/view": "6.43.14",
+ "@fontsource-variable/jetbrains-mono": "5.3.0",
+ "@fontsource-variable/mona-sans": "5.3.0",
+ "@lezer/highlight": "1.2.5"
+ },
+ "devDependencies": {
+ "@playwright/test": "1.64.1",
+ "@types/node": "26.6.5",
+ "pyodide": "314.0.7",
+ "typescript": "6.0.3",
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.3.0"
+ },
+ "engines": {
+ "node": "^22.18.0 || ^24.11.0 || >=26.0.0"
+ }
+}
diff --git a/playground/playwright.config.ts b/playground/playwright.config.ts
new file mode 100644
index 0000000..95e2cd4
--- /dev/null
+++ b/playground/playwright.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig, devices } from "@playwright/test";
+
+// Tests the production build on its own port, so a running dev server on 10021 is untouched.
+export default defineConfig({
+ testDir: "./tests/e2e",
+ // First boot downloads Python, scikit-learn and pandas from jsDelivr and PyPI.
+ timeout: 240_000,
+ expect: { timeout: 120_000 },
+ fullyParallel: false,
+ workers: 1,
+ use: { baseURL: "http://127.0.0.1:10022", trace: "retain-on-failure" },
+ webServer: {
+ command: "vp preview --host 127.0.0.1 --port 10022 --strictPort",
+ url: "http://127.0.0.1:10022",
+ reuseExistingServer: false,
+ timeout: 120_000,
+ },
+ projects: [
+ { name: "chromium", use: { ...devices["Desktop Chrome"] } },
+ { name: "mobile-chromium", use: { ...devices["Pixel 7"] } },
+ ],
+});
diff --git a/playground/public/_headers b/playground/public/_headers
new file mode 100644
index 0000000..8d11b46
--- /dev/null
+++ b/playground/public/_headers
@@ -0,0 +1,7 @@
+/*
+ X-Content-Type-Options: nosniff
+ Referrer-Policy: strict-origin-when-cross-origin
+ Permissions-Policy: camera=(), microphone=(), geolocation=()
+
+/assets/*
+ Cache-Control: public, max-age=31536000, immutable
diff --git a/playground/src/assets/eshkra-mark.png b/playground/src/assets/eshkra-mark.png
new file mode 100644
index 0000000..afb8fa6
Binary files /dev/null and b/playground/src/assets/eshkra-mark.png differ
diff --git a/playground/src/env.d.ts b/playground/src/env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/playground/src/env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/playground/src/examples/classifier.py b/playground/src/examples/classifier.py
new file mode 100644
index 0000000..db5a48b
--- /dev/null
+++ b/playground/src/examples/classifier.py
@@ -0,0 +1,27 @@
+# A decision tree classifier exported with a schema inferred from its training frame.
+import pandas as pd
+from sklearn.tree import DecisionTreeClassifier
+
+from mlbundle import export
+
+training = pd.DataFrame(
+ {
+ "temperature": [2.0, 31.0, 5.5, 28.0, 12.0, 25.5, 1.0, 34.0],
+ "humidity": [0.81, 0.32, 0.77, 0.41, 0.66, 0.35, 0.90, 0.28],
+ "weather": ["cold", "warm", "cold", "warm", "cold", "warm", "cold", "warm"],
+ }
+)
+features = training.drop(columns="weather")
+model = DecisionTreeClassifier(max_depth=3, random_state=7)
+model.fit(features, training["weather"])
+
+# Writes classifier.mlbundle into this run's working directory. The playground then
+# inspects it and verifies it with mlbundle.load before showing it on the right.
+view = export(
+ model,
+ "classifier.mlbundle",
+ training_data=training,
+ target_names=["weather"],
+ metadata={"purpose": "playground example"},
+)
+view.manifest.target.class_labels
diff --git a/playground/src/examples/errors.py b/playground/src/examples/errors.py
new file mode 100644
index 0000000..93d9fde
--- /dev/null
+++ b/playground/src/examples/errors.py
@@ -0,0 +1,36 @@
+# mlbundle rejects unsupported models on the training machine, before writing anything.
+from sklearn.ensemble import HistGradientBoostingClassifier
+from sklearn.tree import DecisionTreeClassifier
+
+from mlbundle import MLBundleError, export
+
+rows = [[0.0, 1.0], [1.0, 0.0], [0.1, 0.9], [0.9, 0.1]]
+labels = ["cold", "warm", "cold", "warm"]
+
+
+class CustomTree(DecisionTreeClassifier):
+ """A user subclass: not on mlbundle's closed trust list."""
+
+
+attempts = {
+ "custom subclass": lambda: export(
+ CustomTree().fit(rows, labels), "custom.mlbundle"
+ ),
+ "HistGradientBoosting": lambda: export(
+ HistGradientBoostingClassifier(max_iter=3).fit(rows * 5, labels * 5),
+ "hist.mlbundle",
+ ),
+ "samples without a frame": lambda: export(
+ DecisionTreeClassifier().fit(rows, labels), "sampled.mlbundle", sample_rows=2
+ ),
+}
+results = []
+for name, attempt in attempts.items():
+ try:
+ attempt()
+ results.append({"case": name, "error": None})
+ except MLBundleError as error:
+ results.append(
+ {"case": name, "error": type(error).__name__, "message": str(error)}
+ )
+results
diff --git a/playground/src/examples/pipeline.py b/playground/src/examples/pipeline.py
new file mode 100644
index 0000000..ca31cea
--- /dev/null
+++ b/playground/src/examples/pipeline.py
@@ -0,0 +1,15 @@
+# An all-scikit-learn Pipeline. Its claims cover every step; version 1 records no
+# structure for it, because the final step only sees transformed features.
+from sklearn.pipeline import make_pipeline
+from sklearn.preprocessing import StandardScaler
+from sklearn.svm import SVR
+
+from mlbundle import export
+
+rows = [[0.0, 10.0], [1.0, 12.0], [2.0, 9.0], [3.0, 15.0], [4.0, 11.0]]
+target = [1.2, 2.1, 2.9, 4.2, 5.1]
+model = make_pipeline(StandardScaler(), SVR(C=3.0)).fit(rows, target)
+
+# Without a frame, names are declared and the schema comes from one default row.
+view = export(model, "pipeline.mlbundle", feature_names=["dose_mg", "weight_kg"])
+{"source": view.schema["source"], "dtypes": view.manifest.features.dtypes}
diff --git a/playground/src/examples/regressor.py b/playground/src/examples/regressor.py
new file mode 100644
index 0000000..2307acf
--- /dev/null
+++ b/playground/src/examples/regressor.py
@@ -0,0 +1,24 @@
+# A small random forest regressor. Its structure lists every fitted tree.
+import numpy as np
+import pandas as pd
+from sklearn.ensemble import RandomForestRegressor
+
+from mlbundle import export
+
+rng = np.random.default_rng(3)
+frame = pd.DataFrame(
+ {
+ "area_m2": rng.uniform(30, 160, 40).round(1),
+ "rooms": rng.integers(1, 6, 40),
+ "floor": rng.integers(0, 12, 40),
+ }
+)
+frame["price_k"] = (
+ frame["area_m2"] * 2.4 + frame["rooms"] * 9 + rng.normal(0, 8, 40)
+).round(1)
+
+model = RandomForestRegressor(n_estimators=3, max_depth=3, random_state=0)
+model.fit(frame.drop(columns="price_k"), frame["price_k"])
+export(model, "regressor.mlbundle", training_data=frame, target_names=["price_k"])
+
+model.feature_importances_.round(3).tolist()
diff --git a/playground/src/examples/ruff.toml b/playground/src/examples/ruff.toml
new file mode 100644
index 0000000..f543fe9
--- /dev/null
+++ b/playground/src/examples/ruff.toml
@@ -0,0 +1,9 @@
+# Playground examples are interactive scripts, not library code.
+extend = "../../../pyproject.toml"
+
+[lint]
+extend-ignore = [
+ "B018", # The final bare expression is the result the playground displays.
+ "T201", # print() output is shown in the playground console.
+ "ERA001", # Commented code is a suggested edit for the visitor to try.
+]
diff --git a/playground/src/examples/samples.py b/playground/src/examples/samples.py
new file mode 100644
index 0000000..5a6b01b
--- /dev/null
+++ b/playground/src/examples/samples.py
@@ -0,0 +1,20 @@
+# plan__basic and plan__premium become one onehot-category field.
+# Samples are real rows: they are included only because sample_rows is set.
+import pandas as pd
+from sklearn.linear_model import LogisticRegression
+
+from mlbundle import export
+
+features = pd.DataFrame(
+ {
+ "age": [34.0, 51.0, 27.0, 45.0, 62.0, 38.0],
+ "plan__basic": [1, 0, 1, 0, 0, 1],
+ "plan__premium": [0, 1, 0, 1, 1, 0],
+ "active": [True, False, True, True, False, True],
+ }
+)
+churned = ["no", "yes", "no", "no", "yes", "no"]
+model = LogisticRegression(max_iter=500).fit(features, churned)
+
+view = export(model, "samples.mlbundle", training_data=features, sample_rows=3)
+[field["kind"] for field in view.schema["fields"]]
diff --git a/playground/src/icons.ts b/playground/src/icons.ts
new file mode 100644
index 0000000..b7e079c
--- /dev/null
+++ b/playground/src/icons.ts
@@ -0,0 +1,27 @@
+const paths = {
+ menu: ' ',
+ run: ' ',
+ stop: ' ',
+ reset: ' ',
+ restart: ' ',
+ copy: ' ',
+ check: ' ',
+ error: ' ',
+ loading: ' ',
+ ready: ' ',
+ download: ' ',
+ upload: ' ',
+ docs: ' ',
+ github: ' ',
+} as const;
+
+export type Icon = keyof typeof paths;
+export const icon = (name: Icon) =>
+ ``;
+
+/** Icon-only control: the label is its accessible name and its hover/focus tooltip. */
+export const labelIcon = (element: HTMLElement, name: Icon, label: string, tooltip = label) => {
+ element.innerHTML = icon(name);
+ element.setAttribute("aria-label", label);
+ element.dataset.tooltip = tooltip;
+};
diff --git a/playground/src/inspector/dom.ts b/playground/src/inspector/dom.ts
new file mode 100644
index 0000000..326363e
--- /dev/null
+++ b/playground/src/inspector/dom.ts
@@ -0,0 +1,87 @@
+type Child = Node | string | number | null | undefined | false;
+
+/**
+ * Builds elements from text only. Bundle contents are untrusted (uploads included),
+ * so nothing from a bundle is ever parsed as HTML.
+ */
+export const h = (
+ tag: K,
+ attributes: Record = {},
+ ...children: Child[]
+): HTMLElementTagNameMap[K] => {
+ const element = document.createElement(tag);
+ for (const [name, value] of Object.entries(attributes)) element.setAttribute(name, value);
+ for (const child of children) {
+ if (child === null || child === undefined || child === false) continue;
+ element.append(typeof child === "object" ? child : String(child));
+ }
+ return element;
+};
+
+/** Short display for any JSON value, including mlbundle's {"$float": "nan"} tags. */
+export const display = (value: unknown): string => {
+ if (value === null || value === undefined) return "—";
+ if (typeof value === "number") return Number.isInteger(value) ? String(value) : String(+value.toPrecision(6));
+ if (typeof value === "string") return value;
+ if (typeof value === "object" && !Array.isArray(value) && "$float" in value) {
+ return String((value as { $float: unknown }).$float);
+ }
+ const text = JSON.stringify(value);
+ return text.length > 160 ? `${text.slice(0, 157)}…` : text;
+};
+
+export const formatBytes = (bytes: number) => {
+ if (bytes < 1024) return `${bytes} B`;
+ const units = ["KiB", "MiB", "GiB"];
+ let value = bytes / 1024;
+ let unit = 0;
+ while (value >= 1024 && unit < units.length - 1) {
+ value /= 1024;
+ unit += 1;
+ }
+ return `${value.toFixed(value < 10 ? 1 : 0)} ${units[unit]}`;
+};
+
+/** An accessible data table. Rows are capped; the caption reports what is hidden. */
+export const table = (
+ caption: string,
+ headers: readonly string[],
+ rows: readonly (readonly unknown[])[],
+ limit = 200,
+) => {
+ const shown = rows.slice(0, limit);
+ const note = rows.length > limit ? ` (first ${limit} of ${rows.length})` : "";
+ return h(
+ "div",
+ { class: "inspector-table", tabindex: "0", role: "region", "aria-label": caption },
+ h(
+ "table",
+ {},
+ h("caption", {}, `${caption}${note}`),
+ h("thead", {}, h("tr", {}, ...headers.map((name) => h("th", { scope: "col" }, name)))),
+ h(
+ "tbody",
+ {},
+ ...shown.map((row) => h("tr", {}, ...row.map((cell) => h("td", {}, display(cell))))),
+ ),
+ ),
+ );
+};
+
+/** A term/description list for key facts. */
+export const facts = (label: string, entries: readonly (readonly [string, unknown])[]) =>
+ h(
+ "dl",
+ { class: "inspector-facts", "aria-label": label },
+ ...entries.flatMap(([term, value]) => [h("dt", {}, term), h("dd", {}, display(value))]),
+ );
+
+export const section = (title: string, ...children: Child[]) =>
+ h("section", { class: "inspector-section" }, h("h3", {}, title), ...children);
+
+export const note = (text: string) => h("p", { class: "inspector-note" }, text);
+
+export type JsonObject = { [key: string]: unknown };
+export const asObject = (value: unknown): JsonObject =>
+ value && typeof value === "object" && !Array.isArray(value) ? (value as JsonObject) : {};
+export const asArray = (value: unknown): unknown[] => (Array.isArray(value) ? value : []);
diff --git a/playground/src/inspector/overview.ts b/playground/src/inspector/overview.ts
new file mode 100644
index 0000000..d2015ac
--- /dev/null
+++ b/playground/src/inspector/overview.ts
@@ -0,0 +1,83 @@
+import type { BundleReport } from "../runtime/protocol.ts";
+import { asArray, asObject, display, facts, formatBytes, h, note, section, table } from "./dom.ts";
+
+const STATUS: Record = {
+ verified: {
+ title: "Verified with mlbundle.load",
+ detail:
+ "Inspected, then loaded through the closed sklearn adapter: model claims and structure were recomputed and matched.",
+ },
+ inspected: {
+ title: "Inspected only",
+ detail:
+ "mlbundle.inspect validated the archive, limits, manifest, hashes, schema and samples. The model was not deserialized, so model claims and structure are not verified.",
+ },
+ unverified: {
+ title: "Rejected by mlbundle.load",
+ detail: "The metadata passed inspection, but loading and recomputing the model claims failed.",
+ },
+ rejected: {
+ title: "Rejected by mlbundle.inspect",
+ detail: "mlbundle refused this file before reading its metadata.",
+ },
+};
+
+export const statusBanner = (bundle: BundleReport) => {
+ const { title, detail } = STATUS[bundle.status];
+ return h(
+ "div",
+ { class: "inspector-status", "data-status": bundle.status, role: "note" },
+ h("strong", {}, title),
+ h("span", {}, detail),
+ bundle.error ? h("code", { class: "inspector-error" }, bundle.error) : null,
+ );
+};
+
+export const overview = (bundle: BundleReport) => {
+ const manifest = asObject(bundle.manifest);
+ if (!bundle.manifest) {
+ return [facts("File", [["File", bundle.fileName], ["Size", formatBytes(bundle.sizeBytes)]])];
+ }
+ const model = asObject(manifest.model);
+ const features = asObject(manifest.features);
+ const target = asObject(manifest.target);
+ const environment = asObject(manifest.environment);
+ const names = asArray(features.names);
+ const dtypes = asArray(features.dtypes);
+ const labels = asArray(target.class_labels);
+ const targetNames = asArray(target.names);
+ const metadata = asObject(manifest.user_metadata);
+ return [
+ facts("Model", [
+ ["Estimator", model.estimator_class],
+ ["Kind", model.kind],
+ ["Library", model.library],
+ ["Serialization", model.serialization],
+ ["Features", model.n_features],
+ ["Outputs", model.n_outputs],
+ ["File", `${bundle.fileName} · ${formatBytes(bundle.sizeBytes)}`],
+ ["Format", `${display(manifest.format_name)} ${display(manifest.format_version)}`],
+ ["Writer", manifest.writer_version],
+ ["Created", manifest.created_at],
+ ]),
+ section(
+ "Features",
+ note(`Origin: ${display(features.origin)}. Training dtypes are declarations; null means none were recorded.`),
+ table("Features in model order", ["#", "Name", "Training dtype"], names.map((name, index) => [index, name, dtypes[index] ?? null])),
+ ),
+ section(
+ "Targets",
+ labels.length
+ ? table("Class labels per output", ["Output", "Name", "Labels"], labels.map((output, index) => [index, targetNames[index] ?? null, asArray(output).map(display).join(", ")]))
+ : note(targetNames.length ? `Regression targets: ${targetNames.map(display).join(", ")}` : "Regression output; no target names were declared."),
+ ),
+ section(
+ "Training environment",
+ note(`Python ${display(environment.python_version)} on ${display(environment.os)} ${display(environment.os_version)} (${display(environment.architecture)}). Declared by the writer, not authenticated.`),
+ table("Package versions", ["Package", "Version"], Object.entries(asObject(environment.packages))),
+ ),
+ Object.keys(metadata).length
+ ? section("User metadata", table("Author metadata", ["Key", "Value"], Object.entries(metadata)))
+ : null,
+ ];
+};
diff --git a/playground/src/inspector/panel.ts b/playground/src/inspector/panel.ts
new file mode 100644
index 0000000..42f2b7f
--- /dev/null
+++ b/playground/src/inspector/panel.ts
@@ -0,0 +1,151 @@
+import { labelIcon } from "../icons.ts";
+import type { BundleReport } from "../runtime/protocol.ts";
+import { createJsonViewer } from "../workspace/viewer.ts";
+import { formatBytes, h, note } from "./dom.ts";
+import { overview, statusBanner } from "./overview.ts";
+import { membersView, samplesView, schemaView } from "./schema.ts";
+import { structureView } from "./structure.ts";
+
+export const TABS = [
+ ["overview", "Overview"],
+ ["manifest", "Manifest"],
+ ["schema", "Schema"],
+ ["structure", "Structure"],
+ ["members", "Members"],
+ ["samples", "Samples"],
+ ["console", "Console"],
+] as const;
+export type TabId = (typeof TABS)[number][0];
+
+const RAW_LIMIT = 200_000;
+const STATUS_LABEL: Record = {
+ verified: "Verified",
+ inspected: "Inspected only",
+ unverified: "Load failed",
+ rejected: "Rejected",
+};
+
+const rawJson = (title: string, value: unknown) => {
+ const text = JSON.stringify(value, null, 2);
+ const clipped = text.length > RAW_LIMIT ? `${text.slice(0, RAW_LIMIT)}\n… [${text.length - RAW_LIMIT} more characters]` : text;
+ return h("details", { class: "inspector-raw" }, h("summary", {}, title), h("pre", { tabindex: "0" }, clipped));
+};
+
+/**
+ * Bundle inspector: a bundle picker, a download action and one tab per view.
+ * Tabs follow the ARIA tabs pattern: arrow keys, Home and End move between them.
+ */
+export const createInspector = (host: HTMLElement, consoleView: HTMLElement) => {
+ const bar = h("div", { class: "inspector-bar" });
+ const picker = h("select", { "aria-label": "Bundle" });
+ const title = h("span", { class: "inspector-title" });
+ const chip = h("span", { class: "inspector-chip" });
+ const download = h("button", { type: "button", class: "icon-button" });
+ labelIcon(download, "download", "Download .mlbundle");
+ bar.append(h("label", { class: "code-select inspector-picker" }, picker), title, chip, download);
+ const tablist = h("div", { class: "inspector-tabs", role: "tablist", "aria-label": "Bundle views" });
+ const panel = h("div", { class: "inspector-panel", role: "tabpanel", tabindex: "0", id: "inspector-panel" });
+ const manifestHost = h("div", { class: "output-json" });
+ const manifestViewer = createJsonViewer(manifestHost, "Manifest JSON");
+ host.append(bar, tablist, panel);
+
+ let bundles: BundleReport[] = [];
+ let selected = 0;
+ let active: TabId = "overview";
+ let empty = "Run the code to export a bundle, or open a .mlbundle file to inspect it.";
+ const tabs = TABS.map(([id, label]) => {
+ const tab = h("button", { type: "button", role: "tab", id: `tab-${id}`, "aria-controls": "inspector-panel" }, label);
+ tab.addEventListener("click", () => show(id));
+ return tab;
+ });
+ tablist.append(...tabs);
+ tablist.addEventListener("keydown", (event) => {
+ const index = TABS.findIndex(([id]) => id === active);
+ const next = { ArrowRight: index + 1, ArrowLeft: index - 1, Home: 0, End: TABS.length - 1 }[event.key];
+ if (next === undefined) return;
+ event.preventDefault();
+ const target = (next + TABS.length) % TABS.length;
+ show(TABS[target]![0]);
+ tabs[target]!.focus();
+ });
+
+ const content = (bundle: BundleReport | undefined): (Node | null)[] => {
+ if (active === "console") return [consoleView];
+ if (!bundle) return [note(empty)];
+ if (active === "manifest") {
+ if (!bundle.manifest) return [statusBanner(bundle)];
+ manifestViewer.show(JSON.stringify(bundle.manifest, null, 2));
+ return [manifestHost];
+ }
+ if (!bundle.manifest) return [statusBanner(bundle)];
+ switch (active) {
+ case "overview":
+ return [statusBanner(bundle), ...overview(bundle)];
+ case "schema":
+ return [...schemaView(bundle), bundle.schema ? rawJson("schema.json", bundle.schema) : null];
+ case "structure":
+ return [...structureView(bundle), bundle.structure ? rawJson("structure.json", bundle.structure) : null];
+ case "members":
+ return membersView(bundle);
+ default:
+ return samplesView(bundle);
+ }
+ };
+
+ const render = () => {
+ const bundle = bundles[selected];
+ for (const [index, tab] of tabs.entries()) {
+ const current = TABS[index]![0] === active;
+ tab.setAttribute("aria-selected", String(current));
+ tab.tabIndex = current ? 0 : -1;
+ }
+ panel.setAttribute("aria-labelledby", `tab-${active}`);
+ panel.classList.toggle("is-manifest", active === "manifest" && Boolean(bundle?.manifest));
+ panel.replaceChildren(...content(bundle).filter((item): item is Node => item !== null));
+ panel.scrollTop = 0;
+ bar.hidden = !bundle;
+ if (!bundle) return;
+ picker.parentElement!.hidden = bundles.length < 2;
+ title.textContent = `${bundle.fileName} · ${formatBytes(bundle.sizeBytes)}`;
+ chip.textContent = STATUS_LABEL[bundle.status];
+ chip.dataset.status = bundle.status;
+ download.hidden = !bundle.bytes;
+ };
+ const show = (id: TabId) => {
+ active = id;
+ render();
+ };
+
+ download.addEventListener("click", () => {
+ const bundle = bundles[selected];
+ if (!bundle?.bytes) return;
+ const url = URL.createObjectURL(new Blob([bundle.bytes as BlobPart], { type: "application/zip" }));
+ const link = h("a", { href: url, download: bundle.fileName });
+ document.body.append(link);
+ link.click();
+ link.remove();
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
+ });
+ picker.addEventListener("change", () => {
+ selected = Number(picker.value);
+ render();
+ });
+
+ render();
+ return {
+ get active() {
+ return active;
+ },
+ show,
+ /** Replace the shown bundles; `message` explains an empty list. */
+ setBundles(next: BundleReport[], message: string) {
+ bundles = next;
+ selected = 0;
+ empty = message;
+ picker.replaceChildren(...next.map((bundle, index) => h("option", { value: String(index) }, bundle.fileName)));
+ render();
+ },
+ };
+};
+
+export type Inspector = ReturnType;
diff --git a/playground/src/inspector/schema.ts b/playground/src/inspector/schema.ts
new file mode 100644
index 0000000..5ccd90e
--- /dev/null
+++ b/playground/src/inspector/schema.ts
@@ -0,0 +1,48 @@
+import type { BundleReport } from "../runtime/protocol.ts";
+import { asArray, asObject, display, h, note, section, table, type JsonObject } from "./dom.ts";
+
+const detail = (field: JsonObject) => {
+ const options = asArray(field.options).map(asObject);
+ if (field.kind === "onehot-category") return options.map((option) => `${display(option.label)} → ${display(option.mappedTo)}`).join(", ");
+ const parts: string[] = [];
+ for (const key of ["step", "min", "max", "options", "categories", "dtype"]) {
+ if (key in field) parts.push(`${key}: ${display(field[key])}`);
+ }
+ const columns = asArray(field.columns).map(asObject);
+ if (columns.length) parts.push(`columns: ${columns.map((column) => `${display(column.id)} (${display(column.kind)})`).join(", ")}`);
+ return parts.join(" · ");
+};
+
+export const schemaView = (bundle: BundleReport) => {
+ if (!bundle.schema) return [note("This bundle has no readable schema.")];
+ const schema = bundle.schema;
+ const fields = asArray(schema.fields).map(asObject);
+ const reports = asArray(schema.reports).map(asObject);
+ return [
+ note(`Schema version ${display(schema.schema_version)}, inferred locally by mlschema from ${schema.source === "default" ? "one default row of 1.0 values" : "the training frame"}. One-hot separator: "${display(schema.onehot_separator)}".`),
+ section(
+ "Fields",
+ table("Input fields", ["Label", "Kind", "Mapped to", "Required", "Details"], fields.map((field) => [field.label, field.kind, field.mappedTo ?? "options", field.required, detail(field)])),
+ ),
+ section(
+ "Reports",
+ table("Outputs", ["Label", "Kind", "Mapped to", "Labels"], reports.map((report) => [report.label, report.kind, report.mappedTo, asArray(report.labels).map(display).join(", ") || null])),
+ ),
+ ];
+};
+
+export const membersView = (bundle: BundleReport) => [
+ note("Members in archive order. The manifest records the SHA-256 and size of every other member; mlbundle checked them. Hashes detect corruption, not deliberate tampering."),
+ table("Archive members", ["Member", "Size (bytes)", "SHA-256"], (bundle.members ?? []).map((member) => [member.name, member.size ?? "manifest", member.sha256 ?? "—"])),
+];
+
+export const samplesView = (bundle: BundleReport) => {
+ if (!bundle.samples) {
+ return [note("No samples. mlbundle includes sample rows only when export is called with sample_rows=N, because they are real training data.")];
+ }
+ const names = asArray(asObject(bundle.manifest?.features).names).map(String);
+ return [
+ h("p", { class: "inspector-note" }, `${bundle.samples.length} explicitly requested feature rows, without target or index.`),
+ table("Sample rows", ["#", ...names], bundle.samples.map((row, index) => [index, ...names.map((name) => row[name])])),
+ ];
+};
diff --git a/playground/src/inspector/structure.ts b/playground/src/inspector/structure.ts
new file mode 100644
index 0000000..f0ed106
--- /dev/null
+++ b/playground/src/inspector/structure.ts
@@ -0,0 +1,108 @@
+import type { BundleReport } from "../runtime/protocol.ts";
+import { asArray, asObject, display, h, note, section, type JsonObject } from "./dom.ts";
+
+const NODE_LIMIT = 511;
+const numbers = (value: unknown) => asArray(value).map((item) => (typeof item === "number" ? item : Number.NaN));
+
+/** Relative bars for importances or coefficients, as a table so values stay readable. */
+const bars = (caption: string, names: readonly string[], values: readonly number[]) => {
+ const peak = Math.max(...values.map((value) => Math.abs(value)).filter(Number.isFinite), 0) || 1;
+ return h(
+ "div",
+ { class: "inspector-table", tabindex: "0", role: "region", "aria-label": caption },
+ h(
+ "table",
+ { class: "inspector-bars" },
+ h("caption", {}, caption),
+ h("thead", {}, h("tr", {}, h("th", { scope: "col" }, "Feature"), h("th", { scope: "col" }, "Value"), h("th", { scope: "col" }, h("span", { class: "visually-hidden" }, "Relative size")))),
+ h(
+ "tbody",
+ {},
+ ...values.map((value, index) => {
+ const width = Number.isFinite(value) ? (Math.abs(value) / peak) * 100 : 0;
+ return h(
+ "tr",
+ {},
+ h("td", {}, names[index] ?? `#${index}`),
+ h("td", {}, display(value)),
+ h("td", { "aria-hidden": "true" }, h("span", { class: "inspector-bar-fill", "data-sign": value < 0 ? "negative" : "positive", style: `width: ${width.toFixed(1)}%` })),
+ );
+ }),
+ ),
+ ),
+ );
+};
+
+/** One sklearn tree as an expandable outline, read straight from the native arrays. */
+export const treeOutline = (tree: JsonObject, names: readonly string[]): HTMLElement[] => {
+ const left = numbers(tree.children_left);
+ const right = numbers(tree.children_right);
+ const feature = numbers(tree.feature);
+ const threshold = asArray(tree.threshold);
+ const value = asArray(tree.value);
+ const samples = asArray(tree.n_node_samples);
+ let rendered = 0;
+ const node = (index: number, depth: number, branch: string): HTMLElement => {
+ rendered += 1;
+ const leaf = left[index] === -1 || left[index] === undefined;
+ const counts = `samples ${display(samples[index])} · value ${display(value[index])}`;
+ if (leaf || rendered >= NODE_LIMIT) {
+ return h("li", { class: "tree-leaf" }, h("span", { class: "tree-branch" }, branch), ` ${leaf ? "leaf" : "…"} · ${counts}`);
+ }
+ const name = names[feature[index]!] ?? `feature ${display(feature[index])}`;
+ const summary = h("summary", {}, h("span", { class: "tree-branch" }, branch), " ", h("strong", {}, name), ` ≤ ${display(threshold[index])}`, h("span", { class: "tree-meta" }, ` · ${counts}`));
+ const children = h("ul", {}, node(left[index]!, depth + 1, "true"), node(right[index]!, depth + 1, "false"));
+ const open = depth < 3 ? { open: "" } : {};
+ return h("li", {}, h("details", open, summary, children));
+ };
+ const root = h("ul", { class: "tree-outline", "aria-label": "Decision tree" }, node(0, 0, "root"));
+ const total = Number(tree.node_count);
+ return [
+ note(`${display(tree.node_count)} nodes, depth ${display(tree.max_depth)}. Splits send rows with feature ≤ threshold to "true". value is sklearn's native node value, shown as stored.`),
+ root,
+ rendered < total ? note(`Showing the first ${rendered} of ${total} nodes; the raw JSON below has every node.`) : null,
+ ].filter((item) => item !== null);
+};
+
+const ensemble = (structure: JsonObject, names: readonly string[]) => {
+ const trees = asArray(structure.trees).map(asObject);
+ const host = h("div", { class: "tree-host" });
+ const select = h("select", { "aria-label": "Tree" }, ...trees.map((_, index) => h("option", { value: String(index) }, `Tree ${index + 1} of ${trees.length}`)));
+ const show = () => host.replaceChildren(...treeOutline(trees[Number(select.value)] ?? {}, names));
+ select.addEventListener("change", show);
+ show();
+ return [note(`Estimator shape ${display(structure.estimator_shape)}.`), h("label", { class: "code-select" }, select), host];
+};
+
+const linear = (structure: JsonObject, names: readonly string[]) => {
+ const coef = asArray(structure.coef);
+ const rows = Array.isArray(coef[0]) ? coef.map(numbers) : [numbers(coef)];
+ return [
+ note(`Intercept: ${display(structure.intercept)}. Coefficients apply to the model's input features, in order.`),
+ ...rows.map((row, index) => bars(rows.length > 1 ? `Coefficients, row ${index}` : "Coefficients", names, row)),
+ ];
+};
+
+export const structureView = (bundle: BundleReport) => {
+ const structure = bundle.structure;
+ if (!structure) {
+ if (bundle.status === "inspected") {
+ return [note("Uploaded files are inspected without deserializing the model, so the playground does not show their structure: structure is only trustworthy once mlbundle.load has recomputed it from the model.")];
+ }
+ if (bundle.status !== "verified") return [note("No verified structure: mlbundle did not load this bundle.")];
+ return [note("This model has no structure in format version 1 (for example an all-scikit-learn Pipeline or an MLP). Claims still cover the whole model.")];
+ }
+ const names = asArray(asObject(bundle.manifest?.features).names).map(String);
+ const family = String(structure.family);
+ const importances = asArray(structure.feature_importances);
+ const body =
+ family === "decision_tree" ? treeOutline(asObject(structure.tree), names)
+ : family === "tree_ensemble" ? ensemble(structure, names)
+ : family === "linear" ? linear(structure, names)
+ : [];
+ return [
+ note(`Family: ${family}. Recomputed by mlbundle.load from the loaded model and compared with structure.json.`),
+ ...body,
+ importances.length ? section("Feature importances", bars("Feature importances", names, numbers(importances))) : null,
+ ];
+};
diff --git a/playground/src/links.ts b/playground/src/links.ts
new file mode 100644
index 0000000..f1f5e92
--- /dev/null
+++ b/playground/src/links.ts
@@ -0,0 +1,9 @@
+// Development links point at the local landing and docs servers, as the landing does.
+const dev = import.meta.env.DEV;
+
+export const LINKS = {
+ home: dev ? "http://127.0.0.1:4340/" : "https://mlbundle.eshkra.dev/",
+ docs: dev ? "http://127.0.0.1:4341/" : "https://docs.mlbundle.eshkra.dev/",
+ github: "https://github.com/UlloaSP/mlbundle",
+ eshkra: "https://eshkra.dev/",
+} as const;
diff --git a/playground/src/main.ts b/playground/src/main.ts
new file mode 100644
index 0000000..e025d8d
--- /dev/null
+++ b/playground/src/main.ts
@@ -0,0 +1,30 @@
+import "@fontsource-variable/mona-sans/standard.css";
+import "@fontsource-variable/jetbrains-mono/wght.css";
+import "./styles/index.css";
+import mark from "../../brand/mlbundle.png?url";
+import { bindMenu } from "./menu.ts";
+import { applyDesign, browserStorage, readSettings, saveSettings } from "./settings.ts";
+import { createShell } from "./shell.ts";
+import { mountViewport } from "./viewport.ts";
+import { mountWorkspace } from "./workspace/mount.ts";
+
+document.querySelector('link[rel="icon"]')!.href = mark;
+const storage = browserStorage();
+const settings = readSettings(storage);
+applyDesign(document.documentElement, settings);
+const root = document.querySelector("#root")!;
+if (new URLSearchParams(location.search).has("editor")) {
+ // The editor frame: the page around it owns the header, the menu and the settings.
+ root.className = "editor-page";
+ mountWorkspace(root, storage);
+} else {
+ const shell = createShell(mark);
+ root.replaceWith(shell);
+ const updateViewport = mountViewport(shell.querySelector(".app-stage")!, settings);
+ bindMenu(shell, settings, (next) => {
+ applyDesign(document.documentElement, next);
+ updateViewport(next);
+ // Unavailable storage keeps the choice for this visit only.
+ saveSettings(storage, next);
+ });
+}
diff --git a/playground/src/menu.ts b/playground/src/menu.ts
new file mode 100644
index 0000000..ae5e131
--- /dev/null
+++ b/playground/src/menu.ts
@@ -0,0 +1,102 @@
+import { icon } from "./icons.ts";
+import {
+ APPEARANCES,
+ STYLES,
+ THEMES,
+ VIEWPORTS,
+ SIZE_LIMITS,
+ validSize,
+ type Settings,
+} from "./settings.ts";
+
+const options = (items: readonly { id: string; label: string }[]) =>
+ items.map(({ id, label }) => ``).join("");
+const sizeInput = (key: "width" | "height", label: string) =>
+ ``;
+
+/** Floating options menu, following MLForm's playground menu. Editor is the only workspace. */
+export const MENU = `
+ `;
+
+export const bindMenu = (
+ shell: HTMLElement,
+ initial: Settings,
+ change: (settings: Settings) => void,
+) => {
+ let settings = initial;
+ const menu = shell.querySelector('[data-role="menu"]')!;
+ const button = menu.querySelector(".app-menu-button")!;
+ const panel = menu.querySelector(".app-menu-panel")!;
+ const editor = menu.querySelector('[data-role="editor-option"]')!;
+ const custom = menu.querySelector('[data-role="custom-size"]')!;
+ const fields = [...menu.querySelectorAll("[data-setting]")];
+
+ const isOpen = () => menu.classList.contains("is-open");
+ const setOpen = (open: boolean, restoreFocus = false) => {
+ menu.classList.toggle("is-open", open);
+ button.setAttribute("aria-expanded", String(open));
+ panel.hidden = !open;
+ if (open) editor.focus();
+ else if (restoreFocus) button.focus();
+ };
+ const render = () => {
+ for (const field of fields) field.value = String(settings[field.dataset.setting as keyof Settings]);
+ custom.hidden = settings.viewport !== "custom";
+ };
+ const update = (field: HTMLInputElement | HTMLSelectElement) => {
+ const key = field.dataset.setting as keyof Settings;
+ const value = key === "width" || key === "height" ? Number(field.value) : field.value;
+ // Out-of-range sizes restore the last valid value instead of resizing.
+ if (typeof value === "number" && !validSize(value)) return render();
+ settings = { ...settings, [key]: value };
+ render();
+ change(settings);
+ };
+
+ for (const field of fields) field.addEventListener("change", () => update(field));
+ button.addEventListener("click", () => setOpen(!isOpen()));
+ button.addEventListener("keydown", (event) => {
+ if (event.key !== "ArrowDown") return;
+ event.preventDefault();
+ setOpen(true);
+ });
+ // Editor is already showing: choosing it only closes the menu.
+ editor.addEventListener("click", (event) => setOpen(false, event.detail === 0));
+ document.addEventListener("click", (event) => {
+ if (isOpen() && event.target instanceof Node && !menu.contains(event.target)) setOpen(false);
+ });
+ document.addEventListener("keydown", (event) => {
+ if (event.key === "Escape" && isOpen()) setOpen(false, true);
+ });
+ menu.addEventListener("focusout", (event) => {
+ if (event.relatedTarget instanceof Node && !menu.contains(event.relatedTarget)) setOpen(false);
+ });
+ // Clicks inside the editor frame never reach this document; the frame takes focus instead.
+ window.addEventListener("blur", () => {
+ if (document.activeElement instanceof HTMLIFrameElement) setOpen(false);
+ });
+ render();
+};
diff --git a/playground/src/runtime/client.ts b/playground/src/runtime/client.ts
new file mode 100644
index 0000000..f5d488a
--- /dev/null
+++ b/playground/src/runtime/client.ts
@@ -0,0 +1,179 @@
+import type { FromWorker, OutputStream, RunReport, RuntimeVersions, ToWorker } from "./protocol.ts";
+import { BOOT_TIMEOUT_MS, RUN_TIMEOUT_MS } from "./versions.ts";
+
+export interface WorkerLike {
+ postMessage(message: ToWorker, transfer?: Transferable[]): void;
+ terminate(): void;
+ onmessage: ((event: MessageEvent) => void) | null;
+ onerror: ((event: ErrorEvent) => void) | null;
+}
+
+export type RuntimeState =
+ | { status: "booting"; message: string }
+ | { status: "ready"; versions: RuntimeVersions }
+ | { status: "running"; versions: RuntimeVersions }
+ | { status: "failed"; message: string };
+
+export type RunResult =
+ | { kind: "finished"; report: RunReport; durationMs: number }
+ | { kind: "timeout"; limitMs: number }
+ | { kind: "cancelled" }
+ | { kind: "failed"; message: string }
+ | { kind: "unavailable" };
+
+export interface RuntimeListener {
+ onState(state: RuntimeState): void;
+ onStream(stream: OutputStream, text: string): void;
+}
+
+export interface RuntimeLimits {
+ bootMs: number;
+ runMs: number;
+}
+
+interface PendingRun {
+ id: number;
+ timer: ReturnType;
+ resolve(result: RunResult): void;
+}
+
+const describe = (error: unknown) => (error instanceof Error ? error.message : String(error));
+
+/**
+ * Owns the Python worker. A run that times out or is cancelled terminates the
+ * worker, which is the only way to stop arbitrary Python, and boots a fresh one.
+ * A worker failure ends any pending run at once and leaves the runtime failed
+ * until `restart()`.
+ */
+export const createPythonRuntime = (
+ createWorker: () => WorkerLike,
+ listener: RuntimeListener,
+ limits: RuntimeLimits = { bootMs: BOOT_TIMEOUT_MS, runMs: RUN_TIMEOUT_MS },
+) => {
+ let worker: WorkerLike | undefined;
+ let state: RuntimeState = { status: "booting", message: "Starting Python…" };
+ let pending: PendingRun | undefined;
+ let bootTimer: ReturnType | undefined;
+ let nextId = 1;
+
+ const setState = (next: RuntimeState) => {
+ state = next;
+ listener.onState(next);
+ };
+ const settle = (result: RunResult) => {
+ if (!pending) return;
+ clearTimeout(pending.timer);
+ const { resolve } = pending;
+ pending = undefined;
+ if (result.kind === "finished" && state.status === "running") {
+ setState({ status: "ready", versions: state.versions });
+ }
+ resolve(result);
+ };
+ const stopWorker = () => {
+ clearTimeout(bootTimer);
+ worker?.terminate();
+ worker = undefined;
+ };
+ const fail = (reason: string) => {
+ const message = `${state.status === "booting" ? "Python could not start" : "Python stopped unexpectedly"}: ${reason}`;
+ stopWorker();
+ settle({ kind: "failed", message });
+ setState({ status: "failed", message });
+ };
+
+ const handle = (message: FromWorker) => {
+ switch (message.type) {
+ case "progress":
+ if (state.status === "booting") setState({ status: "booting", message: message.message });
+ return;
+ case "ready":
+ clearTimeout(bootTimer);
+ setState({ status: "ready", versions: message.versions });
+ return;
+ case "boot-error":
+ fail(message.message);
+ return;
+ case "stream":
+ if (message.id === pending?.id) listener.onStream(message.stream, message.text);
+ return;
+ case "done":
+ if (message.id === pending?.id) {
+ settle({ kind: "finished", report: message.report, durationMs: message.durationMs });
+ }
+ }
+ };
+
+ const start = () => {
+ stopWorker();
+ setState({ status: "booting", message: "Starting Python…" });
+ let current: WorkerLike;
+ try {
+ current = createWorker();
+ } catch (error) {
+ fail(describe(error));
+ return;
+ }
+ worker = current;
+ current.onmessage = ({ data }) => {
+ if (worker === current) handle(data);
+ };
+ current.onerror = (event) => {
+ if (worker !== current) return;
+ event.preventDefault();
+ fail(event.message || "the Python worker crashed.");
+ };
+ bootTimer = setTimeout(
+ () => fail(`no response within ${limits.bootMs / 1000} seconds.`),
+ limits.bootMs,
+ );
+ };
+
+ const submit = (message: ToWorker, transfer: Transferable[] = []): Promise => {
+ if (state.status !== "ready" || !worker) return Promise.resolve({ kind: "unavailable" });
+ const target = worker;
+ const result = new Promise((resolve) => {
+ const timer = setTimeout(() => {
+ settle({ kind: "timeout", limitMs: limits.runMs });
+ start();
+ }, limits.runMs);
+ pending = { id: message.id, timer, resolve };
+ });
+ setState({ status: "running", versions: state.versions });
+ try {
+ target.postMessage(message, transfer);
+ } catch (error) {
+ fail(describe(error));
+ }
+ return result;
+ };
+ const run = (code: string) => submit({ type: "run", id: nextId++, code });
+ /** Metadata-only inspection of a visitor's file; the buffer is transferred. */
+ const inspect = (name: string, data: ArrayBuffer) =>
+ submit({ type: "inspect", id: nextId++, name, data }, [data]);
+
+ const restart = () => {
+ settle({ kind: "cancelled" });
+ start();
+ };
+
+ start();
+ return {
+ run,
+ inspect,
+ restart,
+ /** Stops the current run, if any, by restarting Python. */
+ cancel() {
+ if (pending) restart();
+ },
+ get state() {
+ return state;
+ },
+ dispose() {
+ settle({ kind: "cancelled" });
+ stopWorker();
+ },
+ };
+};
+
+export type PythonRuntime = ReturnType;
diff --git a/playground/src/runtime/harness.py b/playground/src/runtime/harness.py
new file mode 100644
index 0000000..5d6cbc8
--- /dev/null
+++ b/playground/src/runtime/harness.py
@@ -0,0 +1,174 @@
+"""Execution harness for the mlbundle playground. Runs inside Pyodide only."""
+
+import errno
+import json
+import linecache
+import os
+import shutil
+import sys
+import traceback
+from importlib import import_module
+from pathlib import Path
+from types import TracebackType
+
+# This module exists only in the Pyodide runtime exercised by test:runtime.
+from pyodide.code import eval_code_async # pyright: ignore[reportMissingImports]
+
+FILENAME = ""
+WORK_ROOT = Path("/playground")
+MAX_BUNDLES = 8
+MIB = 1024 * 1024
+# Lowered with mlbundle's own ArchiveLimits: a browser tab is not a model server.
+LIMITS_MIB = {
+ "max_archive_bytes": 64,
+ "max_compressed_bytes": 64,
+ "max_member_bytes": 64,
+ "max_total_bytes": 128,
+}
+_runs = 0
+
+
+def _no_hard_links(*_args: object, **_kwargs: object) -> None:
+ raise OSError(errno.ENOSYS, "Pyodide's file system has no hard links")
+
+
+def runtime_versions(source_root: str) -> str:
+ """Prepare the interpreter, import the repository's mlbundle and report versions."""
+ if source_root not in sys.path:
+ sys.path.insert(0, source_root)
+ # Pyodide omits os.link. mlbundle then uses its documented exclusive-create
+ # publication, exactly as on other file systems without hard links.
+ if not hasattr(os, "link"):
+ os.link = _no_hard_links # pyright: ignore[reportAttributeAccessIssue]
+ from importlib.metadata import version # noqa: PLC0415
+
+ import_module("mlbundle")
+ names = ("mlbundle", "mlschema", "scikit-learn", "skops", "pandas", "numpy")
+ return json.dumps(
+ {"python": sys.version.split()[0]} | {name: version(name) for name in names}
+ )
+
+
+def _limits():
+ from mlbundle import ArchiveLimits # noqa: PLC0415
+
+ return ArchiveLimits(**{key: value * MIB for key, value in LIMITS_MIB.items()})
+
+
+def _error_text(error: BaseException) -> str:
+ return f"{type(error).__name__}: {error}"
+
+
+def _metadata(view) -> dict:
+ manifest = json.loads(view.manifest.to_bytes())
+ integrity = manifest.get("integrity", {})
+ members = [
+ {"name": str(name), **integrity.get(str(name), {})} for name in view.members
+ ]
+ return {
+ "manifest": manifest,
+ "members": members,
+ "schema": view.schema,
+ "samples": view.samples,
+ }
+
+
+def _structure(structure: object) -> object:
+ if structure is None:
+ return None
+ from mlbundle.canonical import canonical_dumps # noqa: PLC0415
+
+ # Canonical JSON keeps non-finite numbers as {"$float": ...} tags.
+ return json.loads(canonical_dumps(structure, max_bytes=256 * MIB, max_depth=64))
+
+
+def describe_bundle(path: str, *, verify: bool) -> dict:
+ """Inspect metadata; when verify is set, also load and recompute model claims.
+
+ Uploaded files are never verified here: verification deserializes the model.
+ """
+ from mlbundle import inspect, load # noqa: PLC0415
+
+ target = Path(path)
+ report: dict = {"fileName": target.name, "sizeBytes": target.stat().st_size}
+ try:
+ report |= _metadata(inspect(target, limits=_limits()))
+ except Exception as error: # Every rejection is shown verbatim.
+ return report | {"status": "rejected", "error": _error_text(error)}
+ if not verify:
+ return report | {"status": "inspected", "structure": None}
+ try:
+ loaded = load(target, limits=_limits())
+ except Exception as error:
+ return report | {"status": "unverified", "error": _error_text(error)}
+ return report | {"status": "verified", "structure": _structure(loaded.structure)}
+
+
+def inspect_upload(path: str) -> str:
+ """Metadata-only inspection of a file chosen by the visitor."""
+ try:
+ return json.dumps(describe_bundle(path, verify=False), allow_nan=False)
+ finally:
+ Path(path).unlink(missing_ok=True)
+
+
+def _user_traceback(error: BaseException) -> TracebackType | None:
+ tb = error.__traceback__
+ while tb is not None and tb.tb_frame.f_code.co_filename != FILENAME:
+ tb = tb.tb_next
+ return tb
+
+
+def _format_error(error: BaseException) -> str:
+ lines = traceback.format_exception(type(error), error, _user_traceback(error))
+ return "".join(lines).rstrip()
+
+
+def _outcome(value: object) -> dict:
+ if value is None:
+ return {"kind": "none"}
+ try:
+ text = json.dumps(value, indent=2, ensure_ascii=False, allow_nan=False)
+ except (TypeError, ValueError):
+ return {
+ "kind": "repr",
+ "typeName": type(value).__qualname__,
+ "text": repr(value),
+ }
+ return {"kind": "json", "text": text}
+
+
+def _bundles(directory: Path) -> tuple[list[dict], int]:
+ found = sorted(p for p in directory.glob("*.mlbundle") if p.is_file())
+ reports = [describe_bundle(str(p), verify=True) for p in found[:MAX_BUNDLES]]
+ for report, path in zip(reports, found, strict=False):
+ report["path"] = str(path)
+ return reports, max(0, len(found) - MAX_BUNDLES)
+
+
+async def run(code: str) -> str:
+ """Run code in a fresh namespace and an empty working directory.
+
+ Every *.mlbundle the code writes into that directory is inspected and then
+ verified with mlbundle.load, because the visitor's own code produced it.
+ """
+ global _runs # noqa: PLW0603
+ _runs += 1
+ shutil.rmtree(WORK_ROOT, ignore_errors=True)
+ directory = WORK_ROOT / f"run-{_runs}"
+ directory.mkdir(parents=True)
+ os.chdir(directory)
+ linecache.cache[FILENAME] = (len(code), None, code.splitlines(True), FILENAME)
+ namespace = {"__name__": "__main__"}
+ try:
+ outcome = _outcome(await eval_code_async(code, namespace, filename=FILENAME))
+ except BaseException as error:
+ outcome = {"kind": "error", "text": _format_error(error)}
+ finally:
+ sys.stdout.flush()
+ sys.stderr.flush()
+ os.chdir("/")
+ bundles, skipped = _bundles(directory)
+ return json.dumps(
+ {"outcome": outcome, "bundles": bundles, "skipped": skipped}, allow_nan=False
+ )
diff --git a/playground/src/runtime/protocol.ts b/playground/src/runtime/protocol.ts
new file mode 100644
index 0000000..2477d89
--- /dev/null
+++ b/playground/src/runtime/protocol.ts
@@ -0,0 +1,65 @@
+export interface RuntimeVersions {
+ python: string;
+ mlbundle: string;
+ mlschema: string;
+ "scikit-learn": string;
+ skops: string;
+ pandas: string;
+ numpy: string;
+}
+
+/** What the last expression of a run produced, as reported by harness.py. */
+export type RunOutcome =
+ | { kind: "json"; text: string }
+ | { kind: "repr"; text: string; typeName: string }
+ | { kind: "none" }
+ | { kind: "error"; text: string };
+
+export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
+
+export interface MemberRecord {
+ name: string;
+ sha256?: string;
+ size?: number;
+}
+
+/**
+ * One bundle as mlbundle reported it. `verified`: inspect and load both passed, so
+ * model claims and structure were recomputed. `inspected`: metadata only, the model
+ * was never deserialized. `unverified`: inspect passed, load rejected it.
+ * `rejected`: inspect rejected it.
+ */
+export interface BundleReport {
+ fileName: string;
+ sizeBytes: number;
+ source: "generated" | "uploaded";
+ status: "verified" | "inspected" | "unverified" | "rejected";
+ error?: string;
+ manifest?: { [key: string]: Json };
+ members?: MemberRecord[];
+ schema?: { [key: string]: Json } | null;
+ samples?: { [key: string]: Json }[] | null;
+ structure?: { [key: string]: Json } | null;
+ /** The exact bytes mlbundle wrote, for download. Generated bundles only. */
+ bytes?: Uint8Array;
+}
+
+export interface RunReport {
+ outcome: RunOutcome;
+ bundles: BundleReport[];
+ /** Bundles beyond the per-run limit that were not inspected. */
+ skipped: number;
+}
+
+export type OutputStream = "stdout" | "stderr";
+
+export type ToWorker =
+ | { type: "run"; id: number; code: string }
+ | { type: "inspect"; id: number; name: string; data: ArrayBuffer };
+
+export type FromWorker =
+ | { type: "progress"; message: string }
+ | { type: "ready"; versions: RuntimeVersions }
+ | { type: "boot-error"; message: string }
+ | { type: "stream"; id: number; stream: OutputStream; text: string }
+ | { type: "done"; id: number; report: RunReport; durationMs: number };
diff --git a/playground/src/runtime/session.ts b/playground/src/runtime/session.ts
new file mode 100644
index 0000000..4c9fdac
--- /dev/null
+++ b/playground/src/runtime/session.ts
@@ -0,0 +1,100 @@
+import type { PyodideAPI } from "pyodide";
+import type { BundleReport, RunReport, RuntimeVersions } from "./protocol.ts";
+import { PYODIDE_PACKAGES, PYPI_PINS, SOURCE_ROOT } from "./versions.ts";
+
+export interface PythonSession {
+ versions: RuntimeVersions;
+ run(code: string): Promise;
+ inspect(name: string, data: Uint8Array): Promise;
+}
+
+/** Package-relative path, e.g. `mlbundle/inspection.py`, to file contents. */
+export type SourceTree = Readonly>;
+
+/** The [project] version, so importlib.metadata reports what the manifest records. */
+export const projectVersion = (pyproject: string) => {
+ const match = /^\[project\][^[]*?^version\s*=\s*"([^"]+)"/m.exec(pyproject);
+ if (!match) throw new Error("pyproject.toml declares no [project] version.");
+ return match[1]!;
+};
+
+const writeSources = (pyodide: PyodideAPI, sources: SourceTree, version: string) => {
+ if (!Object.hasOwn(sources, "mlbundle/__init__.py")) {
+ throw new Error("The mlbundle sources are missing from this build.");
+ }
+ for (const [path, text] of Object.entries(sources)) {
+ const target = `${SOURCE_ROOT}/${path}`;
+ pyodide.FS.mkdirTree(target.slice(0, target.lastIndexOf("/")));
+ pyodide.FS.writeFile(target, text);
+ }
+ // Minimal distribution metadata: mlbundle reads its own version from it.
+ const info = `${SOURCE_ROOT}/mlbundle-${version}.dist-info`;
+ pyodide.FS.mkdirTree(info);
+ pyodide.FS.writeFile(
+ `${info}/METADATA`,
+ `Metadata-Version: 2.4\nName: mlbundle\nVersion: ${version}\n`,
+ );
+};
+
+const UPLOAD_PATH = "/uploads/upload.mlbundle";
+
+/**
+ * Prepare a loaded Pyodide instance: install the pinned wheels, write the real
+ * mlbundle sources and load the execution harness. Shared by the browser worker
+ * and the Node runtime tests so both exercise the same path.
+ */
+export const startSession = async (
+ pyodide: PyodideAPI,
+ sources: SourceTree,
+ pyproject: string,
+ harness: string,
+ onProgress: (message: string) => void = () => {},
+): Promise => {
+ onProgress("Loading scikit-learn and pandas…");
+ await pyodide.loadPackage([...PYODIDE_PACKAGES], { messageCallback: () => {} });
+ onProgress("Installing pydantic, skops and mlschema…");
+ const micropip = pyodide.pyimport("micropip");
+ try {
+ await micropip.install.callKwargs([...PYPI_PINS], { deps: false });
+ } finally {
+ micropip.destroy();
+ }
+ onProgress("Loading mlbundle…");
+ writeSources(pyodide, sources, projectVersion(pyproject));
+ const namespace = pyodide.toPy({});
+ pyodide.runPython(harness, { globals: namespace, filename: "harness.py" });
+ const versionsOf = namespace.get("runtime_versions");
+ const runCode = namespace.get("run");
+ const inspectUpload = namespace.get("inspect_upload");
+ const versions = JSON.parse(versionsOf(SOURCE_ROOT)) as RuntimeVersions;
+ versionsOf.destroy();
+
+ // Attach the written bytes, then remove the run directory's files.
+ const collect = (raw: string, source: BundleReport["source"]): RunReport => {
+ const report = JSON.parse(raw) as RunReport & { bundles: (BundleReport & { path?: string })[] };
+ for (const bundle of report.bundles) {
+ bundle.source = source;
+ if (bundle.path && bundle.status !== "rejected") {
+ bundle.bytes = pyodide.FS.readFile(bundle.path) as Uint8Array;
+ }
+ delete bundle.path;
+ }
+ return report;
+ };
+
+ return {
+ versions,
+ async run(code) {
+ return collect(await runCode(code), "generated");
+ },
+ async inspect(name, data) {
+ pyodide.FS.mkdirTree("/uploads");
+ pyodide.FS.writeFile(UPLOAD_PATH, data);
+ const bundle = JSON.parse(inspectUpload(UPLOAD_PATH)) as BundleReport;
+ // The visitor's own file name, never a path: the harness saw a fixed one.
+ bundle.fileName = name;
+ bundle.source = "uploaded";
+ return { outcome: { kind: "none" }, bundles: [bundle], skipped: 0 };
+ },
+ };
+};
diff --git a/playground/src/runtime/sources.ts b/playground/src/runtime/sources.ts
new file mode 100644
index 0000000..cbfdf05
--- /dev/null
+++ b/playground/src/runtime/sources.ts
@@ -0,0 +1,15 @@
+import type { SourceTree } from "./session.ts";
+import pyproject from "../../../pyproject.toml?raw";
+
+// The repository's real package source, bundled at build time. Never a copy.
+// state_fields.json is package data the sklearn adapter reads at import time.
+const files = import.meta.glob(
+ ["../../../src/mlbundle/**/*.py", "../../../src/mlbundle/**/*.json"],
+ { query: "?raw", import: "default", eager: true },
+);
+
+export const MLBUNDLE_SOURCES: SourceTree = Object.fromEntries(
+ Object.entries(files).map(([path, text]) => [path.slice(path.indexOf("mlbundle/")), text]),
+);
+
+export const PYPROJECT = pyproject;
diff --git a/playground/src/runtime/versions.ts b/playground/src/runtime/versions.ts
new file mode 100644
index 0000000..8b727c3
--- /dev/null
+++ b/playground/src/runtime/versions.ts
@@ -0,0 +1,40 @@
+// Pinned so every visitor gets the same interpreter and wheels.
+export const PYODIDE_VERSION = "314.0.7";
+export const PYODIDE_INDEX_URL = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`;
+
+// Wheels shipped with this Pyodide release: numpy 2.4.6, pandas 3.0.2, scipy 1.18.0 and
+// scikit-learn 1.8.0. mlbundle's sklearn catalog is audited against 1.9.1, which has no
+// build for this ABI; the manifest records the versions that actually ran.
+export const PYODIDE_PACKAGES = [
+ "micropip",
+ "numpy",
+ "pandas",
+ "scipy",
+ "scikit-learn",
+] as const;
+
+// Pure-Python wheels from PyPI at the versions in the repository's uv.lock.
+// Pyodide's pydantic (2.12.5) is below mlbundle's floor; pydantic-core publishes a
+// pyemscripten_2026_0 build. The others have no compiled code.
+export const PYPI_PINS = [
+ "pydantic==2.14.1",
+ "pydantic-core==2.50.1",
+ "typing-extensions==4.16.0",
+ "typing-inspection==0.4.4",
+ "annotated-types==0.8.0",
+ "skops==0.16.0",
+ "prettytable==3.18.0",
+ "wcwidth==0.9.2",
+ "packaging==26.3",
+ "mlschema==0.3.0",
+] as const;
+
+// Where the repository's src/mlbundle tree is written inside the Pyodide file system.
+export const SOURCE_ROOT = "/mlbundle/src";
+
+export const BOOT_TIMEOUT_MS = 240_000;
+export const RUN_TIMEOUT_MS = 60_000;
+export const OUTPUT_LIMIT_CHARS = 100_000;
+
+// Uploads above this size are refused before they are read into the browser.
+export const UPLOAD_LIMIT_BYTES = 64 * 1024 * 1024;
diff --git a/playground/src/runtime/worker.ts b/playground/src/runtime/worker.ts
new file mode 100644
index 0000000..82b6a1e
--- /dev/null
+++ b/playground/src/runtime/worker.ts
@@ -0,0 +1,67 @@
+import type { FromWorker, ToWorker } from "./protocol.ts";
+import harness from "./harness.py?raw";
+import { startSession, type PythonSession } from "./session.ts";
+import { MLBUNDLE_SOURCES, PYPROJECT } from "./sources.ts";
+import { OUTPUT_LIMIT_CHARS, PYODIDE_INDEX_URL } from "./versions.ts";
+
+// The DOM and WebWorker libs conflict in one program; type only what is used.
+const scope = self as unknown as {
+ postMessage(message: FromWorker, transfer?: Transferable[]): void;
+ addEventListener(type: "message", listener: (event: MessageEvent) => void): void;
+};
+const post = (message: FromWorker, transfer: Transferable[] = []) =>
+ scope.postMessage(message, transfer);
+let runId = 0;
+let written = 0;
+
+const forward = (stream: "stdout" | "stderr") => (line: string) => {
+ if (runId === 0 || written > OUTPUT_LIMIT_CHARS) return;
+ written += line.length + 1;
+ const text =
+ written > OUTPUT_LIMIT_CHARS ? "\n[Output truncated: over 100,000 characters.]\n" : `${line}\n`;
+ post({ type: "stream", id: runId, stream, text });
+};
+
+const boot = async (): Promise => {
+ post({ type: "progress", message: "Downloading Python…" });
+ // Loaded from the pinned CDN release so the loader always matches its WebAssembly.
+ const { loadPyodide } = (await import(
+ /* @vite-ignore */ `${PYODIDE_INDEX_URL}pyodide.mjs`
+ )) as typeof import("pyodide");
+ const pyodide = await loadPyodide({
+ indexURL: PYODIDE_INDEX_URL,
+ stdout: forward("stdout"),
+ stderr: forward("stderr"),
+ });
+ return startSession(pyodide, MLBUNDLE_SOURCES, PYPROJECT, harness, (message) =>
+ post({ type: "progress", message }),
+ );
+};
+
+const session = boot();
+session.then(
+ ({ versions }) => post({ type: "ready", versions }),
+ (error: unknown) =>
+ post({ type: "boot-error", message: error instanceof Error ? error.message : String(error) }),
+);
+
+scope.addEventListener("message", async ({ data }) => {
+ const python = await session;
+ runId = data.id;
+ written = 0;
+ const started = performance.now();
+ try {
+ const report =
+ data.type === "run"
+ ? await python.run(data.code)
+ : await python.inspect(data.name, new Uint8Array(data.data));
+ const transfer = report.bundles.flatMap((bundle) => (bundle.bytes ? [bundle.bytes.buffer] : []));
+ post({ type: "done", id: data.id, report, durationMs: performance.now() - started }, transfer);
+ } catch (error) {
+ const text = error instanceof Error ? error.message : String(error);
+ const report = { outcome: { kind: "error", text } as const, bundles: [], skipped: 0 };
+ post({ type: "done", id: data.id, report, durationMs: 0 });
+ } finally {
+ runId = 0;
+ }
+});
diff --git a/playground/src/settings.ts b/playground/src/settings.ts
new file mode 100644
index 0000000..e2141a3
--- /dev/null
+++ b/playground/src/settings.ts
@@ -0,0 +1,119 @@
+/** Options-menu settings. The catalogs mirror MLForm's playground (themes, recipes, viewports). */
+export const THEMES = [
+ { id: "eshkra", label: "eshkra" },
+ { id: "m3diseen", label: "M3DISEEN" },
+ { id: "dosetailor", label: "DoseTailor" },
+ { id: "clickhouse", label: "ClickHouse" },
+ { id: "airbnb", label: "Airbnb" },
+ { id: "neutral", label: "Neutral" },
+ { id: "cobalt", label: "Cobalt" },
+ { id: "sage", label: "Sage" },
+ { id: "sunset", label: "Sunset" },
+ { id: "graphite", label: "Graphite" },
+] as const;
+export const STYLES = [
+ { id: "default", label: "Default" },
+ { id: "minimal", label: "Minimal" },
+ { id: "soft", label: "Soft" },
+ { id: "contrast", label: "Contrast" },
+] as const;
+export const APPEARANCES = [
+ { id: "auto", label: "System" },
+ { id: "light", label: "Light" },
+ { id: "dark", label: "Dark" },
+] as const;
+export const VIEWPORTS = [
+ { id: "actual", label: "Current viewport" },
+ { id: "phone", label: "Phone · 390 × 844", width: 390, height: 844 },
+ { id: "tablet", label: "Tablet · 820 × 1180", width: 820, height: 1180 },
+ { id: "laptop", label: "Laptop · 1366 × 768", width: 1366, height: 768 },
+ { id: "pc", label: "PC · 1920 × 1080", width: 1920, height: 1080 },
+ { id: "ultrawide", label: "Ultrawide · 2560 × 1080", width: 2560, height: 1080 },
+ { id: "custom", label: "Custom" },
+] as const;
+export const SIZE_LIMITS = { min: 1, max: 8192 } as const;
+
+type Id = T[number]["id"];
+export interface Settings {
+ theme: Id;
+ style: Id;
+ appearance: Id;
+ viewport: Id;
+ width: number;
+ height: number;
+}
+export const SETTINGS_KEY = "mlbundle-playground-settings";
+export const DEFAULT_SETTINGS: Settings = {
+ theme: "eshkra",
+ style: "default",
+ appearance: "auto",
+ viewport: "actual",
+ width: 1280,
+ height: 800,
+};
+
+export const validSize = (value: unknown): value is number =>
+ typeof value === "number" &&
+ Number.isInteger(value) &&
+ value >= SIZE_LIMITS.min &&
+ value <= SIZE_LIMITS.max;
+
+const pick = (items: T, value: unknown, fallback: Id) =>
+ (items.find(({ id }) => id === value)?.id as Id | undefined) ?? fallback;
+
+/** Each saved field is validated on its own, so one bad value never resets the others. */
+export const parseSettings = (value: unknown): Settings => {
+ const saved = value && typeof value === "object" ? (value as Record) : {};
+ return {
+ theme: pick(THEMES, saved.theme, DEFAULT_SETTINGS.theme),
+ style: pick(STYLES, saved.style, DEFAULT_SETTINGS.style),
+ appearance: pick(APPEARANCES, saved.appearance, DEFAULT_SETTINGS.appearance),
+ viewport: pick(VIEWPORTS, saved.viewport, DEFAULT_SETTINGS.viewport),
+ width: validSize(saved.width) ? saved.width : DEFAULT_SETTINGS.width,
+ height: validSize(saved.height) ? saved.height : DEFAULT_SETTINGS.height,
+ };
+};
+
+export const readSettings = (storage?: Pick): Settings => {
+ try {
+ return parseSettings(JSON.parse(storage?.getItem(SETTINGS_KEY) ?? "null"));
+ } catch {
+ return { ...DEFAULT_SETTINGS };
+ }
+};
+
+/** Returns false when the choice cannot be stored; it still applies to this visit. */
+export const saveSettings = (storage: Pick | undefined, settings: Settings) => {
+ try {
+ storage?.setItem(SETTINGS_KEY, JSON.stringify(settings));
+ return storage !== undefined;
+ } catch {
+ return false;
+ }
+};
+
+/** The simulated viewport, or null when the workspace uses the real window. */
+export const viewportSize = (settings: Settings) => {
+ if (settings.viewport === "actual") return null;
+ const preset = VIEWPORTS.find(({ id }) => id === settings.viewport)!;
+ const size =
+ "width" in preset
+ ? { width: preset.width, height: preset.height }
+ : { width: settings.width, height: settings.height };
+ return { ...size, name: preset.label.split(" · ")[0]! };
+};
+
+export const applyDesign = (root: HTMLElement, settings: Settings) => {
+ root.dataset.theme = settings.theme;
+ root.dataset.style = settings.style;
+ root.dataset.appearance = settings.appearance;
+};
+
+/** The browser's storage, or undefined where access is blocked (some privacy modes). */
+export const browserStorage = () => {
+ try {
+ return window.localStorage;
+ } catch {
+ return undefined;
+ }
+};
diff --git a/playground/src/shell.ts b/playground/src/shell.ts
new file mode 100644
index 0000000..88409ce
--- /dev/null
+++ b/playground/src/shell.ts
@@ -0,0 +1,27 @@
+import { icon } from "./icons.ts";
+import { LINKS } from "./links.ts";
+import { MENU } from "./menu.ts";
+import eshkraMark from "./assets/eshkra-mark.png?url";
+
+export const createShell = (mark: string) => {
+ const shell = document.createElement("main");
+ shell.className = "app-shell";
+ shell.innerHTML = `
+
+
+
+ MLbundle
+
+
+
+ ${MENU}
+
+ `;
+ return shell;
+};
diff --git a/playground/src/styles/base.css b/playground/src/styles/base.css
new file mode 100644
index 0000000..8562a79
--- /dev/null
+++ b/playground/src/styles/base.css
@@ -0,0 +1,85 @@
+:root {
+ font-family: var(--font-sans);
+ color: var(--text);
+ background: var(--bg);
+ scrollbar-color: var(--text-muted) var(--bg);
+ line-height: 1.5;
+ font-weight: 400;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+}
+
+body {
+ background: var(--bg);
+}
+
+button,
+select,
+input {
+ font: inherit;
+ touch-action: manipulation;
+}
+
+a {
+ color: inherit;
+ text-decoration-thickness: 1px;
+ text-underline-offset: 0.25em;
+}
+
+::selection {
+ background: var(--accent);
+ color: var(--text-inverse);
+}
+
+:focus-visible {
+ outline: 2px solid var(--text);
+ outline-offset: 2px;
+}
+
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+}
+
+.app-shell {
+ display: flex;
+ flex-direction: column;
+ height: 100dvh;
+ padding-top: var(--header-height);
+ overflow: hidden;
+ accent-color: var(--accent);
+ caret-color: var(--text);
+}
+
+.editor-page {
+ display: flex;
+ height: 100dvh;
+ overflow: hidden;
+}
+.editor-page > * {
+ flex: 1;
+ min-width: 0;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ * {
+ transition-duration: 0s !important;
+ }
+}
diff --git a/playground/src/styles/controls.css b/playground/src/styles/controls.css
new file mode 100644
index 0000000..e5c48bd
--- /dev/null
+++ b/playground/src/styles/controls.css
@@ -0,0 +1,54 @@
+.icon-button {
+ display: inline-grid;
+ place-items: center;
+ flex-shrink: 0;
+ width: var(--control-size);
+ height: var(--control-size);
+ padding: 0;
+ border: var(--border-width) solid var(--border-strong);
+ border-radius: var(--radius-sm);
+ background: var(--surface);
+ color: var(--text);
+ cursor: pointer;
+ transition: background var(--motion-fast);
+}
+.code-workspace .icon-button {
+ min-height: 0;
+ padding: 0;
+}
+.icon-button:hover:not(:disabled) {
+ background: var(--hover-surface);
+}
+
+/* Icon-only controls name themselves in a tooltip on hover and on keyboard focus. */
+:where([data-tooltip]) {
+ position: relative;
+}
+[data-tooltip]::after {
+ content: attr(data-tooltip);
+ position: absolute;
+ z-index: 60;
+ top: calc(100% + 0.45rem);
+ right: 0;
+ width: max-content;
+ max-width: min(20rem, 80vw);
+ padding: 0.35rem 0.55rem;
+ border: var(--border-width) solid var(--border-strong);
+ border-radius: var(--radius-sm);
+ background: var(--surface-elevated);
+ color: var(--text);
+ font: 500 0.75rem/1.4 var(--font-sans);
+ text-align: left;
+ white-space: nowrap;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity var(--motion-fast);
+}
+[data-tooltip]:focus-visible::after {
+ opacity: 1;
+}
+@media (hover: hover) {
+ [data-tooltip]:hover::after {
+ opacity: 1;
+ }
+}
diff --git a/playground/src/styles/editor.css b/playground/src/styles/editor.css
new file mode 100644
index 0000000..143c425
--- /dev/null
+++ b/playground/src/styles/editor.css
@@ -0,0 +1,37 @@
+:is(.code-editor, .output-json) .cm-editor {
+ height: 100%;
+ background: var(--surface);
+ color: var(--text);
+}
+:is(.code-editor, .output-json) .cm-scroller {
+ overflow: auto;
+ font: var(--code-font-size)/var(--code-line-height) var(--font-mono);
+ font-variant-ligatures: none;
+}
+:is(.code-editor, .output-json) .cm-content {
+ padding: 0.75rem 0;
+ caret-color: var(--text);
+}
+:is(.code-editor, .output-json) .cm-gutters {
+ border-color: var(--border);
+ background: var(--surface-muted);
+ color: var(--text-muted);
+}
+.code-editor :is(.cm-activeLine, .cm-activeLineGutter) {
+ background: var(--hover-surface);
+}
+:is(.code-editor, .output-json) .cm-editor.cm-focused {
+ outline: 2px solid var(--text);
+ outline-offset: -2px;
+}
+.code-editor .cm-cursor {
+ border-left-color: var(--text);
+}
+:is(.code-editor, .output-json) .cm-editor .cm-selectionBackground,
+:is(.code-editor, .output-json) .cm-content ::selection {
+ background: color-mix(in srgb, var(--accent) 30%, transparent) !important;
+}
+.code-editor .cm-matchingBracket {
+ background: color-mix(in srgb, var(--accent) 18%, transparent);
+ outline: 1px solid var(--accent);
+}
diff --git a/playground/src/styles/header.css b/playground/src/styles/header.css
new file mode 100644
index 0000000..121bde9
--- /dev/null
+++ b/playground/src/styles/header.css
@@ -0,0 +1,80 @@
+/* Brand header, as in MLForm's playground (brand-header.css); the menu floats at its left. */
+.app-brand-header {
+ position: fixed;
+ inset: 0 0 auto;
+ z-index: 40;
+ height: var(--header-height);
+ padding: 1rem 1.25rem calc(var(--header-height) - 4rem) 5rem;
+ background: var(--bg);
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+.mlbundle-brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.6rem;
+ min-width: 0;
+ color: inherit;
+ font-size: 1.625rem;
+ text-decoration: none;
+}
+.mlbundle-brand img {
+ width: 1.625rem;
+ height: 2.5rem;
+ object-fit: contain;
+}
+.mlbundle-wordmark {
+ white-space: nowrap;
+}
+.mlbundle-wordmark .ml {
+ font-weight: 800;
+ font-stretch: 125%;
+ letter-spacing: -0.01em;
+}
+.mlbundle-wordmark .rest {
+ font-weight: 300;
+ font-stretch: 100%;
+}
+.app-brand-header a:focus-visible {
+ outline: 2px solid var(--text);
+ outline-offset: 4px;
+}
+.app-brand-header nav {
+ display: flex;
+ gap: 0.25rem;
+ margin-left: auto;
+}
+.app-brand-header nav .icon-button {
+ border-color: transparent;
+ background: transparent;
+ text-decoration: none;
+}
+.app-brand-header nav .icon-button:hover {
+ background: var(--hover-surface);
+}
+@media (max-width: 620px) {
+ :root {
+ --header-height: 4.35rem;
+ }
+ .app-brand-header {
+ gap: 0.65rem;
+ padding-inline: 4.6rem 0.75rem;
+ }
+ .app-brand-header nav {
+ gap: 0;
+ }
+ .mlbundle-brand {
+ gap: 0.35rem;
+ font-size: 1.5rem;
+ }
+ .mlbundle-brand img {
+ width: 1.4rem;
+ height: 2.15rem;
+ }
+}
+@media (max-width: 380px) {
+ .mlbundle-brand {
+ font-size: 1.3rem;
+ }
+}
diff --git a/playground/src/styles/index.css b/playground/src/styles/index.css
new file mode 100644
index 0000000..4f6562a
--- /dev/null
+++ b/playground/src/styles/index.css
@@ -0,0 +1,12 @@
+@import "./tokens.css";
+@import "./base.css";
+@import "./header.css";
+@import "./workspace.css";
+@import "./output.css";
+@import "./editor.css";
+@import "./controls.css";
+@import "./menu.css";
+@import "./viewport.css";
+@import "./themes.css";
+@import "./recipes.css";
+@import "./inspector.css";
diff --git a/playground/src/styles/inspector.css b/playground/src/styles/inspector.css
new file mode 100644
index 0000000..37e6ab6
--- /dev/null
+++ b/playground/src/styles/inspector.css
@@ -0,0 +1,226 @@
+/* Bundle inspector: bundle bar, view tabs and the tab panel. */
+.output-banner {
+ flex-shrink: 0;
+ margin: 0;
+ padding: 0.6rem 0.8rem;
+ border-bottom: var(--border-width) solid var(--border);
+ color: var(--text-muted);
+ font-size: 0.8rem;
+}
+
+.inspector-bar {
+ flex-shrink: 0;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem;
+ min-height: 2.75rem;
+ padding: 0.35rem 0.75rem;
+ border-bottom: var(--border-width) solid var(--border);
+}
+.inspector-title {
+ flex: 1 1 8rem;
+ min-width: 0;
+ overflow: hidden;
+ font: 0.8rem var(--font-mono);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.inspector-chip {
+ flex-shrink: 0;
+ border-radius: 999px;
+ padding: 0.1rem 0.55rem;
+ border: var(--border-width) solid currentColor;
+ font-size: 0.72rem;
+ font-weight: 600;
+}
+.inspector-chip[data-status="verified"] { color: var(--success); }
+.inspector-chip[data-status="inspected"] { color: var(--info); }
+.inspector-chip:is([data-status="unverified"], [data-status="rejected"]) { color: var(--danger); }
+
+.inspector-tabs {
+ flex-shrink: 0;
+ display: flex;
+ gap: 0.15rem;
+ overflow-x: auto;
+ padding: 0 0.5rem;
+ border-bottom: var(--border-width) solid var(--border);
+ scrollbar-width: thin;
+}
+.code-workspace .inspector-tabs [role="tab"] {
+ flex-shrink: 0;
+ min-height: 2.4rem;
+ border: 0;
+ border-bottom: 2px solid transparent;
+ border-radius: 0;
+ background: transparent;
+ color: var(--text-muted);
+ font-weight: 500;
+}
+.code-workspace .inspector-tabs [role="tab"][aria-selected="true"] {
+ border-bottom-color: var(--accent);
+ color: var(--text);
+}
+.inspector-tabs [role="tab"]:focus-visible,
+.inspector-panel:focus-visible,
+.inspector-table:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: -2px;
+}
+
+.inspector-panel {
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+ padding: 0.75rem 0.9rem 1.5rem;
+ font-size: 0.85rem;
+}
+.inspector-panel.is-manifest {
+ display: flex;
+ padding: 0;
+}
+.inspector-panel.is-manifest .output-json {
+ flex: 1;
+}
+
+.inspector-status {
+ display: grid;
+ gap: 0.25rem;
+ margin-bottom: 0.9rem;
+ padding: 0.65rem 0.8rem;
+ border-radius: var(--radius-sm);
+ border: var(--border-width) solid var(--border);
+ border-left: 3px solid var(--info);
+ background: var(--surface-muted);
+}
+.inspector-status[data-status="verified"] { border-left-color: var(--success); }
+.inspector-status:is([data-status="unverified"], [data-status="rejected"]) {
+ border-left-color: var(--danger);
+ background: var(--danger-soft);
+}
+.inspector-status span { color: var(--text-muted); }
+.inspector-error {
+ font: 0.78rem/1.5 var(--font-mono);
+ color: var(--danger);
+ overflow-wrap: anywhere;
+ white-space: pre-wrap;
+}
+
+.inspector-facts {
+ display: grid;
+ grid-template-columns: max-content minmax(0, 1fr);
+ gap: 0.3rem 1rem;
+ margin: 0 0 1rem;
+}
+.inspector-facts dt { color: var(--text-muted); }
+.inspector-facts dd {
+ margin: 0;
+ overflow-wrap: anywhere;
+ font-family: var(--font-mono);
+ font-size: 0.8rem;
+}
+
+.inspector-section { margin-top: 1.1rem; }
+.inspector-section h3,
+.inspector-table caption {
+ margin: 0 0 0.4rem;
+ color: var(--text-muted);
+ font-size: 0.7rem;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-align: left;
+ text-transform: uppercase;
+}
+.inspector-note {
+ margin: 0 0 0.6rem;
+ color: var(--text-muted);
+ line-height: 1.5;
+}
+
+.inspector-table {
+ max-width: 100%;
+ overflow-x: auto;
+ margin-bottom: 0.6rem;
+}
+.inspector-table table {
+ min-width: 100%;
+ border-collapse: collapse;
+ font-size: 0.8rem;
+}
+.inspector-table :is(th, td) {
+ padding: 0.3rem 0.6rem;
+ border-bottom: var(--border-width) solid var(--border);
+ text-align: left;
+ vertical-align: top;
+}
+.inspector-table td {
+ font-family: var(--font-mono);
+ overflow-wrap: anywhere;
+}
+.inspector-table th { color: var(--text-muted); font-weight: 600; }
+
+.inspector-bars td:last-child { width: 45%; }
+.inspector-bar-fill {
+ display: block;
+ height: 0.6rem;
+ min-width: 1px;
+ border-radius: 2px;
+ background: var(--accent);
+}
+.inspector-bar-fill[data-sign="negative"] { background: var(--info); }
+
+.tree-outline,
+.tree-outline ul {
+ margin: 0;
+ padding-left: 1.1rem;
+ list-style: none;
+ font: 0.78rem/1.7 var(--font-mono);
+}
+.tree-outline { padding-left: 0; margin-bottom: 0.8rem; }
+.tree-outline ul { border-left: var(--border-width) solid var(--border); }
+.tree-outline summary { cursor: pointer; }
+.tree-outline summary:focus-visible { outline: 2px solid var(--accent); }
+.tree-branch {
+ color: var(--accent);
+ font-size: 0.7rem;
+ text-transform: uppercase;
+}
+.tree-meta,
+.tree-leaf { color: var(--text-muted); }
+.tree-host { margin-top: 0.6rem; }
+
+.inspector-raw { margin-top: 1rem; }
+.inspector-raw summary { cursor: pointer; color: var(--text-muted); }
+.inspector-raw pre {
+ max-height: 24rem;
+ overflow: auto;
+ margin: 0.4rem 0 0;
+ padding: 0.6rem;
+ border-radius: var(--radius-sm);
+ background: var(--surface-muted);
+ font: 0.75rem/1.5 var(--font-mono);
+}
+
+.output-console {
+ display: flex;
+ flex-direction: column;
+ gap: 0.6rem;
+}
+.output-console :is(.output-error, .output-repr, .output-streams) {
+ max-height: none;
+ margin: 0;
+ padding: 0.7rem;
+ border-radius: var(--radius-sm);
+ font: 0.78rem/1.55 var(--font-mono);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+}
+.output-console :is(.output-repr, .output-streams) { background: var(--surface-muted); }
+.output-console .output-error { border: 0; }
+.output-console .output-note { margin: 0; padding: 0; }
+
+@media (max-width: 620px) {
+ .inspector-panel { padding-inline: 0.6rem; }
+ .inspector-facts { grid-template-columns: minmax(0, 1fr); gap: 0 0; }
+ .inspector-facts dd { margin-bottom: 0.4rem; }
+}
diff --git a/playground/src/styles/menu.css b/playground/src/styles/menu.css
new file mode 100644
index 0000000..5344523
--- /dev/null
+++ b/playground/src/styles/menu.css
@@ -0,0 +1,141 @@
+/* Floating options menu, as in MLForm's playground (demo-menu.css). */
+.app-menu {
+ position: fixed;
+ top: 1rem;
+ left: 1rem;
+ z-index: 50;
+}
+
+.app-menu-button {
+ width: 3rem;
+ height: 3rem;
+ display: grid;
+ place-content: center;
+ padding: 0;
+ border: var(--border-width) solid var(--border-strong);
+ border-radius: var(--radius-xl);
+ background: var(--surface-elevated);
+ color: var(--text);
+ cursor: pointer;
+ transition:
+ transform var(--motion-fast),
+ background var(--motion-fast);
+}
+.app-menu-button::after {
+ right: auto;
+ left: 0;
+}
+.app-menu.is-open .app-menu-button::after {
+ display: none;
+}
+
+.app-menu-button:hover,
+.app-menu-button:focus-visible,
+.app-menu.is-open .app-menu-button {
+ transform: translateY(-1px);
+ background: var(--hover-surface);
+}
+
+.app-menu-button:focus-visible,
+.app-menu-option:focus-visible,
+.app-menu-field :is(select, input):focus-visible {
+ outline: 3px solid var(--text);
+ outline-offset: 3px;
+}
+
+.app-menu-panel {
+ position: absolute;
+ top: calc(100% + 0.55rem);
+ left: 0;
+ width: min(18rem, calc(100vw - 2rem));
+ max-height: calc(100dvh - 5rem);
+ overflow-y: auto;
+ padding: 0.75rem;
+ display: grid;
+ gap: 0.55rem;
+ border: var(--border-width) solid var(--border-strong);
+ border-radius: var(--radius-xl);
+ background: var(--chrome-bg);
+ color: var(--text);
+ box-shadow: var(--shadow);
+ transform-origin: top left;
+}
+.app-menu-panel[hidden] {
+ display: none;
+}
+
+.app-menu-list {
+ display: grid;
+ gap: 0.2rem;
+}
+
+.app-menu-section-label {
+ padding: 0 0.35rem;
+ color: var(--text-muted);
+ font-size: 0.78rem;
+ font-weight: 700;
+}
+
+.app-menu-option {
+ width: 100%;
+ min-height: 2.35rem;
+ padding: 0.55rem 0.7rem;
+ border: 0;
+ border-radius: var(--radius-sm);
+ background: transparent;
+ color: var(--text);
+ text-align: left;
+ cursor: pointer;
+}
+.app-menu-option:hover,
+.app-menu-option:focus-visible {
+ background: var(--hover-surface);
+}
+.app-menu-option.is-active {
+ background: var(--accent);
+ color: var(--text-inverse);
+ font-weight: 700;
+}
+
+.app-menu-settings {
+ display: grid;
+ gap: 0.7rem;
+ padding: 0.8rem 0.35rem 0.2rem;
+ border-top: var(--border-width) solid var(--border);
+}
+
+.app-menu-field {
+ display: grid;
+ gap: 0.25rem;
+ font-size: 0.85rem;
+ font-weight: 600;
+}
+.app-menu-field :is(select, input) {
+ width: 100%;
+ min-width: 0;
+ min-height: 2.35rem;
+ padding: 0.35rem 0.55rem;
+ border: var(--border-width) solid var(--border-strong);
+ border-radius: var(--radius-sm);
+ background: var(--surface);
+ color: var(--text);
+ font-weight: 400;
+ cursor: pointer;
+}
+.app-menu-field input {
+ cursor: text;
+}
+
+.app-menu-custom-size {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.55rem;
+}
+
+@media (forced-colors: active) {
+ .app-menu-option.is-active {
+ forced-color-adjust: none;
+ background: Highlight;
+ color: HighlightText;
+ }
+}
diff --git a/playground/src/styles/output.css b/playground/src/styles/output.css
new file mode 100644
index 0000000..c344657
--- /dev/null
+++ b/playground/src/styles/output.css
@@ -0,0 +1,23 @@
+.code-output {
+ position: relative;
+}
+.output-json {
+ min-height: 6rem;
+ overflow: auto;
+}
+.code-output .output-note {
+ color: var(--text-muted);
+ font-family: var(--font-sans);
+ font-size: 0.8rem;
+}
+.output-error {
+ background: var(--danger-soft);
+ color: var(--danger);
+}
+/* Author display rules (flex, grid) must not override the hidden attribute. */
+[hidden] {
+ display: none !important;
+}
+.console-stderr {
+ color: var(--danger);
+}
diff --git a/playground/src/styles/recipes.css b/playground/src/styles/recipes.css
new file mode 100644
index 0000000..1512f66
--- /dev/null
+++ b/playground/src/styles/recipes.css
@@ -0,0 +1,23 @@
+/* MLForm's style recipes, applied to the playground chrome. Default is tokens.css. */
+:root[data-style="minimal"] {
+ /* Compact density, no motion, no shadows. */
+ --control-size: 2rem;
+ --motion-fast: 0ms linear;
+ --shadow: none;
+}
+:root[data-style="soft"] {
+ --chrome-bg:
+ radial-gradient(circle at top left, color-mix(in srgb, var(--accent) 10%, transparent), transparent 42%),
+ linear-gradient(
+ 180deg,
+ color-mix(in srgb, var(--surface) 92%, var(--surface-elevated)),
+ color-mix(in srgb, var(--surface-muted) 88%, var(--surface-elevated))
+ );
+}
+:root[data-style="contrast"] {
+ /* Spacious density with stronger borders. */
+ --control-size: 2.5rem;
+ --border-width: 1.5px;
+ --border-strong: var(--text);
+ --border: var(--text);
+}
diff --git a/playground/src/styles/themes.css b/playground/src/styles/themes.css
new file mode 100644
index 0000000..9ad27c7
--- /dev/null
+++ b/playground/src/styles/themes.css
@@ -0,0 +1,154 @@
+/* MLForm's other design systems (eshkra lives in tokens.css). Colors and radii only:
+ the playground keeps its own fonts and needs no form runtime. */
+:root[data-theme="m3diseen"] {
+ --radius-lg: 8px;
+ --radius-xl: 12px;
+ --bg: light-dark(#9092c7, #161722);
+ --surface: light-dark(#ffffff, #1e2030);
+ --surface-muted: light-dark(#f3f3f8, #252739);
+ --surface-elevated: light-dark(#ffffff, #2c2e42);
+ --text: light-dark(#212529, #f3f3fa);
+ --text-muted: light-dark(#54566c, #bfc1d7);
+ --text-inverse: light-dark(#ffffff, #161722);
+ --border: light-dark(#d9dae6, #3c3e55);
+ --border-strong: light-dark(#b9bbce, #555875);
+ --accent: light-dark(#5c5f9e, #b0b3e4);
+ --success: light-dark(#246549, #80d6a9);
+ --danger: light-dark(#a73342, #ffa2ad);
+ --danger-soft: light-dark(rgba(167, 51, 66, 0.08), rgba(255, 162, 173, 0.12));
+ --hover-surface: light-dark(#e9e9f4, #383b53);
+}
+:root[data-theme="dosetailor"] {
+ --radius-lg: 8px;
+ --radius-xl: 12px;
+ --bg: light-dark(#a9a4e3, #191626);
+ --surface: light-dark(#ffffff, #221e33);
+ --surface-muted: light-dark(#f5f4fa, #2b2540);
+ --surface-elevated: light-dark(#ffffff, #322b48);
+ --text: light-dark(#212529, #f6f3ff);
+ --text-muted: light-dark(#5e5975, #c9c0de);
+ --text-inverse: light-dark(#ffffff, #191626);
+ --border: light-dark(#dfdceb, #463c5e);
+ --border-strong: light-dark(#beb8d4, #625578);
+ --accent: light-dark(#655dc6, #b7aaf7);
+ --success: light-dark(#246549, #80d6a9);
+ --danger: light-dark(#a73342, #ffa2ad);
+ --danger-soft: light-dark(rgba(167, 51, 66, 0.08), rgba(255, 162, 173, 0.12));
+ --hover-surface: light-dark(#eeecfa, #403657);
+}
+:root[data-theme="clickhouse"] {
+ --radius-xl: 12px;
+ --bg: light-dark(#f7f7f2, #0a0a0a);
+ --surface: light-dark(#ffffff, #121212);
+ --surface-muted: light-dark(#eeeeea, #1a1a1a);
+ --surface-elevated: light-dark(#f4f4ef, #242424);
+ --text: light-dark(#0a0a0a, #ffffff);
+ --text-muted: light-dark(#5a5a5a, #cccccc);
+ --text-inverse: light-dark(#0a0a0a, #0a0a0a);
+ --border: light-dark(#d8d8cf, #2a2a2a);
+ --border-strong: light-dark(#bdbdaf, #3a3a3a);
+ --accent: light-dark(#e6eb52, #faff69);
+ --success: light-dark(#126d31, #22c55e);
+ --danger: light-dark(#b32d2d, #ff7770);
+ --danger-soft: light-dark(rgba(199, 57, 57, 0.12), rgba(239, 68, 68, 0.16));
+ --hover-surface: light-dark(#eeeeea, #242424);
+}
+:root[data-theme="airbnb"] {
+ --radius-sm: 8px;
+ --radius-lg: 20px;
+ --radius-xl: 32px;
+ --bg: light-dark(#ffffff, #121212);
+ --surface: light-dark(#ffffff, #1c1c1c);
+ --surface-muted: light-dark(#f7f7f7, #262626);
+ --surface-elevated: light-dark(#ffffff, #2b2b2b);
+ --text: light-dark(#222222, #f7f7f7);
+ --text-muted: light-dark(#6a6a6a, #b0b0b0);
+ --text-inverse: light-dark(#222222, #121212);
+ --border: light-dark(#dddddd, #3a3a3a);
+ --border-strong: light-dark(#c1c1c1, #505050);
+ --accent: light-dark(#ff385c, #ff385c);
+ --success: light-dark(#007508, #2fbd59);
+ --danger: light-dark(#c13515, #ff6b5f);
+ --danger-soft: light-dark(rgba(193, 53, 21, 0.12), rgba(255, 107, 95, 0.16));
+ --hover-surface: light-dark(#f2f2f2, #333333);
+}
+:root[data-theme="neutral"] {
+ --bg: light-dark(#f4f6f8, #12161c);
+ --surface: light-dark(#ffffff, #181d24);
+ --surface-muted: light-dark(#f0f3f6, #212832);
+ --surface-elevated: light-dark(#ffffff, #202731);
+ --text: light-dark(#172330, #ebf1f7);
+ --text-muted: light-dark(#526170, #a5b1bd);
+ --text-inverse: light-dark(#ffffff, #12161c);
+ --border: light-dark(#d9e1e7, #313a47);
+ --border-strong: light-dark(#b9c5cf, #414b58);
+ --accent: light-dark(#2456c7, #71a7ff);
+ --success: light-dark(#16704f, #42c88e);
+ --danger: light-dark(#b93636, #ff7a72);
+ --danger-soft: light-dark(rgba(185, 54, 54, 0.08), rgba(255, 122, 114, 0.14));
+ --hover-surface: light-dark(#e8edf2, #2a3440);
+}
+:root[data-theme="cobalt"] {
+ --bg: light-dark(#eef4fb, #0d1524);
+ --surface: light-dark(#ffffff, #111c30);
+ --surface-muted: light-dark(#e5eef8, #16233b);
+ --surface-elevated: light-dark(#ffffff, #1a2944);
+ --text: light-dark(#11243a, #edf4ff);
+ --text-muted: light-dark(#49627d, #a3b5cf);
+ --text-inverse: light-dark(#ffffff, #0d1524);
+ --border: light-dark(#c6d7ea, #243552);
+ --border-strong: light-dark(#afc6dd, #304465);
+ --accent: light-dark(#1f5eff, #68a0ff);
+ --success: light-dark(#167347, #3ad38a);
+ --danger: light-dark(#bd3434, #ff7f86);
+ --danger-soft: light-dark(rgba(214, 69, 69, 0.12), rgba(255, 127, 134, 0.14));
+ --hover-surface: light-dark(#d8e5f6, #1f3151);
+}
+:root[data-theme="sage"] {
+ --bg: light-dark(#f2f6f1, #121a15);
+ --surface: light-dark(#fbfdfb, #182119);
+ --surface-muted: light-dark(#e8efe6, #203024);
+ --surface-elevated: light-dark(#ffffff, #24372a);
+ --text: light-dark(#203126, #edf5ef);
+ --text-muted: light-dark(#526557, #a8b9aa);
+ --text-inverse: light-dark(#ffffff, #121a15);
+ --border: light-dark(#cfdbcf, #304234);
+ --border-strong: light-dark(#bbccbc, #3f5544);
+ --accent: light-dark(#2f7d57, #59b07d);
+ --success: light-dark(#256c44, #67d595);
+ --danger: light-dark(#a83f3d, #ff8e8a);
+ --danger-soft: light-dark(rgba(185, 74, 72, 0.12), rgba(255, 142, 138, 0.14));
+ --hover-surface: light-dark(#dde9db, #2a3d30);
+}
+:root[data-theme="sunset"] {
+ --bg: light-dark(#fff6f0, #1e1512);
+ --surface: light-dark(#fffdfb, #261b18);
+ --surface-muted: light-dark(#ffe9dd, #32231f);
+ --surface-elevated: light-dark(#ffffff, #392925);
+ --text: light-dark(#3b241d, #fff1ea);
+ --text-muted: light-dark(#7f5a50, #d3b0a3);
+ --text-inverse: light-dark(#26140e, #1e1512);
+ --border: light-dark(#f0cdbb, #513830);
+ --border-strong: light-dark(#e0b59f, #68473d);
+ --accent: light-dark(#d96a3a, #ff986b);
+ --success: light-dark(#25734f, #57c694);
+ --danger: light-dark(#b43e38, #ff8d86);
+ --danger-soft: light-dark(rgba(202, 74, 69, 0.12), rgba(255, 141, 134, 0.14));
+ --hover-surface: light-dark(#ffdcca, #422d27);
+}
+:root[data-theme="graphite"] {
+ --bg: light-dark(#f1f4f7, #0f1318);
+ --surface: light-dark(#ffffff, #171c22);
+ --surface-muted: light-dark(#e6ebf1, #202731);
+ --surface-elevated: light-dark(#ffffff, #24303d);
+ --text: light-dark(#202934, #ebf1f7);
+ --text-muted: light-dark(#5d6977, #9ca8b5);
+ --text-inverse: light-dark(#ffffff, #0f1318);
+ --border: light-dark(#cfd7e1, #2c3642);
+ --border-strong: light-dark(#bbc6d3, #3b4754);
+ --accent: light-dark(#375a7f, #4ea1ff);
+ --success: light-dark(#176c47, #37c78b);
+ --danger: light-dark(#be3a34, #ff6a61);
+ --danger-soft: light-dark(rgba(190, 58, 52, 0.12), rgba(255, 106, 97, 0.14));
+ --hover-surface: light-dark(#dbe2ea, #24303d);
+}
diff --git a/playground/src/styles/tokens.css b/playground/src/styles/tokens.css
new file mode 100644
index 0000000..1700091
--- /dev/null
+++ b/playground/src/styles/tokens.css
@@ -0,0 +1,46 @@
+/* The eshkra design system, MLForm's default theme; themes.css holds the others.
+ light-dark() follows color-scheme: the system's, or the visitor's choice. */
+:root {
+ color-scheme: light dark;
+ --font-sans: "Mona Sans Variable", "Mona Sans", ui-sans-serif, system-ui, sans-serif;
+ --font-mono: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, monospace;
+ --radius-sm: 6px;
+ --radius-lg: 12px;
+ --radius-xl: 14px;
+ --border-width: 1px;
+ --motion-fast: 120ms ease;
+ --shadow: 0 10px 30px rgb(0 0 0 / 0.08);
+ --control-size: 2.25rem;
+ --code-font-size: 0.8rem;
+ --code-line-height: 1.6;
+ --header-height: 4.75rem;
+
+ --bg: light-dark(#ffffff, #0a0a0d);
+ --surface: light-dark(#ffffff, #131318);
+ --surface-muted: light-dark(#f7f7f9, #1a1a21);
+ --surface-elevated: light-dark(#ffffff, #1c1c24);
+ --text: light-dark(#0b0b10, #f4f4f6);
+ --text-muted: light-dark(#666675, #b6b6c2);
+ --text-inverse: light-dark(#ffffff, #0a0a0d);
+ --border: light-dark(#e7e7ee, #24242c);
+ --border-strong: light-dark(#d2d2dc, #34343e);
+ /* The ink shade keeps small orange text readable on white. */
+ --accent: light-dark(#c2410c, #ff9a55);
+ --success: light-dark(#16704f, #65d2a1);
+ --info: light-dark(#3b4fc4, #9aa8ff);
+ --danger: light-dark(#b93636, #ff8e8a);
+ --danger-soft: light-dark(rgba(185, 54, 54, 0.08), rgba(255, 142, 138, 0.12));
+ --hover-surface: light-dark(#fff0e8, #29221f);
+ /* Derived roles follow whichever design system sets the palette. */
+ --button-bg: var(--text);
+ --button-text: var(--surface);
+ --chrome-bg: var(--surface-elevated);
+}
+
+:root[data-appearance="light"] {
+ color-scheme: light;
+}
+
+:root[data-appearance="dark"] {
+ color-scheme: dark;
+}
diff --git a/playground/src/styles/viewport.css b/playground/src/styles/viewport.css
new file mode 100644
index 0000000..eca0c97
--- /dev/null
+++ b/playground/src/styles/viewport.css
@@ -0,0 +1,51 @@
+/* The editor frame fills the stage, or simulates a device size as in MLForm's preview.css. */
+.app-stage {
+ flex: 1;
+ min-width: 0;
+ min-height: 0;
+ overflow: hidden;
+}
+.app-preview,
+.app-preview-viewport,
+.app-preview-frame {
+ width: 100%;
+ height: 100%;
+}
+.app-preview-frame {
+ display: block;
+ border: 0;
+ background: var(--bg);
+}
+
+.app-stage.is-preview {
+ overflow: auto;
+ padding: 0 1rem;
+ border-top: var(--border-width) solid var(--border);
+}
+.is-preview .app-preview {
+ display: grid;
+ justify-items: center;
+ align-content: start;
+ gap: 0.55rem;
+ height: auto;
+ padding-block: 0.75rem 1rem;
+}
+.is-preview .app-preview-label {
+ color: var(--text-muted);
+ font: 700 0.82rem/1.5 var(--font-mono);
+ font-variant-ligatures: none;
+ text-align: center;
+}
+.is-preview .app-preview-viewport {
+ position: relative;
+ overflow: hidden;
+ border: var(--border-width) solid var(--border-strong);
+ border-radius: var(--radius-lg);
+ box-sizing: content-box;
+}
+.is-preview .app-preview-frame {
+ position: absolute;
+ top: 0;
+ left: 0;
+ transform-origin: top left;
+}
diff --git a/playground/src/styles/workspace.css b/playground/src/styles/workspace.css
new file mode 100644
index 0000000..3977922
--- /dev/null
+++ b/playground/src/styles/workspace.css
@@ -0,0 +1,210 @@
+.code-workspace {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ min-height: 0;
+}
+
+.code-toolbar {
+ /* Above the panes, so control tooltips can overlap them. */
+ position: relative;
+ z-index: 5;
+ flex-shrink: 0;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.35rem;
+ min-height: 2.75rem;
+ padding: 0.35rem 0.75rem;
+ border-block: var(--border-width) solid var(--border);
+ background: var(--chrome-bg);
+}
+
+.code-select {
+ display: flex;
+ min-width: 0;
+}
+.code-select select {
+ max-width: 100%;
+ min-width: 0;
+}
+.code-position {
+ margin-right: auto;
+}
+
+.code-workspace button,
+.code-workspace select {
+ min-height: var(--control-size);
+ border: var(--border-width) solid var(--border-strong);
+ border-radius: var(--radius-sm);
+ padding: 0.3rem 0.6rem;
+ background: var(--surface);
+ color: var(--text);
+ font-size: 0.85rem;
+ cursor: pointer;
+}
+.code-workspace button:hover:not(:disabled) {
+ background: var(--hover-surface);
+}
+.code-workspace :is(button, select):disabled {
+ cursor: not-allowed;
+ opacity: 0.55;
+}
+
+.code-workspace .code-run {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.4rem;
+ border-color: var(--button-bg);
+ background: var(--button-bg);
+ color: var(--button-text);
+ font-weight: 600;
+}
+.code-workspace .code-run:hover:not(:disabled) {
+ border-color: var(--accent);
+ background: var(--accent);
+ color: var(--text-inverse);
+}
+
+.code-status {
+ display: grid;
+ place-items: center;
+ width: var(--control-size);
+ height: var(--control-size);
+ flex-shrink: 0;
+ color: var(--text-muted);
+}
+.code-status[data-symbol="check"] { color: var(--success); }
+.code-status[data-symbol="error"] { color: var(--danger); }
+.code-status[data-symbol="loading"] svg { animation: status-spin 1s linear infinite; }
+@keyframes status-spin { to { transform: rotate(360deg); } }
+@media (prefers-reduced-motion: reduce) {
+ .code-status[data-symbol="loading"] svg { animation: none; }
+}
+
+.code-notice {
+ flex-basis: 100%;
+ margin: 0;
+ font-size: 0.8rem;
+}
+
+.code-split {
+ flex: 1;
+ display: grid;
+ min-width: 0;
+ min-height: 0;
+ grid-template-columns: minmax(0, var(--code-share)) 0.75rem minmax(0, var(--output-share));
+ grid-template-rows: minmax(0, 1fr);
+}
+.code-source {
+ grid-column: 1;
+ grid-row: 1;
+}
+.code-separator {
+ grid-column: 2;
+ grid-row: 1;
+}
+.code-output {
+ grid-column: 3;
+ grid-row: 1;
+}
+.code-split[data-position="right"] {
+ grid-template-columns: minmax(0, var(--output-share)) 0.75rem minmax(0, var(--code-share));
+}
+.code-split[data-position="right"] .code-source {
+ grid-column: 3;
+}
+.code-split[data-position="right"] .code-output {
+ grid-column: 1;
+}
+.code-split:is([data-position="top"], [data-position="bottom"]) {
+ grid-template-columns: minmax(0, 1fr);
+ grid-template-rows: minmax(0, var(--code-share)) 0.75rem minmax(0, var(--output-share));
+}
+.code-split:is([data-position="top"], [data-position="bottom"]) > * {
+ grid-column: 1;
+}
+.code-split:is([data-position="top"], [data-position="bottom"]) .code-output {
+ grid-row: 3;
+}
+.code-split:is([data-position="top"], [data-position="bottom"]) .code-separator {
+ grid-row: 2;
+ cursor: row-resize;
+}
+.code-split[data-position="bottom"] {
+ grid-template-rows: minmax(0, var(--output-share)) 0.75rem minmax(0, var(--code-share));
+}
+.code-split[data-position="bottom"] .code-source {
+ grid-row: 3;
+}
+.code-split[data-position="bottom"] .code-output {
+ grid-row: 1;
+}
+
+.code-pane {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ min-height: 0;
+ overflow: hidden;
+ background: var(--surface);
+}
+.code-editor {
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.code-separator {
+ display: grid;
+ place-items: center;
+ cursor: col-resize;
+ touch-action: none;
+ background: var(--bg);
+ border-inline: var(--border-width) solid var(--border);
+}
+.code-separator::after {
+ content: "";
+ width: 2px;
+ height: 2rem;
+ border-radius: 2px;
+ background: var(--border-strong);
+}
+.code-separator:hover::after,
+.code-separator:focus-visible::after {
+ background: var(--accent);
+}
+.code-separator[aria-orientation="horizontal"] {
+ border-inline: 0;
+ border-block: var(--border-width) solid var(--border);
+}
+.code-separator[aria-orientation="horizontal"]::after {
+ width: 2rem;
+ height: 2px;
+}
+.code-split.is-resizing {
+ user-select: none;
+}
+
+@media (max-width: 620px) {
+ .code-toolbar {
+ padding-inline: 0.4rem;
+ }
+ .code-select:not(.code-position) {
+ flex: 1 1 100%;
+ }
+ .code-select:not(.code-position) select {
+ width: 100%;
+ }
+ .code-position {
+ margin-right: auto;
+ flex: 1;
+ }
+}
+
+@media (forced-colors: active) {
+ .code-separator::after {
+ background: ButtonText;
+ }
+}
diff --git a/playground/src/viewport.ts b/playground/src/viewport.ts
new file mode 100644
index 0000000..661e3d6
--- /dev/null
+++ b/playground/src/viewport.ts
@@ -0,0 +1,53 @@
+import { applyDesign, viewportSize, type Settings } from "./settings.ts";
+
+/**
+ * The editor always runs in one same-origin frame. Viewport changes only resize or scale
+ * that frame, so the code, the output and the running Python survive every change.
+ */
+export const mountViewport = (stage: HTMLElement, initial: Settings) => {
+ let settings = initial;
+ stage.innerHTML = `
+
+
+ `;
+ const preview = stage.querySelector(".app-preview")!;
+ const label = stage.querySelector(".app-preview-label")!;
+ const viewport = stage.querySelector(".app-preview-viewport")!;
+ const frame = stage.querySelector("iframe")!;
+ const syncDesign = () => {
+ const root = frame.contentDocument?.documentElement;
+ if (root) applyDesign(root, settings);
+ };
+ const resize = () => {
+ const size = viewportSize(settings);
+ stage.classList.toggle("is-preview", size !== null);
+ label.hidden = size === null;
+ if (!size) {
+ viewport.removeAttribute("style");
+ frame.removeAttribute("style");
+ return;
+ }
+ const { width, height, name } = size;
+ // Scale down to fit the stage width, as MLForm's preview does; height may scroll.
+ const room = preview.clientWidth - (viewport.offsetWidth - viewport.clientWidth);
+ const scale = Math.min(1, Math.max(1, room) / width);
+ viewport.style.width = `${width * scale}px`;
+ viewport.style.height = `${height * scale}px`;
+ frame.style.width = `${width}px`;
+ frame.style.height = `${height}px`;
+ frame.style.transform = scale < 1 ? `scale(${scale})` : "";
+ label.textContent = `${name} · ${width} × ${height} px${scale < 1 ? ` · ${Math.round(scale * 100)} %` : ""}`;
+ };
+ frame.addEventListener("load", syncDesign);
+ const url = new URL(window.location.href);
+ url.search = "?editor=1";
+ url.hash = "";
+ frame.src = url.href;
+ new ResizeObserver(resize).observe(stage);
+ resize();
+ return (next: Settings) => {
+ settings = next;
+ syncDesign();
+ resize();
+ };
+};
diff --git a/playground/src/workspace/editor.ts b/playground/src/workspace/editor.ts
new file mode 100644
index 0000000..b789bf6
--- /dev/null
+++ b/playground/src/workspace/editor.ts
@@ -0,0 +1,69 @@
+import { defaultKeymap, history, historyKeymap, indentWithTab, isolateHistory } from "@codemirror/commands";
+import { python } from "@codemirror/lang-python";
+import { bracketMatching, indentOnInput, indentUnit } from "@codemirror/language";
+import { EditorState } from "@codemirror/state";
+import { EditorView, highlightActiveLine, keymap, lineNumbers } from "@codemirror/view";
+import { codeHighlighting } from "./highlight.ts";
+
+export const createCodeEditor = (
+ parent: HTMLElement,
+ code: string,
+ onChange: (value: string) => void,
+ onRun: () => void,
+) => {
+ const editor = new EditorView({
+ parent,
+ state: EditorState.create({
+ doc: code,
+ extensions: [
+ lineNumbers(),
+ history(),
+ highlightActiveLine(),
+ indentOnInput(),
+ indentUnit.of(" "),
+ bracketMatching(),
+ python(),
+ codeHighlighting,
+ // Escape then Tab leaves the editor, so indenting with Tab does not trap focus.
+ keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
+ EditorView.contentAttributes.of({
+ "aria-label": "Python code",
+ spellcheck: "false",
+ autocorrect: "off",
+ autocapitalize: "off",
+ }),
+ EditorView.updateListener.of((update) => {
+ if (update.docChanged) onChange(update.state.doc.toString());
+ }),
+ ],
+ }),
+ });
+ const runShortcut = (event: KeyboardEvent) => {
+ if (event.key !== "Enter" || !(event.ctrlKey || event.metaKey) || event.altKey) return;
+ if (event.isComposing) return;
+ // Handle this app command before CodeMirror's Android Enter synthesis drops modifiers.
+ event.preventDefault();
+ event.stopPropagation();
+ onRun();
+ };
+ editor.contentDOM.addEventListener("keydown", runShortcut, true);
+ return {
+ setCode(value: string) {
+ editor.dispatch({
+ ...(value !== editor.state.doc.toString()
+ ? {
+ changes: { from: 0, to: editor.state.doc.length, insert: value },
+ annotations: isolateHistory.of("full"),
+ }
+ : {}),
+ selection: { anchor: 0 },
+ effects: EditorView.scrollIntoView(0, { y: "start" }),
+ });
+ },
+ focus: () => editor.focus(),
+ destroy() {
+ editor.contentDOM.removeEventListener("keydown", runShortcut, true);
+ editor.destroy();
+ },
+ };
+};
diff --git a/playground/src/workspace/examples.ts b/playground/src/workspace/examples.ts
new file mode 100644
index 0000000..02163eb
--- /dev/null
+++ b/playground/src/workspace/examples.ts
@@ -0,0 +1,28 @@
+const sources = import.meta.glob("../examples/*.py", {
+ query: "?raw",
+ import: "default",
+ eager: true,
+});
+
+export interface CodeExample {
+ id: string;
+ label: string;
+ code: string;
+}
+
+const CATALOG = [
+ ["classifier", "Classifier with schema"],
+ ["regressor", "Random forest regressor"],
+ ["samples", "Explicit samples and one-hot"],
+ ["pipeline", "sklearn Pipeline"],
+ ["errors", "Rejected models"],
+] as const;
+
+export const CODE_EXAMPLES: readonly CodeExample[] = CATALOG.map(([id, label]) => {
+ const code = sources[`../examples/${id}.py`];
+ if (code === undefined) throw new Error(`Missing example file: ${id}.py`);
+ return { id, label, code };
+});
+
+export const getCodeExample = (id: unknown): CodeExample =>
+ CODE_EXAMPLES.find((example) => example.id === id) ?? CODE_EXAMPLES[0]!;
diff --git a/playground/src/workspace/highlight.ts b/playground/src/workspace/highlight.ts
new file mode 100644
index 0000000..bedddde
--- /dev/null
+++ b/playground/src/workspace/highlight.ts
@@ -0,0 +1,12 @@
+import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
+import { tags } from "@lezer/highlight";
+
+export const codeHighlighting = syntaxHighlighting(
+ HighlightStyle.define([
+ { tag: [tags.keyword, tags.controlKeyword], color: "var(--accent)", fontWeight: "600" },
+ { tag: [tags.string, tags.number, tags.bool, tags.null], color: "var(--success)" },
+ { tag: tags.comment, color: "var(--text-muted)", fontStyle: "italic" },
+ { tag: [tags.propertyName, tags.definition(tags.variableName)], color: "var(--info)" },
+ { tag: [tags.className, tags.typeName], color: "var(--accent)", fontStyle: "italic" },
+ ]),
+);
diff --git a/playground/src/workspace/mount.ts b/playground/src/workspace/mount.ts
new file mode 100644
index 0000000..8f9381a
--- /dev/null
+++ b/playground/src/workspace/mount.ts
@@ -0,0 +1,215 @@
+import { icon, labelIcon } from "../icons.ts";
+import { formatBytes } from "../inspector/dom.ts";
+import { createPythonRuntime, type RunResult, type RuntimeState } from "../runtime/client.ts";
+import { RUN_TIMEOUT_MS, UPLOAD_LIMIT_BYTES } from "../runtime/versions.ts";
+import { createCodeEditor } from "./editor.ts";
+import { CODE_EXAMPLES, getCodeExample } from "./examples.ts";
+import { createOutputPanel } from "./output.ts";
+import { bindCodeSplit } from "./split.ts";
+import { isCodePosition, readWorkspaceState, saveWorkspaceState } from "./state.ts";
+import { createStatus } from "./status.ts";
+
+const MARKUP = `
+
+
+
+
+
+
+
+
+`;
+
+const failedRead = (text: string): RunResult => ({
+ kind: "finished",
+ durationMs: 0,
+ report: { outcome: { kind: "error", text }, bundles: [], skipped: 0 },
+});
+
+export const mountWorkspace = (container: HTMLElement, storage: Storage | undefined) => {
+ const first = CODE_EXAMPLES[0]!;
+ let state = readWorkspaceState(storage, {
+ code: first.code,
+ exampleId: first.id,
+ // Narrow screens start stacked; the position control still offers every layout.
+ position: window.innerWidth < 720 ? "top" : "left",
+ ratio: 50,
+ });
+ let runtimeState: RuntimeState = { status: "booting", message: "Starting Python…" };
+ let autoRun = true;
+
+ const shell = document.createElement("section");
+ shell.className = "code-workspace";
+ shell.setAttribute("aria-label", "Python code and bundle inspector");
+ shell.innerHTML = MARKUP;
+ container.replaceChildren(shell);
+ const part = (role: string) =>
+ shell.querySelector(`[data-role="${role}"]`)!;
+ const exampleSelect = part("example");
+ const positionSelect = part("position");
+ const runButton = part("run");
+ const uploadButton = part("upload");
+ const fileInput = part("file");
+ const status = createStatus(part("status"));
+ const notice = part("notice");
+ exampleSelect.replaceChildren(...CODE_EXAMPLES.map(({ id, label }) => new Option(label, id)));
+ exampleSelect.value = getCodeExample(state.exampleId).id;
+ positionSelect.value = state.position;
+ const output = createOutputPanel(shell.querySelector(".code-output")!);
+
+ const save = () => {
+ if (saveWorkspaceState(storage, state)) return;
+ notice.hidden = false;
+ notice.textContent = "Saving in this browser is unavailable. Copy your code before leaving.";
+ };
+ const renderRunButton = () => {
+ const running = runtimeState.status === "running";
+ if (running) labelIcon(runButton, "stop", "Stop");
+ else labelIcon(runButton, "run", "Run", "Run (Ctrl/⌘ + Enter)");
+ runButton.setAttribute("aria-description", running
+ ? "Stop this run. Python restarts."
+ : `Run the code (Ctrl/⌘ + Enter). Runs stop after ${RUN_TIMEOUT_MS / 1000} seconds.`);
+ runButton.disabled = runtimeState.status === "failed";
+ runButton.setAttribute("aria-busy", String(running));
+ // A new example would otherwise show the result of the code still running.
+ exampleSelect.disabled = running;
+ part("reset").disabled = running;
+ uploadButton.disabled = runtimeState.status !== "ready";
+ };
+
+ const run = async () => {
+ if (runtimeState.status === "running") return;
+ if (runtimeState.status !== "ready") {
+ autoRun = runtimeState.status === "booting";
+ if (autoRun) status.show("Will run when Python is ready", "loading");
+ return;
+ }
+ output.begin();
+ const result = await python.run(state.code);
+ output.finish(result, "run");
+ if (result.kind === "finished") {
+ const failed = result.report.outcome.kind === "error";
+ status.show(failed ? "Error" : "Done", failed ? "error" : "check");
+ } else if (result.kind === "timeout") status.show("Timed out", "error");
+ else if (result.kind === "cancelled") status.show("Stopped", "stop");
+ };
+
+ const inspectFile = async () => {
+ const file = fileInput.files?.[0];
+ fileInput.value = "";
+ if (!file || runtimeState.status !== "ready") return;
+ output.begin();
+ if (file.size > UPLOAD_LIMIT_BYTES) {
+ // Refused before reading, so a huge file never enters the tab's memory.
+ output.finish(failedRead(`${file.name} is ${formatBytes(file.size)}. The playground inspects files up to ${formatBytes(UPLOAD_LIMIT_BYTES)}; use the mlbundle CLI for larger bundles.`), "inspect");
+ status.show("File too large", "error");
+ return;
+ }
+ let data: ArrayBuffer;
+ try {
+ data = await file.arrayBuffer();
+ } catch {
+ output.finish(failedRead(`${file.name} could not be read.`), "inspect");
+ status.show("Error", "error");
+ return;
+ }
+ const result = await python.inspect(file.name, data);
+ output.finish(result, "inspect");
+ if (result.kind === "finished") {
+ const rejected = result.report.bundles[0]?.status !== "inspected";
+ status.show(rejected ? "Rejected" : "Inspected", rejected ? "error" : "check");
+ } else if (result.kind === "timeout") status.show("Timed out", "error");
+ };
+
+ const python = createPythonRuntime(
+ () => new Worker(new URL("../runtime/worker.ts", import.meta.url), { type: "module" }),
+ {
+ onState(next) {
+ runtimeState = next;
+ renderRunButton();
+ if (next.status === "booting") {
+ status.show(next.message, "loading");
+ output.showBooting(next.message);
+ } else if (next.status === "failed") {
+ status.show("Python unavailable", "error");
+ output.showFailure(next.message);
+ } else if (next.status === "running") status.show("Running…", "loading");
+ else {
+ output.showReady();
+ if (!["Done", "Error", "Inspected", "Rejected", "File too large"].includes(status.label)) {
+ status.show("Ready");
+ }
+ if (autoRun) {
+ autoRun = false;
+ void run();
+ }
+ }
+ },
+ onStream: (stream, text) => output.stream(stream, text),
+ },
+ );
+
+ const editor = createCodeEditor(
+ shell.querySelector(".code-editor")!,
+ state.code,
+ (code) => {
+ state = { ...state, code };
+ save();
+ },
+ () => void run(),
+ );
+ const split = bindCodeSplit(
+ shell.querySelector(".code-split")!,
+ shell.querySelector(".code-separator")!,
+ state.position,
+ state.ratio,
+ (position, ratio) => {
+ state = { ...state, position, ratio };
+ save();
+ },
+ );
+
+ const loadExample = (id: string) => {
+ const example = getCodeExample(id);
+ state = { ...state, exampleId: example.id, code: example.code };
+ editor.setCode(example.code);
+ save();
+ void run();
+ };
+ runButton.addEventListener("click", () => {
+ if (runtimeState.status === "running") python.cancel();
+ else void run();
+ });
+ exampleSelect.addEventListener("change", () => loadExample(exampleSelect.value));
+ part("reset").addEventListener("click", () => {
+ loadExample(state.exampleId);
+ editor.focus();
+ });
+ part("restart").addEventListener("click", () => {
+ autoRun = false;
+ python.restart();
+ });
+ uploadButton.addEventListener("click", () => fileInput.click());
+ fileInput.addEventListener("change", () => void inspectFile());
+ positionSelect.addEventListener("change", () => {
+ if (isCodePosition(positionSelect.value)) split.setPosition(positionSelect.value);
+ });
+ renderRunButton();
+};
diff --git a/playground/src/workspace/output.ts b/playground/src/workspace/output.ts
new file mode 100644
index 0000000..4ba7257
--- /dev/null
+++ b/playground/src/workspace/output.ts
@@ -0,0 +1,92 @@
+import { createInspector } from "../inspector/panel.ts";
+import { h } from "../inspector/dom.ts";
+import type { RunResult } from "../runtime/client.ts";
+import type { OutputStream, RunReport } from "../runtime/protocol.ts";
+
+const RESTART_HINT = "Check your connection, then choose Restart Python.";
+const NO_BUNDLE =
+ "This run wrote no .mlbundle file. Call export(model, \"name.mlbundle\", …) to write one into the run's working directory.";
+
+/**
+ * The right pane: the bundle inspector, with the run's console as its last tab.
+ * The console holds errors, the last expression and print() output.
+ */
+export const createOutputPanel = (pane: HTMLElement) => {
+ const error = h("pre", { class: "output-error", role: "alert", hidden: "" });
+ const message = h("p", { class: "output-note", role: "status" });
+ const result = h("pre", { class: "output-repr", tabindex: "0", "aria-label": "Last expression", hidden: "" });
+ const streams = h("pre", { class: "output-streams", tabindex: "0", "aria-label": "Console output", hidden: "" });
+ const consoleView = h("div", { class: "output-console" }, error, message, result, streams);
+ const notice = h("p", { class: "output-banner", role: "status", hidden: "" });
+ pane.append(notice);
+ const inspector = createInspector(pane, consoleView);
+ let bootFailed = false;
+
+ const setText = (element: HTMLElement, text: string) => {
+ element.hidden = !text;
+ element.textContent = text;
+ };
+ const showFailure = (text: string) => {
+ setText(error, `${text}\n${RESTART_HINT}`);
+ setText(notice, "Python is unavailable. See Console.");
+ bootFailed = true;
+ inspector.show("console");
+ };
+ const showReport = (report: RunReport, label: string) => {
+ const { outcome } = report;
+ setText(error, outcome.kind === "error" ? outcome.text : "");
+ setText(result, outcome.kind === "json" || outcome.kind === "repr" ? outcome.text : "");
+ const skipped = report.skipped ? ` ${report.skipped} more bundle(s) were not inspected (limit 8 per run).` : "";
+ setText(message, `${label}${skipped}`);
+ inspector.setBundles(report.bundles, outcome.kind === "error" ? "The code raised an error before writing a bundle. See Console." : NO_BUNDLE);
+ if (outcome.kind === "error" && report.bundles.length === 0) inspector.show("console");
+ else if (inspector.active === "console") inspector.show("overview");
+ };
+
+ return {
+ begin() {
+ streams.replaceChildren();
+ streams.hidden = true;
+ setText(notice, "");
+ pane.setAttribute("aria-busy", "true");
+ },
+ stream(stream: OutputStream, text: string) {
+ streams.append(h("span", { class: `console-${stream}` }, text));
+ streams.hidden = false;
+ },
+ finish(run: RunResult, action: "run" | "inspect") {
+ pane.setAttribute("aria-busy", "false");
+ if (run.kind === "timeout") {
+ setText(error, `Stopped after ${run.limitMs / 1000} seconds. Python restarted, so variables and imports were reset.`);
+ setText(result, "");
+ inspector.setBundles([], "The run timed out before finishing.");
+ inspector.show("console");
+ } else if (run.kind === "cancelled") {
+ setText(error, "");
+ setText(message, "Stopped. Python restarted, so variables and imports were reset.");
+ inspector.setBundles([], "The run was stopped.");
+ } else if (run.kind === "failed") {
+ showFailure(run.message);
+ } else if (run.kind === "unavailable") {
+ setText(notice, "Python is not ready yet.");
+ } else {
+ const seconds = (run.durationMs / 1000).toFixed(1);
+ const label = action === "inspect" ? `Inspected in ${seconds} s with mlbundle.inspect.` : `Finished in ${seconds} s.`;
+ if (run.report.outcome.kind === "none" && action === "run") setText(result, "");
+ showReport(run.report, label);
+ }
+ },
+ /** A startup failure stays visible until Python boots again. */
+ showFailure,
+ showBooting(text: string) {
+ if (bootFailed) {
+ bootFailed = false;
+ setText(error, "");
+ }
+ setText(notice, `${text} The first visit downloads Python, scikit-learn and pandas; later visits use your browser cache.`);
+ },
+ showReady() {
+ if (!bootFailed) setText(notice, "");
+ },
+ };
+};
diff --git a/playground/src/workspace/split.ts b/playground/src/workspace/split.ts
new file mode 100644
index 0000000..026f6b5
--- /dev/null
+++ b/playground/src/workspace/split.ts
@@ -0,0 +1,90 @@
+import { clampRatio, type CodePosition } from "./state.ts";
+
+export const bindCodeSplit = (
+ split: HTMLElement,
+ separator: HTMLElement,
+ initialPosition: CodePosition,
+ initialRatio: number,
+ onChange: (position: CodePosition, ratio: number) => void,
+) => {
+ let position = initialPosition;
+ let ratio = initialRatio;
+ let pointer: number | null = null;
+ const horizontal = () => position === "left" || position === "right";
+ const reversed = () => position === "right" || position === "bottom";
+ const update = () => {
+ ratio = clampRatio(ratio);
+ split.dataset.position = position;
+ split.style.setProperty("--code-share", `${ratio}fr`);
+ split.style.setProperty("--output-share", `${100 - ratio}fr`);
+ separator.setAttribute("aria-orientation", horizontal() ? "vertical" : "horizontal");
+ separator.setAttribute("aria-valuenow", String(Math.round(ratio)));
+ separator.setAttribute("aria-valuetext", `Code: ${Math.round(ratio)} %`);
+ };
+ const move = (event: PointerEvent) => {
+ if (pointer !== event.pointerId) return;
+ const bounds = split.getBoundingClientRect();
+ const offset = horizontal() ? event.clientX - bounds.left : event.clientY - bounds.top;
+ const size = horizontal() ? bounds.width : bounds.height;
+ const divider = horizontal() ? separator.offsetWidth : separator.offsetHeight;
+ ratio = ((offset - divider / 2) / Math.max(1, size - divider)) * 100;
+ if (reversed()) ratio = 100 - ratio;
+ update();
+ };
+ const end = () => {
+ if (pointer === null) return;
+ pointer = null;
+ split.classList.remove("is-resizing");
+ onChange(position, ratio);
+ };
+ const start = (event: PointerEvent) => {
+ if (event.button !== 0) return;
+ event.preventDefault();
+ pointer = event.pointerId;
+ separator.focus();
+ separator.setPointerCapture(pointer);
+ split.classList.add("is-resizing");
+ };
+ const keydown = (event: KeyboardEvent) => {
+ const less = horizontal() ? "ArrowLeft" : "ArrowUp";
+ const more = horizontal() ? "ArrowRight" : "ArrowDown";
+ if (![less, more, "Home", "End", "Enter"].includes(event.key)) return;
+ event.preventDefault();
+ if (event.key === "Home") ratio = 20;
+ else if (event.key === "End") ratio = 80;
+ else if (event.key === "Enter") ratio = 50;
+ else ratio += (event.key === more ? 1 : -1) * (reversed() ? -1 : 1) * (event.shiftKey ? 10 : 2);
+ update();
+ onChange(position, ratio);
+ };
+ const reset = () => {
+ ratio = 50;
+ update();
+ onChange(position, ratio);
+ };
+ separator.addEventListener("pointerdown", start);
+ separator.addEventListener("pointermove", move);
+ separator.addEventListener("pointerup", end);
+ separator.addEventListener("pointercancel", end);
+ separator.addEventListener("lostpointercapture", end);
+ separator.addEventListener("keydown", keydown);
+ separator.addEventListener("dblclick", reset);
+ update();
+ return {
+ setPosition(next: CodePosition, nextRatio = ratio) {
+ position = next;
+ ratio = nextRatio;
+ update();
+ onChange(position, ratio);
+ },
+ destroy() {
+ separator.removeEventListener("pointerdown", start);
+ separator.removeEventListener("pointermove", move);
+ separator.removeEventListener("pointerup", end);
+ separator.removeEventListener("pointercancel", end);
+ separator.removeEventListener("lostpointercapture", end);
+ separator.removeEventListener("keydown", keydown);
+ separator.removeEventListener("dblclick", reset);
+ },
+ };
+};
diff --git a/playground/src/workspace/state.ts b/playground/src/workspace/state.ts
new file mode 100644
index 0000000..f40e9e3
--- /dev/null
+++ b/playground/src/workspace/state.ts
@@ -0,0 +1,50 @@
+export type CodePosition = "left" | "right" | "top" | "bottom";
+export interface WorkspaceState {
+ code: string;
+ exampleId: string;
+ position: CodePosition;
+ ratio: number;
+}
+
+const STORAGE_KEY = "mlbundle-playground-workspace";
+export const isCodePosition = (value: unknown): value is CodePosition =>
+ value === "left" || value === "right" || value === "top" || value === "bottom";
+
+export const clampRatio = (ratio: number) => Math.max(20, Math.min(80, ratio));
+
+export const readWorkspaceState = (
+ storage: Pick | undefined,
+ fallback: WorkspaceState,
+): WorkspaceState => {
+ try {
+ const saved: unknown = JSON.parse(storage?.getItem(STORAGE_KEY) ?? "null");
+ if (!saved || typeof saved !== "object") return fallback;
+ return {
+ code: "code" in saved && typeof saved.code === "string" ? saved.code : fallback.code,
+ exampleId:
+ "exampleId" in saved && typeof saved.exampleId === "string"
+ ? saved.exampleId
+ : fallback.exampleId,
+ position:
+ "position" in saved && isCodePosition(saved.position) ? saved.position : fallback.position,
+ ratio:
+ "ratio" in saved && typeof saved.ratio === "number" && Number.isFinite(saved.ratio)
+ ? clampRatio(saved.ratio)
+ : fallback.ratio,
+ };
+ } catch {
+ return fallback;
+ }
+};
+
+export const saveWorkspaceState = (
+ storage: Pick | undefined,
+ state: WorkspaceState,
+): boolean => {
+ try {
+ storage?.setItem(STORAGE_KEY, JSON.stringify(state));
+ return storage !== undefined;
+ } catch {
+ return false;
+ }
+};
diff --git a/playground/src/workspace/status.ts b/playground/src/workspace/status.ts
new file mode 100644
index 0000000..dae56aa
--- /dev/null
+++ b/playground/src/workspace/status.ts
@@ -0,0 +1,15 @@
+import { icon, type Icon } from "../icons.ts";
+
+export const createStatus = (element: HTMLElement) => {
+ let label = "";
+ return {
+ get label() { return label; },
+ show(text: string, symbol: Icon = "ready") {
+ label = text;
+ element.innerHTML = `${icon(symbol)}`;
+ element.querySelector("span")!.textContent = text;
+ element.dataset.tooltip = text;
+ element.dataset.symbol = symbol;
+ },
+ };
+};
diff --git a/playground/src/workspace/viewer.ts b/playground/src/workspace/viewer.ts
new file mode 100644
index 0000000..24f558d
--- /dev/null
+++ b/playground/src/workspace/viewer.ts
@@ -0,0 +1,29 @@
+import { json } from "@codemirror/lang-json";
+import { EditorState } from "@codemirror/state";
+import { EditorView, lineNumbers } from "@codemirror/view";
+import { codeHighlighting } from "./highlight.ts";
+
+/** Read-only, focusable JSON view. */
+export const createJsonViewer = (parent: HTMLElement, label: string) => {
+ const view = new EditorView({
+ parent,
+ state: EditorState.create({
+ doc: "",
+ extensions: [
+ EditorState.readOnly.of(true),
+ EditorView.editable.of(false),
+ lineNumbers(),
+ json(),
+ codeHighlighting,
+ EditorView.contentAttributes.of({ "aria-label": label, tabindex: "0" }),
+ ],
+ }),
+ });
+ return {
+ show(text: string) {
+ view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text } });
+ view.scrollDOM.scrollTop = 0;
+ },
+ text: () => view.state.doc.toString(),
+ };
+};
diff --git a/playground/tests/e2e/playground.spec.ts b/playground/tests/e2e/playground.spec.ts
new file mode 100644
index 0000000..27f3344
--- /dev/null
+++ b/playground/tests/e2e/playground.spec.ts
@@ -0,0 +1,100 @@
+import { readFile } from "node:fs/promises";
+import { expect, test, type FrameLocator, type Page } from "@playwright/test";
+
+const frame = (page: Page) => page.frameLocator('iframe[title="Python editor"]');
+const editor = (ws: FrameLocator) => ws.getByRole("textbox", { name: "Python code" });
+const tab = (ws: FrameLocator, name: string) => ws.getByRole("tab", { name, exact: true });
+const panel = (ws: FrameLocator) => ws.getByRole("tabpanel");
+const chip = (ws: FrameLocator) => ws.locator(".inspector-chip");
+
+const replaceCode = async (page: Page, code: string) => {
+ await editor(frame(page)).click();
+ await page.keyboard.press("ControlOrMeta+a");
+ await page.keyboard.press("Delete");
+ // insertText keeps indentation exactly; typing would trigger auto-indent.
+ await page.keyboard.insertText(code);
+};
+
+test.beforeEach(async ({ page }) => {
+ await page.goto("/");
+ await page.evaluate(() => localStorage.clear());
+ await page.reload();
+});
+
+test("exports, verifies and visualizes the classifier example", async ({ page }) => {
+ const ws = frame(page);
+ await expect(chip(ws)).toHaveText("Verified");
+ await expect(page).toHaveTitle("MLbundle Playground");
+ await expect(page.getByRole("link", { name: "MLbundle home" })).toBeVisible();
+ await expect(ws.locator(".inspector-title")).toContainText("classifier.mlbundle");
+ await expect(panel(ws)).toContainText("DecisionTreeClassifier");
+ await expect(panel(ws)).toContainText("Emscripten");
+ // Narrow screens start with the code stacked above the inspector.
+ const narrow = page.viewportSize()!.width < 720;
+ await expect(ws.locator(".code-split")).toHaveAttribute("data-position", narrow ? "top" : "left");
+ await expect(ws.getByRole("combobox", { name: "Bundle" })).toBeHidden();
+
+ await tab(ws, "Structure").click();
+ await expect(panel(ws)).toContainText("Family: decision_tree");
+ await expect(ws.getByRole("list", { name: "Decision tree" })).toContainText("≤");
+ // Arrow keys move between tabs, as in the ARIA tabs pattern.
+ await tab(ws, "Structure").press("ArrowRight");
+ await expect(tab(ws, "Members")).toBeFocused();
+ await expect(panel(ws)).toContainText("model.skops");
+ await tab(ws, "Members").press("ArrowRight");
+ await expect(panel(ws)).toContainText("sample_rows=N");
+ await tab(ws, "Samples").press("ArrowLeft");
+ await tab(ws, "Members").press("Home");
+ await expect(tab(ws, "Overview")).toHaveAttribute("aria-selected", "true");
+ await tab(ws, "Schema").click();
+ await expect(panel(ws)).toContainText("Predicted class");
+ await tab(ws, "Manifest").click();
+ await expect(ws.getByRole("textbox", { name: "Manifest JSON" })).toContainText('"format_name": "mlbundle"');
+});
+
+test("downloads the written bundle and inspects uploads without loading them", async ({ page }) => {
+ const ws = frame(page);
+ await expect(chip(ws)).toHaveText("Verified");
+ const [download] = await Promise.all([
+ page.waitForEvent("download"),
+ ws.getByRole("button", { name: "Download .mlbundle" }).click(),
+ ]);
+ expect(download.suggestedFilename()).toBe("classifier.mlbundle");
+ const bytes = await readFile((await download.path())!);
+ expect([...bytes.subarray(0, 2)]).toEqual([0x50, 0x4b]);
+
+ const input = ws.locator('input[type="file"]');
+ await input.setInputFiles({ name: "uploaded.mlbundle", mimeType: "application/zip", buffer: bytes });
+ await expect(chip(ws)).toHaveText("Inspected only");
+ await expect(ws.locator(".inspector-title")).toContainText("uploaded.mlbundle");
+ await expect(ws.getByRole("button", { name: "Download .mlbundle" })).toBeHidden();
+ await tab(ws, "Structure").click();
+ await expect(panel(ws)).toContainText("without deserializing the model");
+
+ await input.setInputFiles({ name: "notes.mlbundle", mimeType: "application/zip", buffer: Buffer.from("hello") });
+ await expect(chip(ws)).toHaveText("Rejected");
+ await tab(ws, "Overview").click();
+ await expect(panel(ws)).toContainText("InvalidContainerError");
+});
+
+test("runs edited code, reports errors and keeps examples resettable", async ({ page }) => {
+ const ws = frame(page);
+ await expect(chip(ws)).toHaveText("Verified");
+ await replaceCode(page, "from mlbundle import export\nprint('about to fail')\nexport(object(), 'x.mlbundle')");
+ await page.keyboard.press("ControlOrMeta+Enter");
+ await expect(tab(ws, "Console")).toHaveAttribute("aria-selected", "true");
+ await expect(panel(ws)).toContainText("UnsupportedModelError");
+ await expect(panel(ws)).toContainText("about to fail");
+
+ await ws.getByRole("combobox", { name: "Example" }).selectOption("regressor");
+ await expect(ws.locator(".inspector-title")).toContainText("regressor.mlbundle");
+ await tab(ws, "Structure").click();
+ await expect(ws.getByRole("combobox", { name: "Tree" })).toHaveValue("0");
+ await ws.getByRole("combobox", { name: "Tree" }).selectOption("2");
+ await expect(panel(ws)).toContainText("Feature importances");
+
+ await replaceCode(page, "1 + 1");
+ await ws.getByRole("button", { name: "Reset example" }).click();
+ await expect(editor(ws)).toContainText("RandomForestRegressor");
+ await expect(chip(ws)).toHaveText("Verified");
+});
diff --git a/playground/tests/runtime/examples.test.ts b/playground/tests/runtime/examples.test.ts
new file mode 100644
index 0000000..23b4045
--- /dev/null
+++ b/playground/tests/runtime/examples.test.ts
@@ -0,0 +1,151 @@
+import assert from "node:assert/strict";
+import { readFileSync, readdirSync } from "node:fs";
+import { join } from "node:path";
+import { before, test } from "node:test";
+import type { BundleReport, Json, RunReport } from "../../src/runtime/protocol.ts";
+import { projectVersion, type PythonSession } from "../../src/runtime/session.ts";
+import { bootNodeSession, examplesDir, readExample, repoRoot } from "./load.ts";
+
+type Obj = { [key: string]: Json };
+let session: PythonSession;
+before(async () => {
+ session = await bootNodeSession();
+});
+
+const runExample = async (file: string): Promise => {
+ const report = await session.run(readExample(file));
+ assert.notEqual(report.outcome.kind, "error", JSON.stringify(report.outcome));
+ return report;
+};
+const only = (report: RunReport): BundleReport => {
+ assert.equal(report.bundles.length, 1);
+ const bundle = report.bundles[0]!;
+ assert.equal(bundle.status, "verified", bundle.error ?? "");
+ assert.equal(bundle.source, "generated");
+ return bundle;
+};
+const value = (report: RunReport): unknown => {
+ assert.equal(report.outcome.kind, "json", JSON.stringify(report.outcome));
+ return JSON.parse((report.outcome as { text: string }).text);
+};
+const model = (bundle: BundleReport) => bundle.manifest!.model as Obj;
+const names = (bundle: BundleReport) => bundle.members!.map((member) => member.name);
+
+test("the runtime runs the repository's mlbundle on the pinned wheels", () => {
+ assert.equal(session.versions.python.split(".").slice(0, 2).join("."), "3.14");
+ assert.equal(session.versions.mlbundle, projectVersion(readFileSync(join(repoRoot, "pyproject.toml"), "utf8")));
+ assert.equal(session.versions.mlschema, "0.3.0");
+ assert.equal(session.versions.skops, "0.16.0");
+ assert.match(session.versions["scikit-learn"], /^1\.8\./);
+});
+
+test("the example catalog is complete", () => {
+ const files = readdirSync(examplesDir).filter((file) => file.endsWith(".py"));
+ assert.deepEqual(files.sort(), [
+ "classifier.py",
+ "errors.py",
+ "pipeline.py",
+ "regressor.py",
+ "samples.py",
+ ]);
+});
+
+test("the classifier example writes a verified bundle with a tree", async () => {
+ const report = await runExample("classifier.py");
+ const bundle = only(report);
+ assert.deepEqual(value(report), [["cold", "warm"]]);
+ assert.equal(model(bundle).estimator_class, "DecisionTreeClassifier");
+ assert.equal(model(bundle).kind, "classifier");
+ assert.deepEqual(names(bundle), ["manifest.json", "model.skops", "schema.json", "structure.json"]);
+ for (const member of bundle.members!.slice(1)) assert.match(member.sha256 ?? "", /^[0-9a-f]{64}$/);
+ assert.equal(bundle.structure!.family, "decision_tree");
+ assert.equal(bundle.samples, null);
+ // The download is the archive mlbundle wrote: a ZIP of the reported size.
+ assert.equal(bundle.bytes!.length, bundle.sizeBytes);
+ assert.deepEqual([...bundle.bytes!.subarray(0, 4)], [0x50, 0x4b, 0x03, 0x04]);
+ const environment = bundle.manifest!.environment as Obj;
+ assert.equal(environment.os, "Emscripten");
+ assert.equal((environment.packages as Obj)["scikit-learn"], session.versions["scikit-learn"]);
+});
+
+test("the regressor example records one tree per estimator", async () => {
+ const bundle = only(await runExample("regressor.py"));
+ assert.equal(model(bundle).kind, "regressor");
+ assert.equal(bundle.structure!.family, "tree_ensemble");
+ assert.equal((bundle.structure!.trees as Json[]).length, 3);
+ assert.deepEqual((bundle.manifest!.features as Obj).names, ["area_m2", "rooms", "floor"]);
+});
+
+test("samples appear only when requested and one-hot columns form one field", async () => {
+ const report = await runExample("samples.py");
+ const bundle = only(report);
+ assert.deepEqual(value(report), ["number", "onehot-category", "boolean"]);
+ assert.equal(bundle.samples!.length, 3);
+ assert.ok(names(bundle).includes("samples.json"));
+ assert.equal(bundle.structure!.family, "linear");
+});
+
+test("an all-sklearn pipeline is verified with null structure", async () => {
+ const bundle = only(await runExample("pipeline.py"));
+ assert.equal(bundle.structure, null);
+ assert.ok(!names(bundle).includes("structure.json"));
+ assert.equal((bundle.schema as Obj).source, "default");
+});
+
+test("rejections happen at export and write nothing", async () => {
+ const report = await runExample("errors.py");
+ assert.equal(report.bundles.length, 0);
+ const results = value(report) as Obj[];
+ assert.deepEqual(
+ results.map((entry) => [entry.case, entry.error]),
+ [
+ ["custom subclass", "UnsupportedModelError"],
+ ["HistGradientBoosting", "UnsupportedModelError"],
+ ["samples without a frame", "InvalidSamplesError"],
+ ],
+ );
+});
+
+test("an uploaded bundle is inspected without loading its model", async () => {
+ const generated = only(await runExample("classifier.py"));
+ const report = await session.inspect("my model.mlbundle", generated.bytes!.slice());
+ const bundle = report.bundles[0]!;
+ assert.equal(bundle.status, "inspected");
+ assert.equal(bundle.source, "uploaded");
+ assert.equal(bundle.fileName, "my model.mlbundle");
+ assert.equal(bundle.structure, null);
+ assert.deepEqual(bundle.manifest, generated.manifest);
+ assert.deepEqual(bundle.schema, generated.schema);
+ assert.equal(bundle.bytes, undefined);
+});
+
+test("damaged and non-bundle uploads are rejected by mlbundle.inspect", async () => {
+ const generated = only(await runExample("classifier.py"));
+ const tampered = generated.bytes!.slice();
+ const middle = Math.floor(tampered.length / 2);
+ tampered[middle] = tampered[middle]! ^ 0xff;
+ const damaged = (await session.inspect("damaged.mlbundle", tampered)).bundles[0]!;
+ assert.equal(damaged.status, "rejected");
+ assert.match(damaged.error!, /Error: /);
+ const notes = new TextEncoder().encode("hello");
+ const text = (await session.inspect("notes.mlbundle", notes)).bundles[0]!;
+ assert.equal(text.status, "rejected");
+ assert.match(text.error!, /^InvalidContainerError/);
+});
+
+test("user edits run as written in a fresh namespace and directory", async () => {
+ const failed = await session.run("from mlbundle import export\nexport(object(), 'x.mlbundle')");
+ assert.equal(failed.outcome.kind, "error");
+ const traceback = (failed.outcome as { text: string }).text;
+ assert.match(traceback, /File "", line 2[\s\S]*UnsupportedModelError/);
+ assert.equal(failed.bundles.length, 0);
+
+ const stray = await session.run("open('stray.mlbundle', 'wb').write(b'not a zip')\nleftover = 1");
+ assert.equal(stray.bundles[0]!.status, "rejected");
+ assert.equal(stray.bundles[0]!.bytes, undefined);
+ const next = await session.run("import os\nsorted(os.listdir('.'))");
+ assert.deepEqual(next.outcome, { kind: "json", text: "[]" });
+ assert.deepEqual(next.bundles, []);
+ const name = await session.run("leftover");
+ assert.match((name.outcome as { text: string }).text, /NameError/);
+});
diff --git a/playground/tests/runtime/load.ts b/playground/tests/runtime/load.ts
new file mode 100644
index 0000000..a8cf690
--- /dev/null
+++ b/playground/tests/runtime/load.ts
@@ -0,0 +1,40 @@
+import { readFileSync, readdirSync } from "node:fs";
+import { join, relative } from "node:path";
+import { fileURLToPath } from "node:url";
+import { loadPyodide } from "pyodide";
+import { startSession, type PythonSession, type SourceTree } from "../../src/runtime/session.ts";
+
+const here = fileURLToPath(new URL(".", import.meta.url));
+export const repoRoot = join(here, "../../..");
+export const examplesDir = join(here, "../../src/examples");
+
+const packageFiles = (dir: string): string[] =>
+ readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
+ const path = join(dir, entry.name);
+ if (entry.isDirectory()) return entry.name === "__pycache__" ? [] : packageFiles(path);
+ return /\.(py|json)$/.test(entry.name) ? [path] : [];
+ });
+
+/** Same tree the Vite build bundles: every .py and .json file under src/mlbundle. */
+export const readSources = (): SourceTree => {
+ const srcRoot = join(repoRoot, "src");
+ return Object.fromEntries(
+ packageFiles(join(srcRoot, "mlbundle")).map((path) => [
+ relative(srcRoot, path).replaceAll("\\", "/"),
+ readFileSync(path, "utf8"),
+ ]),
+ );
+};
+
+export const readExample = (file: string) => readFileSync(join(examplesDir, file), "utf8");
+
+/** Boots real Pyodide with the pinned wheels; downloads from jsDelivr and PyPI. */
+export const bootNodeSession = async (): Promise => {
+ const pyodide = await loadPyodide({ stdout: () => {}, stderr: () => {} });
+ return startSession(
+ pyodide,
+ readSources(),
+ readFileSync(join(repoRoot, "pyproject.toml"), "utf8"),
+ readFileSync(join(here, "../../src/runtime/harness.py"), "utf8"),
+ );
+};
diff --git a/playground/tests/unit/client.test.ts b/playground/tests/unit/client.test.ts
new file mode 100644
index 0000000..9a67172
--- /dev/null
+++ b/playground/tests/unit/client.test.ts
@@ -0,0 +1,203 @@
+import assert from "node:assert/strict";
+import { test } from "node:test";
+import {
+ createPythonRuntime,
+ type RuntimeState,
+ type WorkerLike,
+} from "../../src/runtime/client.ts";
+import type { FromWorker, RunReport, ToWorker } from "../../src/runtime/protocol.ts";
+
+const VERSIONS = {
+ python: "3.14.2",
+ mlbundle: "0.2.0",
+ mlschema: "0.3.0",
+ "scikit-learn": "1.8.0",
+ skops: "0.16.0",
+ pandas: "3.0.2",
+ numpy: "2.4.6",
+};
+const NONE: RunReport = { outcome: { kind: "none" }, bundles: [], skipped: 0 };
+const JSON_REPORT: RunReport = { outcome: { kind: "json", text: "[]" }, bundles: [], skipped: 0 };
+const LIMITS = { bootMs: 40, runMs: 40 };
+
+class FakeWorker implements WorkerLike {
+ onmessage: WorkerLike["onmessage"] = null;
+ onerror: WorkerLike["onerror"] = null;
+ sent: ToWorker[] = [];
+ transfers: Transferable[][] = [];
+ terminated = false;
+ failPost = false;
+ postMessage(message: ToWorker, transfer: Transferable[] = []) {
+ if (this.failPost) throw new Error("could not clone message");
+ this.sent.push(message);
+ this.transfers.push(transfer);
+ }
+ terminate() {
+ this.terminated = true;
+ }
+ emit(data: FromWorker) {
+ this.onmessage?.({ data } as MessageEvent);
+ }
+ crash(message: string) {
+ this.onerror?.({ message, preventDefault() {} } as ErrorEvent);
+ }
+}
+
+const setup = (limits = LIMITS) => {
+ const workers: FakeWorker[] = [];
+ const states: RuntimeState[] = [];
+ const streams: string[] = [];
+ const runtime = createPythonRuntime(
+ () => {
+ const worker = new FakeWorker();
+ workers.push(worker);
+ return worker;
+ },
+ { onState: (state) => states.push(state), onStream: (_, text) => streams.push(text) },
+ limits,
+ );
+ const latest = () => workers.at(-1)!;
+ return { runtime, workers, states, streams, latest };
+};
+
+const ready = () => {
+ const context = setup();
+ context.latest().emit({ type: "ready", versions: VERSIONS });
+ return context;
+};
+
+test("boots, reports progress and becomes ready", () => {
+ const { runtime, latest, states } = setup();
+ latest().emit({ type: "progress", message: "Loading scikit-learn and pandas…" });
+ latest().emit({ type: "ready", versions: VERSIONS });
+ assert.deepEqual(
+ states.map((state) => state.status),
+ ["booting", "booting", "ready"],
+ );
+ assert.deepEqual(runtime.state, { status: "ready", versions: VERSIONS });
+ runtime.dispose();
+});
+
+test("runs code, forwards its streams and ignores stale run ids", async () => {
+ const { runtime, latest, streams } = ready();
+ const result = runtime.run("print(1)");
+ assert.equal(runtime.state.status, "running");
+ const { id } = latest().sent[0]!;
+ latest().emit({ type: "stream", id: id + 1, stream: "stdout", text: "stale\n" });
+ latest().emit({ type: "stream", id, stream: "stdout", text: "1\n" });
+ latest().emit({ type: "done", id: id + 1, report: NONE, durationMs: 1 });
+ latest().emit({ type: "done", id, report: JSON_REPORT, durationMs: 3 });
+ assert.deepEqual(await result, { kind: "finished", report: JSON_REPORT, durationMs: 3 });
+ assert.deepEqual(latest().sent[0], { type: "run", id, code: "print(1)" });
+ assert.deepEqual(streams, ["1\n"]);
+ assert.equal(runtime.state.status, "ready");
+ runtime.dispose();
+});
+
+test("inspects an upload by transferring its bytes and returns the report", async () => {
+ const { runtime, latest } = ready();
+ const data = new Uint8Array([0x50, 0x4b, 3, 4]).buffer;
+ const result = runtime.inspect("model.mlbundle", data);
+ assert.equal(runtime.state.status, "running");
+ const sent = latest().sent[0]!;
+ assert.equal(sent.type, "inspect");
+ assert.equal(sent.type === "inspect" && sent.name, "model.mlbundle");
+ assert.deepEqual(latest().transfers[0], [data], "the buffer is transferred, not copied");
+ const report: RunReport = {
+ outcome: { kind: "none" },
+ skipped: 0,
+ bundles: [
+ { fileName: "model.mlbundle", sizeBytes: 4, source: "uploaded", status: "rejected", error: "InvalidContainerError: x" },
+ ],
+ };
+ latest().emit({ type: "done", id: sent.id, report, durationMs: 2 });
+ assert.deepEqual(await result, { kind: "finished", report, durationMs: 2 });
+ assert.equal(runtime.state.status, "ready");
+ const blocked = runtime.inspect("again.mlbundle", new ArrayBuffer(1));
+ assert.deepEqual(await runtime.run("1"), { kind: "unavailable" }, "one request at a time");
+ latest().emit({ type: "done", id: latest().sent[1]!.id, report: NONE, durationMs: 1 });
+ assert.equal((await blocked).kind, "finished");
+ runtime.dispose();
+});
+
+test("refuses to run before Python is ready", async () => {
+ const { runtime, latest } = setup();
+ assert.deepEqual(await runtime.run("1"), { kind: "unavailable" });
+ assert.equal(latest().sent.length, 0);
+ runtime.dispose();
+});
+
+test("a run past the time limit terminates the worker and boots a new one", async () => {
+ const { runtime, workers } = ready();
+ assert.deepEqual(await runtime.run("while True: pass"), { kind: "timeout", limitMs: 40 });
+ assert.equal(workers[0]!.terminated, true);
+ assert.equal(workers.length, 2);
+ assert.equal(runtime.state.status, "booting");
+ runtime.dispose();
+});
+
+test("cancel stops the run by restarting Python", async () => {
+ const { runtime, workers } = ready();
+ const result = runtime.run("while True: pass");
+ runtime.cancel();
+ assert.deepEqual(await result, { kind: "cancelled" });
+ assert.equal(workers[0]!.terminated, true);
+ assert.equal(workers.length, 2);
+ runtime.cancel();
+ assert.equal(workers.length, 2, "cancel without a run does nothing");
+ runtime.dispose();
+});
+
+test("a worker crash during a run settles it as failed at once", async () => {
+ const { runtime, workers, latest } = ready();
+ const result = runtime.run("1");
+ latest().crash("out of memory");
+ const message = "Python stopped unexpectedly: out of memory";
+ assert.deepEqual(await result, { kind: "failed", message });
+ assert.deepEqual(runtime.state, { status: "failed", message });
+ assert.equal(workers[0]!.terminated, true);
+ await new Promise((resolve) => setTimeout(resolve, LIMITS.runMs * 2));
+ assert.equal(workers.length, 1, "no restart until the visitor asks");
+ runtime.restart();
+ assert.equal(workers.length, 2);
+ runtime.dispose();
+});
+
+test("boot errors and boot timeouts leave the runtime failed", async () => {
+ const errored = setup();
+ errored.latest().emit({ type: "boot-error", message: "network down" });
+ assert.deepEqual(errored.runtime.state, {
+ status: "failed",
+ message: "Python could not start: network down",
+ });
+ const slow = setup();
+ await new Promise((resolve) => setTimeout(resolve, LIMITS.bootMs * 2));
+ assert.deepEqual(slow.runtime.state, {
+ status: "failed",
+ message: "Python could not start: no response within 0.04 seconds.",
+ });
+ assert.equal(slow.latest().terminated, true);
+});
+
+test("a worker that cannot be created or messaged fails explicitly", async () => {
+ const runtime = createPythonRuntime(
+ () => {
+ throw new Error("Worker is not supported");
+ },
+ { onState() {}, onStream() {} },
+ LIMITS,
+ );
+ assert.deepEqual(runtime.state, {
+ status: "failed",
+ message: "Python could not start: Worker is not supported",
+ });
+ assert.deepEqual(await runtime.run("1"), { kind: "unavailable" });
+
+ const { runtime: posting, latest } = ready();
+ latest().failPost = true;
+ assert.deepEqual(await posting.run("1"), {
+ kind: "failed",
+ message: "Python stopped unexpectedly: could not clone message",
+ });
+ assert.equal(posting.state.status, "failed");
+});
diff --git a/playground/tests/unit/config.test.ts b/playground/tests/unit/config.test.ts
new file mode 100644
index 0000000..18b1e5f
--- /dev/null
+++ b/playground/tests/unit/config.test.ts
@@ -0,0 +1,64 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { test } from "node:test";
+import { projectVersion } from "../../src/runtime/session.ts";
+import { PYODIDE_VERSION, PYPI_PINS } from "../../src/runtime/versions.ts";
+import { readWorkspaceState, saveWorkspaceState } from "../../src/workspace/state.ts";
+
+const FALLBACK = { code: "x", exampleId: "classifier", position: "left", ratio: 50 } as const;
+const read = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8");
+
+test("the CDN loader version matches the installed pyodide package", () => {
+ const installed = JSON.parse(read("../../node_modules/pyodide/package.json")) as { version: string };
+ assert.equal(PYODIDE_VERSION, installed.version);
+});
+
+test("pure-Python pins match the repository lock", () => {
+ const lock = read("../../../uv.lock");
+ for (const pin of PYPI_PINS) {
+ const [name, version] = pin.split("==") as [string, string];
+ assert.match(lock, new RegExp(`name = "${name}"\nversion = "${version.replaceAll(".", "\\.")}"`), pin);
+ }
+});
+
+test("the bundled package reports the version pyproject.toml declares", () => {
+ const pyproject = read("../../../pyproject.toml");
+ const declared = /^version = "([^"]+)"/m.exec(pyproject)![1];
+ assert.equal(projectVersion(pyproject), declared);
+ assert.equal(projectVersion('[tool.x]\nversion = "9"\n[project]\nname = "a"\nversion = "1.2"\n'), "1.2");
+ assert.throws(() => projectVersion('[tool.x]\nversion = "9"\n'), /no \[project\] version/);
+});
+
+test("workspace state restores valid fields and falls back on invalid ones", () => {
+ const storage = new Map();
+ const backend = {
+ getItem: (key: string) => storage.get(key) ?? null,
+ setItem: (key: string, value: string) => void storage.set(key, value),
+ };
+ assert.deepEqual(readWorkspaceState(backend, FALLBACK), FALLBACK);
+ assert.equal(
+ saveWorkspaceState(backend, { code: "y", exampleId: "samples", position: "top", ratio: 95 }),
+ true,
+ );
+ assert.deepEqual(readWorkspaceState(backend, FALLBACK), {
+ code: "y",
+ exampleId: "samples",
+ position: "top",
+ ratio: 80,
+ });
+ assert.equal([...storage.keys()][0], "mlbundle-playground-workspace");
+ storage.set([...storage.keys()][0]!, '{"position":"diagonal","ratio":"wide"}');
+ assert.deepEqual(readWorkspaceState(backend, FALLBACK), FALLBACK);
+ storage.set([...storage.keys()][0]!, "{not json");
+ assert.deepEqual(readWorkspaceState(backend, FALLBACK), FALLBACK);
+});
+
+test("saving reports unavailable storage", () => {
+ const full = {
+ setItem() {
+ throw new Error("QuotaExceededError");
+ },
+ };
+ assert.equal(saveWorkspaceState(full, FALLBACK), false);
+ assert.equal(saveWorkspaceState(undefined, FALLBACK), false);
+});
diff --git a/playground/tests/unit/settings.test.ts b/playground/tests/unit/settings.test.ts
new file mode 100644
index 0000000..80135f0
--- /dev/null
+++ b/playground/tests/unit/settings.test.ts
@@ -0,0 +1,104 @@
+import assert from "node:assert/strict";
+import { test } from "node:test";
+import {
+ APPEARANCES,
+ DEFAULT_SETTINGS,
+ readSettings,
+ saveSettings,
+ SETTINGS_KEY,
+ STYLES,
+ THEMES,
+ VIEWPORTS,
+ viewportSize,
+ type Settings,
+} from "../../src/settings.ts";
+
+const memory = (initial?: string) => {
+ const items = new Map(initial === undefined ? [] : [[SETTINGS_KEY, initial]]);
+ return {
+ items,
+ getItem: (key: string) => items.get(key) ?? null,
+ setItem: (key: string, value: string) => void items.set(key, value),
+ };
+};
+const CHOSEN: Settings = {
+ theme: "cobalt",
+ style: "contrast",
+ appearance: "dark",
+ viewport: "custom",
+ width: 500,
+ height: 700,
+};
+
+test("catalogs offer MLForm's choices with unique ids", () => {
+ assert.deepEqual(
+ THEMES.map(({ id }) => id),
+ ["eshkra", "m3diseen", "dosetailor", "clickhouse", "airbnb", "neutral", "cobalt", "sage", "sunset", "graphite"],
+ );
+ assert.deepEqual(STYLES.map(({ label }) => label), ["Default", "Minimal", "Soft", "Contrast"]);
+ assert.deepEqual(APPEARANCES.map(({ label }) => label), ["System", "Light", "Dark"]);
+ for (const items of [THEMES, STYLES, APPEARANCES, VIEWPORTS]) {
+ assert.equal(new Set(items.map(({ id }) => id)).size, items.length);
+ }
+});
+
+test("saved settings round-trip", () => {
+ const storage = memory();
+ assert.deepEqual(readSettings(storage), DEFAULT_SETTINGS);
+ assert.equal(saveSettings(storage, CHOSEN), true);
+ assert.deepEqual(readSettings(storage), CHOSEN);
+});
+
+test("each invalid field falls back alone", () => {
+ const saved = { ...CHOSEN, theme: "unknown", style: 3, viewport: "watch", width: 0 };
+ assert.deepEqual(readSettings(memory(JSON.stringify(saved))), {
+ ...CHOSEN,
+ theme: DEFAULT_SETTINGS.theme,
+ style: DEFAULT_SETTINGS.style,
+ viewport: DEFAULT_SETTINGS.viewport,
+ width: DEFAULT_SETTINGS.width,
+ });
+ for (const size of [8193, 1.5, -1, "640", null]) {
+ assert.equal(readSettings(memory(JSON.stringify({ height: size }))).height, DEFAULT_SETTINGS.height);
+ }
+ assert.equal(readSettings(memory(JSON.stringify({ width: 8192 }))).width, 8192);
+ assert.equal(readSettings(memory(JSON.stringify({ appearance: "sepia" }))).appearance, "auto");
+});
+
+test("unreadable storage yields the defaults", () => {
+ for (const raw of ["{not json", "[1, 2]", "42", "null"]) {
+ assert.deepEqual(readSettings(memory(raw)), DEFAULT_SETTINGS);
+ }
+ const blocked = {
+ getItem(): string {
+ throw new Error("SecurityError");
+ },
+ };
+ assert.deepEqual(readSettings(blocked), DEFAULT_SETTINGS);
+ assert.deepEqual(readSettings(undefined), DEFAULT_SETTINGS);
+});
+
+test("saving reports unavailable storage", () => {
+ const full = {
+ setItem() {
+ throw new Error("QuotaExceededError");
+ },
+ };
+ assert.equal(saveSettings(full, CHOSEN), false);
+ assert.equal(saveSettings(undefined, CHOSEN), false);
+});
+
+test("viewport size follows the preset or the custom size", () => {
+ assert.equal(viewportSize(DEFAULT_SETTINGS), null);
+ assert.deepEqual(viewportSize({ ...CHOSEN, viewport: "phone" }), {
+ width: 390,
+ height: 844,
+ name: "Phone",
+ });
+ assert.deepEqual(viewportSize({ ...CHOSEN, viewport: "ultrawide" }), {
+ width: 2560,
+ height: 1080,
+ name: "Ultrawide",
+ });
+ assert.deepEqual(viewportSize(CHOSEN), { width: 500, height: 700, name: "Custom" });
+});
diff --git a/playground/tsconfig.json b/playground/tsconfig.json
new file mode 100644
index 0000000..0dc1a5f
--- /dev/null
+++ b/playground/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "lib": ["ES2023", "DOM", "DOM.Iterable", "WebWorker.ImportScripts"],
+ "types": ["node", "vite/client"],
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "exactOptionalPropertyTypes": true,
+ "verbatimModuleSyntax": true,
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "skipLibCheck": true,
+ "noEmit": true
+ },
+ "include": ["src/**/*.ts", "tests/**/*.ts", "*.ts"]
+}
diff --git a/playground/vite.config.ts b/playground/vite.config.ts
new file mode 100644
index 0000000..74e0eed
--- /dev/null
+++ b/playground/vite.config.ts
@@ -0,0 +1,11 @@
+import { fileURLToPath } from "node:url";
+import { defineConfig } from "vite-plus";
+
+const repoRoot = fileURLToPath(new URL("..", import.meta.url));
+
+export default defineConfig({
+ // The worker bundles ../src/mlbundle and ../pyproject.toml; the header uses ../brand.
+ server: { fs: { allow: [repoRoot] } },
+ worker: { format: "es" },
+ build: { target: "es2022" },
+});
diff --git a/playground/wrangler.jsonc b/playground/wrangler.jsonc
new file mode 100644
index 0000000..e428612
--- /dev/null
+++ b/playground/wrangler.jsonc
@@ -0,0 +1,12 @@
+{
+ "name": "mlbundle-playground",
+ "compatibility_date": "2026-10-19",
+ "workers_dev": false,
+ "preview_urls": false,
+ "assets": {
+ "directory": "./dist",
+ },
+ "routes": [
+ { "pattern": "playground.mlbundle.eshkra.dev", "custom_domain": true },
+ ],
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..e2d91b2
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,6106 @@
+---
+lockfileVersion: '9.0'
+
+importers:
+
+ .:
+ configDependencies: {}
+ packageManagerDependencies:
+ pnpm:
+ specifier: 12.14.0
+ version: 12.14.0
+
+packages:
+
+ '@pnpm/exe.android-arm64@12.14.0':
+ resolution: {integrity: sha512-+iSh7x21C3A2hqc+Z0ZmbKTcn6YNlzrXM142PsnQH/dLj0DYxFzoBvc6XAnL/yfQ5nnH2ebBiRj/zVxr61fZbQ==}
+ cpu: [arm64]
+ os: [android]
+
+ '@pnpm/exe.android-x64@12.14.0':
+ resolution: {integrity: sha512-qT+0LqEr2wBVaxcG2T8cX/R8SSRz2Rvpp8nRR4fh8tnGUZRch87gFmsHUAoSQh7w6j1h0qTjrA384j4xi8frvw==}
+ cpu: [x64]
+ os: [android]
+
+ '@pnpm/exe.darwin-arm64@12.14.0':
+ resolution: {integrity: sha512-tnrdUB8ecJoehuKKqi1YHCBVl61xtJ9XVGcx5EviC6fWO4DeGI8GtXrjvJxDBM3nuVjJeWistgsUp/N142x4sw==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@pnpm/exe.darwin-x64@12.14.0':
+ resolution: {integrity: sha512-vlBfGkruqQRsEHTW43cPwpYd7QlbIJ//cvpAdb3bVXkI8SgTPoXhCnc05m+ZNiPrrEkBsX/t7KHw9aXpkzPm3Q==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@pnpm/exe.freebsd-x64@12.14.0':
+ resolution: {integrity: sha512-Qg5rxBrGYl6Tfso+EyygPf84fY1Fzy55E/0or+iuYKtnf0vc0MlHJfVIlT7pBukMRntQHRik7hUi0Z4s7AlJsw==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@pnpm/exe.linux-arm64-musl@12.14.0':
+ resolution: {integrity: sha512-DRIe4yQkXBGA/PhVJdzlbnllxBdAtnMMvTVLLcGpsK5FvJz9kxD2Hyxtn68ovmNkVX06f6p45cQwfm3wx2jn1A==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@pnpm/exe.linux-arm64@12.14.0':
+ resolution: {integrity: sha512-7joGShZ44Nk3WpNU+60JoOPjCXlgxhJAq11S2UiVtwCUDgTer1yTPPfxGW8pxaD6046actYLIYDbZNlubHWs0A==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@pnpm/exe.linux-ppc64@12.14.0':
+ resolution: {integrity: sha512-gSoryn7ySdXd//gjgf3MvvA8ZerJayaSbuMZMtAWq4vb2/wlVEgpl0Kvqt54y58TRj533Qjq2txKgnxd4JyoAQ==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@pnpm/exe.linux-riscv64@12.14.0':
+ resolution: {integrity: sha512-EMjew1H9WqQ7pL+tYqCPwiuB+0PeCiJRV7fyWeuFw0wwYUajFBjcaPO3IGAVu9sdmJDU+e+HYv/Zey6aKae0Vw==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@pnpm/exe.linux-s390x@12.14.0':
+ resolution: {integrity: sha512-shYOj/SFoXD35fHe0pWr2JofwSXulC0BnEIXLj2QokDi0EF//AfKsui35yKIkTEl+s2VJybc3PO4iJNhKIj5Qw==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@pnpm/exe.linux-x64-musl@12.14.0':
+ resolution: {integrity: sha512-btZiZ2Ot0llKK1ndSfA5i3IRX7/S0VrmzseNH/BJ4c+Cady+S81H6vvU1zByggHMyqQhC8OgxO5PiIY7D9bH0Q==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@pnpm/exe.linux-x64@12.14.0':
+ resolution: {integrity: sha512-F6K8e/jWxiuEYJaQtRRUG9ynBSTEHa+t7Lf86MKuOO5UA7WVVMbi+oB8nY6iKW++ncWS2yG6T9faXZGxqNsfoQ==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@pnpm/exe.win32-arm64@12.14.0':
+ resolution: {integrity: sha512-DOjlkVB6TtTKeAAe0hNZXn5S7YqESmo1Z6SGbR4BZNQ4isRg31XVHjX2RFTbY8gnZq68IwzyLmPqlEjMmYxofg==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@pnpm/exe.win32-x64@12.14.0':
+ resolution: {integrity: sha512-/LSX6mew/sLf08XqcjemuklNKf1K/Ftpgj4//hK/HKzI2dKbmxDB27a/2OaQ5a0O3/3OFzlssXDziZsi+yVBwQ==}
+ cpu: [x64]
+ os: [win32]
+
+ pnpm@12.14.0:
+ resolution: {integrity: sha512-wYum2HmT09Fky16JSPMrLAbuhUSH4GzY2GeBNC+z5btGNDZwNHEehoArqpw3vqkGzhkprQjXCITJfYAd8TOJpg==}
+ engines: {node: '>=18.*'}
+ hasBin: true
+
+snapshots:
+
+ '@pnpm/exe.android-arm64@12.14.0':
+ optional: true
+
+ '@pnpm/exe.android-x64@12.14.0':
+ optional: true
+
+ '@pnpm/exe.darwin-arm64@12.14.0':
+ optional: true
+
+ '@pnpm/exe.darwin-x64@12.14.0':
+ optional: true
+
+ '@pnpm/exe.freebsd-x64@12.14.0':
+ optional: true
+
+ '@pnpm/exe.linux-arm64-musl@12.14.0':
+ optional: true
+
+ '@pnpm/exe.linux-arm64@12.14.0':
+ optional: true
+
+ '@pnpm/exe.linux-ppc64@12.14.0':
+ optional: true
+
+ '@pnpm/exe.linux-riscv64@12.14.0':
+ optional: true
+
+ '@pnpm/exe.linux-s390x@12.14.0':
+ optional: true
+
+ '@pnpm/exe.linux-x64-musl@12.14.0':
+ optional: true
+
+ '@pnpm/exe.linux-x64@12.14.0':
+ optional: true
+
+ '@pnpm/exe.win32-arm64@12.14.0':
+ optional: true
+
+ '@pnpm/exe.win32-x64@12.14.0':
+ optional: true
+
+ pnpm@12.14.0:
+ optionalDependencies:
+ '@pnpm/exe.android-arm64': 12.14.0
+ '@pnpm/exe.android-x64': 12.14.0
+ '@pnpm/exe.darwin-arm64': 12.14.0
+ '@pnpm/exe.darwin-x64': 12.14.0
+ '@pnpm/exe.freebsd-x64': 12.14.0
+ '@pnpm/exe.linux-arm64': 12.14.0
+ '@pnpm/exe.linux-arm64-musl': 12.14.0
+ '@pnpm/exe.linux-ppc64': 12.14.0
+ '@pnpm/exe.linux-riscv64': 12.14.0
+ '@pnpm/exe.linux-s390x': 12.14.0
+ '@pnpm/exe.linux-x64': 12.14.0
+ '@pnpm/exe.linux-x64-musl': 12.14.0
+ '@pnpm/exe.win32-arm64': 12.14.0
+ '@pnpm/exe.win32-x64': 12.14.0
+
+---
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+overrides:
+ vite: npm:@voidzero-dev/vite-plus-core@1.3.0
+
+importers:
+
+ .:
+ devDependencies:
+ vite-plus:
+ specifier: 1.3.0
+ version: 1.3.0(@astrojs/compiler-rs@0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3))(@types/node@26.6.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(playwright@1.64.1)(typescript@6.0.3)(yaml@2.9.1)
+ wrangler:
+ specifier: 4.151.1
+ version: 4.151.1(@types/node@26.6.5)
+
+ docs:
+ dependencies:
+ '@astrojs/starlight':
+ specifier: 0.42.1
+ version: 0.42.1(astro@7.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(typescript@6.0.3)(yaml@2.9.1))(supports-color@10.2.2)(typescript@6.0.3)
+ astro:
+ specifier: 7.3.2
+ version: 7.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(typescript@6.0.3)(yaml@2.9.1)
+ sharp:
+ specifier: 0.35.5
+ version: 0.35.5(@types/node@26.6.5)
+ devDependencies:
+ '@astrojs/check':
+ specifier: 0.9.10
+ version: 0.9.10(@emnapi/runtime@1.11.3)(prettier-plugin-astro@1.1.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(prettier@3.9.10))(prettier@3.9.10)(typescript@6.0.3)
+ typescript:
+ specifier: 6.0.3
+ version: 6.0.3
+
+ landing:
+ dependencies:
+ astro:
+ specifier: 7.3.2
+ version: 7.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(typescript@6.0.3)(yaml@2.9.1)
+ sharp:
+ specifier: 0.35.5
+ version: 0.35.5(@types/node@26.6.5)
+ devDependencies:
+ '@astrojs/check':
+ specifier: 0.9.10
+ version: 0.9.10(@emnapi/runtime@1.11.3)(prettier-plugin-astro@1.1.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(prettier@3.9.10))(prettier@3.9.10)(typescript@6.0.3)
+ prettier:
+ specifier: 3.9.10
+ version: 3.9.10
+ prettier-plugin-astro:
+ specifier: 1.1.1
+ version: 1.1.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(prettier@3.9.10)
+ typescript:
+ specifier: 6.0.3
+ version: 6.0.3
+
+ playground:
+ dependencies:
+ '@codemirror/commands':
+ specifier: 6.11.1
+ version: 6.11.1
+ '@codemirror/lang-json':
+ specifier: 6.0.2
+ version: 6.0.2
+ '@codemirror/lang-python':
+ specifier: 6.2.1
+ version: 6.2.1
+ '@codemirror/language':
+ specifier: 6.13.1
+ version: 6.13.1
+ '@codemirror/state':
+ specifier: 6.7.6
+ version: 6.7.6
+ '@codemirror/view':
+ specifier: 6.43.14
+ version: 6.43.14
+ '@fontsource-variable/jetbrains-mono':
+ specifier: 5.3.0
+ version: 5.3.0
+ '@fontsource-variable/mona-sans':
+ specifier: 5.3.0
+ version: 5.3.0
+ '@lezer/highlight':
+ specifier: 1.2.5
+ version: 1.2.5
+ devDependencies:
+ '@playwright/test':
+ specifier: 1.64.1
+ version: 1.64.1
+ '@types/node':
+ specifier: 26.6.5
+ version: 26.6.5
+ pyodide:
+ specifier: 314.0.7
+ version: 314.0.7
+ typescript:
+ specifier: 6.0.3
+ version: 6.0.3
+ vite:
+ specifier: npm:@voidzero-dev/vite-plus-core@1.3.0
+ version: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)'
+
+packages:
+
+ '@astrojs/astro2tsx@0.1.3':
+ resolution: {integrity: sha512-GMDC7Eo0myBUA/QLBl0u+OcegJ8LiBhccmNZR34KmprQNP8gQUJlWlOOZWUsxNA2rNS6GuPYcGDyLoPnjmn4/Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
+ '@astrojs/check@0.9.10':
+ resolution: {integrity: sha512-zgx/UQMozdjOa3bOxjgeCFdtpE3c9rRX6xHwa+2QXvy8z8Akifu2AtubHyv/zzC2znO8dl8fFWL4K+Ba9kS8HQ==}
+ hasBin: true
+ peerDependencies:
+ typescript: ^5.0.0 || ^6.0.0
+
+ '@astrojs/compiler-binding-android-arm64@0.4.1':
+ resolution: {integrity: sha512-Wtx2DZORuNTLASLLZ8hlrjFaJe1LUloatcCn9EPq7knBhRpjMGXeJANIJMeBZG4igvz7x+3nyAJM2iwCSW9xog==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@astrojs/compiler-binding-darwin-arm64@0.4.1':
+ resolution: {integrity: sha512-7iwXcU+hB60Y1SvOfnWHNioT72gDsDQKaDipXh2Onwp/TvQMozhbvhy13wJR8Af940a0h6PbddCeJFpj791UDA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@astrojs/compiler-binding-darwin-x64@0.4.1':
+ resolution: {integrity: sha512-pifjICos49RXhSVKSDu5gDB0/8j0O7DmxSp1DaWW2PyLG5ekwSkcrfOZto5JqhdI7QYMZB3XOiXpsdOXpS4npA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@astrojs/compiler-binding-linux-arm64-gnu@0.4.1':
+ resolution: {integrity: sha512-hsZMlNWc6LqWHh9LlSwTq1/XCY7pxA5rkkKIdZRV8mgbOz9OwLvjEoe5Jry13eIZjMUQO8XnQyzk8o/Pjw2YFA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@astrojs/compiler-binding-linux-arm64-musl@0.4.1':
+ resolution: {integrity: sha512-a3EW27f+Z9uf57vYmpLgUriA1y9wDhKz57PT8D8XlFZt2HZEWhUxKuxI1jvKVI9p8okYee11WXEm/FHY1G9yoQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@astrojs/compiler-binding-linux-x64-gnu@0.4.1':
+ resolution: {integrity: sha512-XruPTKB/SS4DFzeAjQ1ZyM0ieHDjapHkOSnnzRApZ4FXY2YAI7UY3mETjfJiIS/acyZyrnPH5jObv1gATTdUJQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@astrojs/compiler-binding-linux-x64-musl@0.4.1':
+ resolution: {integrity: sha512-E8j7lGanqNpudtkUbsFaOLJhmwFe4Ecqux7K9hkj4wULIPNZRM2bYZNcMSj8r1oWvaUiwZl8LaaNwv3+14jqhw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@astrojs/compiler-binding-wasm32-wasi@0.4.1':
+ resolution: {integrity: sha512-vNLxIv41ZE+nmPFtw24cRW5ZXG+ZPcAGtgigtjCP8lvcxKI7u2GrXoMsCgEH5fMw8WCEJpwLvsVBg15rJKAkyA==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
+ '@astrojs/compiler-binding-win32-arm64-msvc@0.4.1':
+ resolution: {integrity: sha512-/xTDgRsFDoTh0/+KFXJ74DY4Jqn8CoSHwa8UP5RAblHfnRHkenwfSl4zYDET61O9vXKLk09UzHjjq09yP20/AQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@astrojs/compiler-binding-win32-x64-msvc@0.4.1':
+ resolution: {integrity: sha512-0x5J6iHZO0Fjo/CVzoIbXKzWnGESftDbEUQ/oXQ61HhUAFoR7+T1jfToZSNAS+gYyBXHSTpMWYC7lzarbhClcg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@astrojs/compiler-binding@0.4.1':
+ resolution: {integrity: sha512-ZYVDW1P58OXyxqZbOMp+/2U9KA7y2esIx9r4pWdpZ/l6fCZhaw4ul+D7EgpRVxYLkyTWqzANbTTUFGPFP8zDHA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
+ '@astrojs/compiler-rs@0.4.1':
+ resolution: {integrity: sha512-//NmAuRhy7eU9iP0ceI5Pymy55M+nUZ//kS6XeOyhBWdDib2KbJOyFaVJ52k7sDkJwTKVox7oPu3SpYiVnGm5A==}
+ engines: {node: '>=22.12.0'}
+
+ '@astrojs/internal-helpers@0.11.0':
+ resolution: {integrity: sha512-3rzxJ+xbo0+8YyqOzLziIN32wmsHdCjEVz2sGOpRxJ+Ben/KiLph4ItxBy1abEL+E8fkRzqjg0rfXmaHJGw9JA==}
+
+ '@astrojs/internal-helpers@0.13.0':
+ resolution: {integrity: sha512-20jxQqnSmpskcGyad7N5l6Xf1gz0UCa6lNde44bEBdaXGpncEYfAZJ142r2ZgzBaG4dUyzGmhqZWv1INU2eiHQ==}
+
+ '@astrojs/language-server@2.17.2':
+ resolution: {integrity: sha512-WzqCBYo0RtKrkPLPMnTz2+UOQPosaFhoG/PvoSyPIgBU0BKeS6qeaYv7Rscq7P1LxJdoN0VosQF2h2TZTsIGHg==}
+ hasBin: true
+ peerDependencies:
+ prettier: ^3.0.0
+ prettier-plugin-astro: '>=0.11.0'
+ peerDependenciesMeta:
+ prettier:
+ optional: true
+ prettier-plugin-astro:
+ optional: true
+
+ '@astrojs/markdown-satteri@0.4.1':
+ resolution: {integrity: sha512-EniHbFNa6SQHDih7JnpPos3NWXO6hdt4raBcOosfGmlfc3gP9CI/sESA3VOf1PgJZ7SFfewlZW9Livn0JugEZg==}
+
+ '@astrojs/markdown-satteri@0.4.4':
+ resolution: {integrity: sha512-Byhe9CAiGFJrhMACKpgN/FPf4lcwapqwFQAB7E1sPELTEJINfBIpgnEYOBTXzr//fvSSNhfp/X3AqN4mUV9p+A==}
+
+ '@astrojs/mdx@8.1.0':
+ resolution: {integrity: sha512-xzrlXrjpBYvC27MTkP+2NMiJG4Jjl2DcoNc/EF9RNxAp0dwhvXC3b22+7OO4kHJQKfISk2uy38HYp1UxX7JfSg==}
+ engines: {node: '>=22.22.0'}
+ peerDependencies:
+ '@astrojs/markdown-remark': ^7.3.0
+ '@astrojs/markdown-satteri': ^0.4.0
+ astro: ^7.2.10
+ peerDependenciesMeta:
+ '@astrojs/markdown-remark':
+ optional: true
+
+ '@astrojs/prism@4.0.2':
+ resolution: {integrity: sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==}
+ engines: {node: '>=22.12.0'}
+
+ '@astrojs/prism@4.1.0':
+ resolution: {integrity: sha512-zvRaL3MKTAXWfVQrHAlYtocRZWXBM6ruF3Bl7Lc7QgFHLRTfwMKo33KczlkvkXDY+qcytKhAWOT9dwUUYLUxAw==}
+ engines: {node: '>=22.22.0'}
+
+ '@astrojs/sitemap@3.7.4':
+ resolution: {integrity: sha512-LbKNC24bdUWcQf/pThB6qLlSqHojxGjZDURIzFocY8rlWnAn2t74nnhnK6S5x0NHriHoAduLEpVjRykmeGiVvA==}
+
+ '@astrojs/starlight@0.42.1':
+ resolution: {integrity: sha512-ABS/A0IfbJeMbuhCKVkrAcO3EWxhpeLveFuL28+7+BQje7ZHa9OXZbUoiLXdYUhH7XFaiAQjAg44RiF2uuZPJw==}
+ peerDependencies:
+ '@astrojs/markdown-remark': ^7.3.0
+ astro: ^7.2.10
+ peerDependenciesMeta:
+ '@astrojs/markdown-remark':
+ optional: true
+
+ '@astrojs/telemetry@3.3.3':
+ resolution: {integrity: sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ==}
+ engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0}
+
+ '@astrojs/yaml2ts@0.2.4':
+ resolution: {integrity: sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A==}
+
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.9':
+ resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/runtime@7.29.10':
+ resolution: {integrity: sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.8':
+ resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@blazediff/core@1.10.0':
+ resolution: {integrity: sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ==}
+
+ '@bruits/satteri-darwin-arm64@0.10.5':
+ resolution: {integrity: sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@bruits/satteri-darwin-x64@0.10.5':
+ resolution: {integrity: sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@bruits/satteri-linux-arm64-gnu@0.10.5':
+ resolution: {integrity: sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@bruits/satteri-linux-arm64-musl@0.10.5':
+ resolution: {integrity: sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@bruits/satteri-linux-x64-gnu@0.10.5':
+ resolution: {integrity: sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@bruits/satteri-linux-x64-musl@0.10.5':
+ resolution: {integrity: sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@bruits/satteri-wasm32-wasi@0.10.5':
+ resolution: {integrity: sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
+ '@bruits/satteri-win32-arm64-msvc@0.10.5':
+ resolution: {integrity: sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@bruits/satteri-win32-x64-msvc@0.10.5':
+ resolution: {integrity: sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ==}
+ cpu: [x64]
+ os: [win32]
+
+ '@capsizecss/unpack@4.0.1':
+ resolution: {integrity: sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==}
+ engines: {node: '>=18'}
+
+ '@clack/core@1.5.1':
+ resolution: {integrity: sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==}
+ engines: {node: '>= 20.12.0'}
+
+ '@clack/prompts@1.8.1':
+ resolution: {integrity: sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==}
+ engines: {node: '>= 20.12.0'}
+
+ '@cloudflare/kv-asset-handler@0.5.0':
+ resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==}
+ engines: {node: '>=22.0.0'}
+
+ '@cloudflare/unenv-preset@2.16.2':
+ resolution: {integrity: sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==}
+ peerDependencies:
+ unenv: 2.0.0-rc.24
+ workerd: '>1.20260305.0 <2.0.0-0'
+ peerDependenciesMeta:
+ workerd:
+ optional: true
+
+ '@cloudflare/workerd-darwin-64@1.20261012.1':
+ resolution: {integrity: sha512-dkTL9aY3V4D40b+jBpGPCistWUPFQ/M61wlWi6i1o8MvHFFgEw7EQkcaYEv6Yd3bkPDD/3TFT4hRsMb2xVZSwQ==}
+ engines: {node: '>=16'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@cloudflare/workerd-darwin-arm64@1.20261012.1':
+ resolution: {integrity: sha512-2/nVGcKXjNNI+DhxKSepTKvFng9H7aN2WXv6Zu67iqgI6JrxypNt5kTPv4ZHosxrcfyKMci/2RfN7GIrN0BitQ==}
+ engines: {node: '>=16'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@cloudflare/workerd-linux-64@1.20261012.1':
+ resolution: {integrity: sha512-uMe7ONHnLxStD1Ye0YFHBO36uj4cStv5P53rW+5GVWXFT8BRjqVixlH0IqCAHe1Pmcwyd1+7fVoGde89MZSCJg==}
+ engines: {node: '>=16'}
+ cpu: [x64]
+ os: [linux]
+
+ '@cloudflare/workerd-linux-arm64@1.20261012.1':
+ resolution: {integrity: sha512-I0VMdcXk7ygG5vYDce0ePTBFdnBXC0i9WiBY5mWiv0iTinbfea6/rHqSQ7B1UMpjgrYVs9E0m4bZ9ufsk5GCFA==}
+ engines: {node: '>=16'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@cloudflare/workerd-windows-64@1.20261012.1':
+ resolution: {integrity: sha512-c5hQvNx574UEoknaCFvMa/NdLrrYiALNbe6sx1okW8uOiRYWxrFuA8WpXFnBJ5XJypm0TEAHYaUTGv3rRLpY7Q==}
+ engines: {node: '>=16'}
+ cpu: [x64]
+ os: [win32]
+
+ '@codemirror/autocomplete@6.20.3':
+ resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==}
+
+ '@codemirror/commands@6.11.1':
+ resolution: {integrity: sha512-O/4hG3SC1YwcmQ0d2UVNDs+AsaNWd1iHVxbTeEBuqH+6bExAiPK3iS/BvpY6rZGURALv4ZD3sIgcCmRvw3ehBg==}
+
+ '@codemirror/lang-json@6.0.2':
+ resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
+
+ '@codemirror/lang-python@6.2.1':
+ resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==}
+
+ '@codemirror/language@6.13.1':
+ resolution: {integrity: sha512-zfvU2r3TyylQLMj/te+5CnQ0rSqZi8B1oTCBbQkagcLW4Xc6imab6qN+pEJhsuNOLaJqiRyBXp9UhWA1DhVvDw==}
+
+ '@codemirror/state@6.7.6':
+ resolution: {integrity: sha512-kAz+AncRtKuIknedxT1bq4XwXv4UowhbkHU1myPrtVb/jZtImWuV5BXzv5vK6i3kYACsdiZiQKFQQ5Mq7elW8w==}
+
+ '@codemirror/streamparser@6.0.0':
+ resolution: {integrity: sha512-JTFPF6xrfjt0mnfGPgzcLiMwYmvIyNaoizp8BYzv8FE8KrT6GBGDZ190rtt+O7Wke3gG/MFHyiY9YGNeimof4w==}
+
+ '@codemirror/view@6.43.14':
+ resolution: {integrity: sha512-hC45YnmJNR1rz42OF57BLoQNFMV12/13drnvibDsR2quNDmldcb4WvYVV81hFQg56i9lcqQMEDqilhX2QDcGQQ==}
+
+ '@cspotcode/source-map-support@0.8.1':
+ resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
+ engines: {node: '>=12'}
+
+ '@ctrl/tinycolor@4.2.1':
+ resolution: {integrity: sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==}
+ engines: {node: '>=14'}
+
+ '@emmetio/abbreviation@2.3.3':
+ resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==}
+
+ '@emmetio/css-abbreviation@2.1.8':
+ resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==}
+
+ '@emmetio/css-parser@0.4.1':
+ resolution: {integrity: sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ==}
+
+ '@emmetio/html-matcher@1.3.0':
+ resolution: {integrity: sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==}
+
+ '@emmetio/scanner@1.0.4':
+ resolution: {integrity: sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==}
+
+ '@emmetio/stream-reader-utils@0.1.0':
+ resolution: {integrity: sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==}
+
+ '@emmetio/stream-reader@2.2.0':
+ resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==}
+
+ '@emnapi/core@1.11.1':
+ resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
+
+ '@emnapi/core@1.11.3':
+ resolution: {integrity: sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==}
+
+ '@emnapi/runtime@1.11.1':
+ resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
+
+ '@emnapi/runtime@1.11.3':
+ resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
+
+ '@emnapi/wasi-threads@1.2.2':
+ resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
+
+ '@emnapi/wasi-threads@1.2.3':
+ resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==}
+
+ '@esbuild/aix-ppc64@0.28.2':
+ resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.28.2':
+ resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.28.2':
+ resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.28.2':
+ resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.28.2':
+ resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.28.2':
+ resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.28.2':
+ resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.28.2':
+ resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.28.2':
+ resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.28.2':
+ resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.28.2':
+ resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.28.2':
+ resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.28.2':
+ resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.28.2':
+ resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.28.2':
+ resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.28.2':
+ resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.28.2':
+ resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.28.2':
+ resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.28.2':
+ resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.28.2':
+ resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.28.2':
+ resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.28.2':
+ resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.28.2':
+ resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.28.2':
+ resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.28.2':
+ resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.28.2':
+ resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@expressive-code/core@0.44.2':
+ resolution: {integrity: sha512-Yw5KonQCD1HoOO2P89y4wifqS9qrJcGVY0LkVMUsVZjhi++bhz4WwQ37inenKFNOEVo50P7cjoVsCdaVnUbq8w==}
+
+ '@expressive-code/plugin-frames@0.44.2':
+ resolution: {integrity: sha512-gVLm6TQMNIRWIaM7o+mCtxkGNjLZY5/2faSTLoXeo9wv/Pkj/0QAtZyXPVPrRsPARqiipATNFmfvQFLJudxPHQ==}
+
+ '@expressive-code/plugin-shiki@0.44.2':
+ resolution: {integrity: sha512-ebWIvCDzXvjyD8rH6T6DfytYJw1GoArLR4nusIAE4VxXrCQf3d+owytiv010KU/dUg+SRLZskDn0N+HaFXeuSg==}
+
+ '@expressive-code/plugin-text-markers@0.44.2':
+ resolution: {integrity: sha512-SNI7EgUSiADqbqUzzMbj1ZeC/r2pa5eg3KyFOGAd3a7EsIA4qzjXQLp+ODWQtHVDKFCdGs4J3ovSJUqvKIK96g==}
+
+ '@fontsource-variable/jetbrains-mono@5.3.0':
+ resolution: {integrity: sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==}
+
+ '@fontsource-variable/mona-sans@5.3.0':
+ resolution: {integrity: sha512-PkxRB3KI4z9Gjpk7vVqgO3GMSyQuOIIj4TaRDbvbcDj8SzWwI38jvLTQxJM/cTRorhcT7h8qjcllR+qjRtM/Fg==}
+
+ '@img/colour@1.1.0':
+ resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
+ engines: {node: '>=18'}
+
+ '@img/sharp-darwin-arm64@0.35.5':
+ resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-darwin-x64@0.35.5':
+ resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-freebsd-wasm32@0.35.5':
+ resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==}
+ engines: {node: '>=20.9.0'}
+ os: [freebsd]
+
+ '@img/sharp-libvips-darwin-arm64@1.3.4':
+ resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-libvips-darwin-x64@1.3.4':
+ resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-libvips-linux-arm64@1.3.4':
+ resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-arm@1.3.4':
+ resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-ppc64@1.3.4':
+ resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-riscv64@1.3.4':
+ resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-s390x@1.3.4':
+ resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-x64@1.3.4':
+ resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.4':
+ resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-libvips-linuxmusl-x64@1.3.4':
+ resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-linux-arm64@0.35.5':
+ resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-arm@0.35.5':
+ resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-ppc64@0.35.5':
+ resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-riscv64@0.35.5':
+ resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==}
+ engines: {node: '>=20.9.0'}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-s390x@0.35.5':
+ resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-x64@0.35.5':
+ resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linuxmusl-arm64@0.35.5':
+ resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-linuxmusl-x64@0.35.5':
+ resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-wasm32@0.35.5':
+ resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==}
+ engines: {node: '>=20.9.0'}
+
+ '@img/sharp-webcontainers-wasm32@0.35.5':
+ resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [wasm32]
+
+ '@img/sharp-win32-arm64@0.35.5':
+ resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@img/sharp-win32-ia32@0.35.5':
+ resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==}
+ engines: {node: ^20.9.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@img/sharp-win32-x64@0.35.5':
+ resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [win32]
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.6.0':
+ resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@jridgewell/trace-mapping@0.3.9':
+ resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
+
+ '@lezer/common@1.5.3':
+ resolution: {integrity: sha512-H0iErY4e43LpXbYDyBci5W4v/RwTgGV3YzYOlJPiGZ8RY8w51kVE+xLn1tg3Z6UmSfyETvtsUK3r7YsgDQEI7Q==}
+
+ '@lezer/highlight@1.2.5':
+ resolution: {integrity: sha512-O1GMVKgtf5YspFaRzpmqkVgMtIk2HG9uMAbH6wdAIdNh3TVzCsch/+cRi+6l2//UGDKe6NB9BW3cvuR5ezMQ0w==}
+
+ '@lezer/json@1.0.3':
+ resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
+
+ '@lezer/lr@1.4.11':
+ resolution: {integrity: sha512-UwgZcl1R66H2hDzBrA7kWyYmX6s8p5TQ+e/t7CuvgCtBEPxbEPaHYHu+pJBvTvDotEJpuQkJjbXuHiw5Cixxgw==}
+
+ '@lezer/python@1.1.19':
+ resolution: {integrity: sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==}
+
+ '@marijn/find-cluster-break@1.0.4':
+ resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==}
+
+ '@napi-rs/wasm-runtime@1.2.4':
+ resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4
+ '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4
+
+ '@napi-rs/wasm-runtime@1.2.5':
+ resolution: {integrity: sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4
+ '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4
+
+ '@oslojs/encoding@1.1.0':
+ resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
+
+ '@oxc-project/runtime@0.154.0':
+ resolution: {integrity: sha512-cBQNGoh659BNgvA8OC/e4BNDtFkv03dJaYdkp2adgOQNtISNAPQ+nB4S8XvWtNk7Xl+0g+7aa+iRGR25Xycd1Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
+ '@oxc-project/types@0.154.0':
+ resolution: {integrity: sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==}
+
+ '@oxfmt/binding-android-arm-eabi@0.73.0':
+ resolution: {integrity: sha512-dhunAdMbi/4o0Y66/+Wl6rpsNqpvE4i26Qw6bB77A97L2lJoktWg6m+X9QOon8iOEEqzD9rEZqFi5uZ4oIzhCA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [android]
+
+ '@oxfmt/binding-android-arm64@0.73.0':
+ resolution: {integrity: sha512-dBoypjC2/xeyKyIhN2waX0/XeoccH9k0lVVe+sGRc4X8HSJVRtqyZDoXdr7bGY9A75c51dX0z0dLDpGQKwCdCg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@oxfmt/binding-darwin-arm64@0.73.0':
+ resolution: {integrity: sha512-Tn7Y+qWIpCQK+qdYwBcn6YvqeBuPiuG3tcNlYg16a5QL2o3rSmrgBB1nCyYqo0uvc1LNrfQW9/CK2tzFuID/Fg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@oxfmt/binding-darwin-x64@0.73.0':
+ resolution: {integrity: sha512-yVb0E0tg/+9Xc9YJCBq66qF1DC/aN5mhb5nnHz0k4ikwkMANtSul2VPDSwjYdXR6/DFimc+pPUgLBqkIa5Ctuw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oxfmt/binding-freebsd-x64@0.73.0':
+ resolution: {integrity: sha512-IKR4lFnBkYNL9Uqdxa5GMTQLvnLj+3sDwTvh2atHCzah0xeo+1xR5bigRU8uyXlOsA4VpXUn0UAimq4E4RBGmQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@oxfmt/binding-linux-arm-gnueabihf@0.73.0':
+ resolution: {integrity: sha512-7qnZ4g0ZCO3aZevOOQpZIABYwwXJW1lLDo2elviGdPQ0i11255xKEMFsi97fC1/nauQFseCJfELUy/k2JoOP+g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxfmt/binding-linux-arm-musleabihf@0.73.0':
+ resolution: {integrity: sha512-36eBico2j/WPB84gb4FYd/jjqGCKOwU4M+TWLBUIqlnzTozQueLy0joA5BwsSp2OZFVkOlpCwvA3ndPamdiyLw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxfmt/binding-linux-arm64-gnu@0.73.0':
+ resolution: {integrity: sha512-VZ3cSgCAT18knKo52DcACmAuIoqG6qVEyeZRFbJd0xmYQ8GyYjuBc6IAh6MZHijT6q7p4/HPI+6ZWsCuhdV6iw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@oxfmt/binding-linux-arm64-musl@0.73.0':
+ resolution: {integrity: sha512-8YVzH59XSHcqwXNgc5qK851zCqwpmBgOifARXEPPe5/xTsIbHceYoPZAyqfpYV0CfKmzSaEDBgYHFza4rhx+Zw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@oxfmt/binding-linux-ppc64-gnu@0.73.0':
+ resolution: {integrity: sha512-MqRJnmCys0rwOgpWyEidSA7v/bvOqfNDJhPkQoR9mPVYrvSmMaW2Xhdpl28cbTA+brOM1is62GjIvJplVlUVpw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@oxfmt/binding-linux-riscv64-gnu@0.73.0':
+ resolution: {integrity: sha512-4t2F3SWJSK/O/kRRFnL0elJzn75fJGGu71gs1HtiWcS80SmJOpkHiwwLnTp9suGdns8VHEPxYS0GTcyVwHKWBA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@oxfmt/binding-linux-riscv64-musl@0.73.0':
+ resolution: {integrity: sha512-D8b4I8I1kLdJQFwdFXNV+cwh3xWSiNKofjdGUk0nDpc0AHOds8T+poGbP9RWQMvgsvMb9OMgp/aV7ahBR37TbQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [musl]
+
+ '@oxfmt/binding-linux-s390x-gnu@0.73.0':
+ resolution: {integrity: sha512-QrC1S5VL1+N5rvorB15jH7CPvjVHA7GyUI5oVEd8Bd9m5605RDBQYTxDQ920DuylRoZOh2V7/xXpfCtFUrojUQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@oxfmt/binding-linux-x64-gnu@0.73.0':
+ resolution: {integrity: sha512-dFcUKQPltCEPa+YlkF3zqufn6nJPZdG73LpnjBvZFFsU3laCMssDXSYB99mVaj43FqdEgjpz5TO6gbYXcczd9g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@oxfmt/binding-linux-x64-musl@0.73.0':
+ resolution: {integrity: sha512-LmQe+nV8e+eUwfPNCxDF+owbsg88toBiR/8rZRgVOeKQP3MaGefXZRvBoc1UG37iWEQ/9JlCXG9Mc4nf4N3agw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@oxfmt/binding-openharmony-arm64@0.73.0':
+ resolution: {integrity: sha512-8weOr1OKknE0G2DbR6ejJUT9AOsoBU6cXlsetmbihHv1K+bK7qP61T6lJxY0k/aAO7GyOJiRGNlL+04r4eH8Xg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@oxfmt/binding-win32-arm64-msvc@0.73.0':
+ resolution: {integrity: sha512-mZV+QxXYgb8E18gzAFItb937VIsE4dqWdStrD3tRRC52UubOYEuU+e7NaIqUsGdB5YsJqNeuck3Ue/af2r9wyA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@oxfmt/binding-win32-ia32-msvc@0.73.0':
+ resolution: {integrity: sha512-QJPUsbgUb97ZPPMkFUq6VQq0fLhC4Np19KL9aB/qKVwXnpKE6MIiILXqzhKnQ/UbnEDjKJ7Xv1d6zH4bYPCUdQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@oxfmt/binding-win32-x64-msvc@0.73.0':
+ resolution: {integrity: sha512-cGcze2fu+ykIaFtkEvZlkZ3HvnCLk+WISthtq99zWTWn0x13t5L9TxvGdLZMUMvtfYR+ICkiCwKnguVubdcw0w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@oxlint-tsgolint/darwin-arm64@7.0.2004':
+ resolution: {integrity: sha512-uJk3ttju7Lz+Nv8WG4CsLyewoL1qf/Ny8S8OksJ7Bm/F14Tc9gMfW6arM1PCc57IzH/X/I79YsCWTgxX1FKibQ==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@oxlint-tsgolint/darwin-x64@7.0.2004':
+ resolution: {integrity: sha512-0BFfHTzDxNDFPcu0UZAXT6CyCJHAXM0dZveyDb4KEHa2qF/DSnOKOrmvK/wFuFol9kvijbGM9JfuOsor+zWXRw==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oxlint-tsgolint/linux-arm64@7.0.2004':
+ resolution: {integrity: sha512-TAehQc+qdRXyDN3nl/r/3/oPrBVrbtrDeGMkFyG6mf+XphpoJ2lAMVFLclyvrVZCw02RbKROjyw8h72XwQQHcw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@oxlint-tsgolint/linux-x64@7.0.2004':
+ resolution: {integrity: sha512-LPunZvm6ZbWPfHMAyfZf9ci3HHvNEzLejO8zdqO3GoVGoSEHqqufHUD7+OgeatVSaJP7uXDJ2HaNOHIoH7go4Q==}
+ cpu: [x64]
+ os: [linux]
+
+ '@oxlint-tsgolint/win32-arm64@7.0.2004':
+ resolution: {integrity: sha512-FVWplzDImWhiuawnIZVETaQecv4GWom6Jv4Fx2xj7hhXXDZ5okEgiw1gTCImHTIQsi2ycC5oSxVwrn69bMh7iw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@oxlint-tsgolint/win32-x64@7.0.2004':
+ resolution: {integrity: sha512-7PsxuUx11Ry9DYTWcljTzRbmZY9B6O9fFFfZV+aodQEMy6pfRT3fhoQRv9PtGN/89z1pPz3nFKrsFm6WqNtX7w==}
+ cpu: [x64]
+ os: [win32]
+
+ '@oxlint/binding-android-arm-eabi@1.88.0':
+ resolution: {integrity: sha512-k6Fgkf86ubfeH4GvqYsRolfuVAp0pfYVZvifSlXNHPdMaWUOfxUrmLEeDD7hF6U2oJuhsVxhSWq86/xCJ+9MmA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [android]
+
+ '@oxlint/binding-android-arm64@1.88.0':
+ resolution: {integrity: sha512-AsIyhl9o1ejwtac7NMbhKpMdSml6NWR0ljKjZERxC8OHYDAW8o/Om9n1QHVLi4VCEUo42Zte2/Op9kyoGpJZMg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@oxlint/binding-darwin-arm64@1.88.0':
+ resolution: {integrity: sha512-qgBjB7eEKcCB4RqjXGuoSZq74g0wD09fWFJ+HWcCJcKaga8TeUHtb+BY0SsxdleSLBgx4RZ7qmgo0EzqEq7JoQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@oxlint/binding-darwin-x64@1.88.0':
+ resolution: {integrity: sha512-sCz3X5Mw/xdzEzn4fsCt1Iq8NKJnGNfhR+b9B5k+4DysYAHsmIhYoocnBPiQ9kvY3+pUun2A0Z8Vtun/Nv6YoQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@oxlint/binding-freebsd-x64@1.88.0':
+ resolution: {integrity: sha512-xTOdYOIqCufgIj0knvxu86FBfovP6QBiD7ngNva01gb8xPQCD5gXkE5APGczH35zStim1O3zqvFbLQmY85xXRg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@oxlint/binding-linux-arm-gnueabihf@1.88.0':
+ resolution: {integrity: sha512-BLadtw88NdYJBrAtC1dYzrPfSP2d5zgx6iuZycdD0YmZBJ2Nqbit9Z4PSaIb6gxK25dweArGMPbOmq/0ugwUPg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm-musleabihf@1.88.0':
+ resolution: {integrity: sha512-wILSczl/DaZsCcf7j7tvNbssp8hxEHIJxrv4j7+JjI3TAL1U/JTWbeUtoJJC/vo8+2hkTmHLYoKJjmiNbijdXw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@oxlint/binding-linux-arm64-gnu@1.88.0':
+ resolution: {integrity: sha512-jutT2Hy8JCuZuMwIFHo2HtyxfOfBJHyQxlBHBK1SL9W2f8n/WhjAP0FnDMz9d/mjhv9+QA0mx5AdQ9fDjsrFFg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@oxlint/binding-linux-arm64-musl@1.88.0':
+ resolution: {integrity: sha512-8N8iGV9hh4Lkeuq7QsEkjoXNRRflqR1G2qyoGYXcA0k5qllrdqmfylKEXyHIVERxqNDyMO4StqDQ0uLxXcBQAg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@oxlint/binding-linux-ppc64-gnu@1.88.0':
+ resolution: {integrity: sha512-5ydcQt3znNQ/rGEWYRvpS2cK9+T18P2VYgnyJ+eloEAwi9YUmsXy5VGrRqNWIK6qzWwHGnEE3ojVqqQNo+f0hA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@oxlint/binding-linux-riscv64-gnu@1.88.0':
+ resolution: {integrity: sha512-iGx0uvFT54b/5DPJdEBCxYIrVB3v0uTy+JhRh3CyEp8/22BXDkG8rffkFwREdLsdj9mOrPbJYVOZw8ipcBuzjw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@oxlint/binding-linux-riscv64-musl@1.88.0':
+ resolution: {integrity: sha512-Wt47mKMWelVjRjuR7BCFxXkB0m79K1sTEbqMHRWciKUgo5iv7sWsF4RXSOyFYHD9zerNrkRZblnN273rhwOA1Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [musl]
+
+ '@oxlint/binding-linux-s390x-gnu@1.88.0':
+ resolution: {integrity: sha512-g3L12KCS4+1gtUt3RbJ4F+Yxx4LNmmZLEPfhd8rAHEJjJk2lAgNfnjGoP/phJyhG5WS6VbNYbJtadJelNfvTMA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@oxlint/binding-linux-x64-gnu@1.88.0':
+ resolution: {integrity: sha512-og/XHVDOD9ZJgRiZkT9DgzQd4sr+JZW7e4l17Q/jVMZiIbeEd4uIu9iAoUbTF1gIvVZoRc4QDOxyVUvQGp4WHg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@oxlint/binding-linux-x64-musl@1.88.0':
+ resolution: {integrity: sha512-EmhBsp+xuKb6YbZNEMLBo80Xw71AZOIm18do8ni/GLX/Lw1XzNhVYpbxO1KasYtoAvrGtCOdCPULRlIE0bPbTQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@oxlint/binding-openharmony-arm64@1.88.0':
+ resolution: {integrity: sha512-DBUcx/KwYdg1DAz+G01P/GFv512J04EsZadbHptx2FTpWT/MWRg21ZToNHt9pnsfQRBQ3et/g+vqzbvrdAVbuA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@oxlint/binding-win32-arm64-msvc@1.88.0':
+ resolution: {integrity: sha512-xeVd0n4XG5HvynxfYvXi6uB+JvZbVZataowPS8cYpMbYg9mwE4sSyElFQd4LVJFifq8bRLDSR18XgEildmcXAg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@oxlint/binding-win32-ia32-msvc@1.88.0':
+ resolution: {integrity: sha512-MA6iOw//amleaz2mVMsZrA7NHlEexjTrhIxSMFCXQi6IUuQO+3Od00GZhr2iCYw6RFzMW36jkKK/WHBXMsoQAw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@oxlint/binding-win32-x64-msvc@1.88.0':
+ resolution: {integrity: sha512-3Sz6rVWe8Dy+ufsXurFN1GX2dX43F/lQMSLelK9gFjiba7nHvLlqlhR5cHlWVnCpSqUY1hN9pyCX9XToe6ve6g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@oxlint/plugins@1.88.0':
+ resolution: {integrity: sha512-G2xbfWTrbqYRhR/vfeKNMQf/sS3/ti0iKJAAkRxHuXWZH/hne/dyIb9QKk5LuR6cJLARxsL6FMoAYmfspd74MQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ '@pagefind/darwin-arm64@1.5.2':
+ resolution: {integrity: sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@pagefind/darwin-x64@1.5.2':
+ resolution: {integrity: sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@pagefind/default-ui@1.5.2':
+ resolution: {integrity: sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg==}
+
+ '@pagefind/freebsd-x64@1.5.2':
+ resolution: {integrity: sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@pagefind/linux-arm64@1.5.2':
+ resolution: {integrity: sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@pagefind/linux-x64@1.5.2':
+ resolution: {integrity: sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==}
+ cpu: [x64]
+ os: [linux]
+
+ '@pagefind/windows-arm64@1.5.2':
+ resolution: {integrity: sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@pagefind/windows-x64@1.5.2':
+ resolution: {integrity: sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==}
+ cpu: [x64]
+ os: [win32]
+
+ '@playwright/test@1.64.1':
+ resolution: {integrity: sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==}
+ engines: {node: '>=20'}
+ hasBin: true
+
+ '@polka/url@1.0.0-next.29':
+ resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
+
+ '@poppinss/colors@4.1.6':
+ resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==}
+
+ '@poppinss/dumper@0.6.5':
+ resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==}
+
+ '@poppinss/exception@1.2.3':
+ resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==}
+
+ '@prettier/parse-srcset@3.1.0':
+ resolution: {integrity: sha512-FIRv2rZotO9NP/r66taYqD1zICpPiBbrECqjKV/xqNotqDxF7fLzzUeK+1RSRx9Tenk0DajR/GcRmTJ2qtHaKQ==}
+
+ '@shikijs/core@4.5.0':
+ resolution: {integrity: sha512-6CIgwq+bt2JZ3OSjb/nBkkwEf/CfpHcbam+Mg5Pllco7HAGRQ2/EFhZrrJtbyNTgKwknfVS+llsXGygiDGwJWw==}
+ engines: {node: '>=20'}
+
+ '@shikijs/engine-javascript@4.5.0':
+ resolution: {integrity: sha512-Ed6UdEj84LyYkwnxM5CvSMWAD+id9ExTYri0/+x41U9zrgNifPSS9lZm0JqBP0VfYI8yr8tY5nbNq9+p9R0kFA==}
+ engines: {node: '>=20'}
+
+ '@shikijs/engine-oniguruma@4.5.0':
+ resolution: {integrity: sha512-dKVmbJecVdB9recl8a/CU4jdpNXyjJQrIcesIkpS70s5HS02ohpnCSpMUbotPnEUzbAcLQ+S4hwZYbkBBiTcJw==}
+ engines: {node: '>=20'}
+
+ '@shikijs/langs@4.5.0':
+ resolution: {integrity: sha512-wwBTqwduXSz0X4v9Qa2EWeFaYJEw1clCncWjoRNQRbVDu3SPRTRWucnouYapKj3b8Q+sk/41Vbc4B5c8nuYzLA==}
+ engines: {node: '>=20'}
+
+ '@shikijs/primitive@4.5.0':
+ resolution: {integrity: sha512-UM7ShlA8oz0OWxLLO8BmO9Ip6fVqdkt8yF0EH7nOCqHfrx1eEAzF0ZUVK7L+I8NEgeu1PEWSRlxaiPPeCb4iDg==}
+ engines: {node: '>=20'}
+
+ '@shikijs/themes@4.5.0':
+ resolution: {integrity: sha512-zGb87H3UBbpuI4yoh5UguB1XbU+2BDU3kflM7UfZ+Cy2hWo9lBtqjSVK20gmLY6ukYl040hAypGXMgud9vul8Q==}
+ engines: {node: '>=20'}
+
+ '@shikijs/types@4.5.0':
+ resolution: {integrity: sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==}
+ engines: {node: '>=20'}
+
+ '@shikijs/vscode-textmate@10.0.2':
+ resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
+
+ '@sindresorhus/is@7.2.0':
+ resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==}
+ engines: {node: '>=18'}
+
+ '@speed-highlight/core@1.2.24':
+ resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==}
+
+ '@testing-library/dom@10.4.2':
+ resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==}
+ engines: {node: '>=18'}
+
+ '@testing-library/user-event@14.6.8':
+ resolution: {integrity: sha512-vgSSqnANMnkcYR/vUNDbwS7aM3OErRLNUG8vCVVlj4oc6cKffUrccX7UAS5AmJQQNZTSibi9z5GvvRZ0aYgUCQ==}
+ engines: {node: '>=12', npm: '>=6'}
+ peerDependencies:
+ '@testing-library/dom': '>=7.21.4'
+
+ '@tybys/wasm-util@0.10.4':
+ resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==}
+
+ '@types/aria-query@5.0.4':
+ resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+ '@types/debug@4.1.13':
+ resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+ '@types/emscripten@1.41.6':
+ resolution: {integrity: sha512-uN+9i8bFT5CUcZfyIEYDrSueACEyKGbUs5kC/72DGlZZoinh84sJfVV0i8UOJD1asdzkvLPBRrKs41kZ8MdEXg==}
+
+ '@types/estree-jsx@1.0.5':
+ resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@types/hast@3.0.5':
+ resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==}
+
+ '@types/js-yaml@4.0.9':
+ resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
+
+ '@types/mdast@4.0.4':
+ resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
+
+ '@types/ms@2.1.0':
+ resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
+
+ '@types/nlcst@2.0.3':
+ resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==}
+
+ '@types/node@24.19.2':
+ resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==}
+
+ '@types/node@26.6.5':
+ resolution: {integrity: sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==}
+
+ '@types/sax@1.2.7':
+ resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
+
+ '@types/unist@2.0.11':
+ resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
+
+ '@types/unist@3.0.3':
+ resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+
+ '@ungap/structured-clone@1.4.0':
+ resolution: {integrity: sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==}
+
+ '@vitest/browser-preview@5.0.5':
+ resolution: {integrity: sha512-ViNg4UlomGSJHtnnr7Txm+55P7/aAMMoS+1WYnWjp5w5tzf7YEUAICcs2sb1uMovEnC8el/8zUfkWZ8+7fPCKA==}
+ peerDependencies:
+ vitest: 5.0.5
+
+ '@vitest/browser@5.0.5':
+ resolution: {integrity: sha512-IDMoZX/iZ8rkilw7weaSgN8tJ7iZ9hUv0wYoKXyiyjWjWFwLSWAY707DcVTgxTYOudWC1E3HA9LYn7kVa9a7uQ==}
+ peerDependencies:
+ vitest: 5.0.5
+
+ '@vitest/mocker@5.0.5':
+ resolution: {integrity: sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==}
+ peerDependencies:
+ msw: ^2.4.9 || ^3.0.0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+
+ '@vitest/pretty-format@5.0.5':
+ resolution: {integrity: sha512-24sSdIlMA9DVPXnYiB9siwU0bTd887Y9jX3+vabztCSsvHwo7ipvJ/a2L3Rljw8+2rK1f5fCj3h5U9I/Jsqgbw==}
+
+ '@vitest/snapshot@5.0.5':
+ resolution: {integrity: sha512-dHyFElWMQy0wFBGrIaPPeUWfJFh+jAUrv46ImIJGd8CADdKV3mlrAu3FyD0wesdxELFCs9M807e040Kba/XKtA==}
+
+ '@vitest/spy@5.0.5':
+ resolution: {integrity: sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==}
+
+ '@vitest/ui@5.0.5':
+ resolution: {integrity: sha512-dCoGUOdgmCaSB2g+WQ/spPCBkytAWdNUkFSXY4z+eYp2d8JEOY8v0uDRaJTG9qf/kTlTVZ4CuYMYMz/f1HS/qQ==}
+ peerDependencies:
+ vitest: 5.0.5
+
+ '@vitest/utils@5.0.5':
+ resolution: {integrity: sha512-/rBu7gzLDQI/SC79Tn98b5DwOgaHU/Gt4I9OVeP3YofzNaYtbImRCOJQmT1S6vOJg9gEIlWC+r3QxKkjuSa1nQ==}
+
+ '@voidzero-dev/vite-plus-core@1.3.0':
+ resolution: {integrity: sha512-Bk/LBwNrxoB8XbGUtGJE3r+U9P4puvdZ6UH36ekygE1J+on/ytwM/yBgQwBnvmX6/MVax+2xxj8wk0N/KkyNeA==}
+ engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0}
+ peerDependencies:
+ '@arethetypeswrong/core': ^0.18.1
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@typescript/native-preview': '*'
+ '@vitejs/devtools': ^0.7.1
+ '@volar/typescript': ~2.4.0
+ '@vue/language-core': ~3.2.0 || ~3.3.0
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ publint: ^0.3.8
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ typescript: ^5.0.0 || ^6.0.0 || ^7.0.0
+ unplugin-unused: '>=0.5.0'
+ unrun: '*'
+ vue-tsc: ~3.2.0 || ~3.3.0
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@arethetypeswrong/core':
+ optional: true
+ '@types/node':
+ optional: true
+ '@typescript/native-preview':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ '@volar/typescript':
+ optional: true
+ '@vue/language-core':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ publint:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ typescript:
+ optional: true
+ unplugin-unused:
+ optional: true
+ unrun:
+ optional: true
+ vue-tsc:
+ optional: true
+ yaml:
+ optional: true
+
+ '@voidzero-dev/vite-plus-darwin-arm64@1.3.0':
+ resolution: {integrity: sha512-BF4W+UwGxUkHPrH2pZLhafJfkJT20WZIbMzZgZLGQ44LPpZPX8Sb+qtblUnXUkkCzTqad4XtSPgiLKglvZkNQQ==}
+ engines: {node: '>=20.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@voidzero-dev/vite-plus-darwin-x64@1.3.0':
+ resolution: {integrity: sha512-vLZTk0r+s3e7mycj4SGBbcpK+xyM8a86hXQZtQbJyW7xrGJzsavISy7/m4gGZpWN32Hl4Tmx9V/ffeaoXLkRdg==}
+ engines: {node: '>=20.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@voidzero-dev/vite-plus-linux-arm64-gnu@1.3.0':
+ resolution: {integrity: sha512-H43Dj9RkEyjOFXxTk3DLTa6hqmHhxUN9vbbqNdwQoc3/TPNhC1zsLZy3qgUFbYGLdS7M9jICCqFzj5GDSYrhIw==}
+ engines: {node: '>=20.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@voidzero-dev/vite-plus-linux-arm64-musl@1.3.0':
+ resolution: {integrity: sha512-Se2GlvzUZOvFy+5hoQHoG7LFeiplKs+EJwaskayCSLwZAP+e6/LTK7EK+8OcdMpmdsLPtZoTJHkIjSzrQowJwQ==}
+ engines: {node: '>=20.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@voidzero-dev/vite-plus-linux-x64-gnu@1.3.0':
+ resolution: {integrity: sha512-moxojk6zmog2SaLE4/cG75acE6X3cjR0/52Ba8OG2ct0r+dv55VSE0kellAuxS6o9VHy7TBhkIpwpFpQ34rnAA==}
+ engines: {node: '>=20.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@voidzero-dev/vite-plus-linux-x64-musl@1.3.0':
+ resolution: {integrity: sha512-CykxH008Dy9u20Myki9/UIqq3Dbo1ZMqa098aS6AtWp3QKyEKzSLPB3jKDUklgmu83xiFe5BpPA+33oF0yNY7A==}
+ engines: {node: '>=20.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@voidzero-dev/vite-plus-win32-arm64-msvc@1.3.0':
+ resolution: {integrity: sha512-l61S9b8aeiDpi8wPRfz4HonqRkLpt/OVYs0sCskvlXYSW/L8qfLOtuWH/xpF8xDjMIfBefKejBVfU1UlRax5Tw==}
+ engines: {node: '>=20.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@voidzero-dev/vite-plus-win32-x64-msvc@1.3.0':
+ resolution: {integrity: sha512-b6Ng8YezA6JA3adQBNA+8Mn26+OX4fw/tsPfnk//5Cx+1XFDZoAxWYXtGzc0yUQPEKnoGtfw/0OUvcibg61yYg==}
+ engines: {node: '>=20.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ '@volar/kit@2.4.28':
+ resolution: {integrity: sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg==}
+ peerDependencies:
+ typescript: '*'
+
+ '@volar/language-core@2.4.28':
+ resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
+
+ '@volar/language-server@2.4.28':
+ resolution: {integrity: sha512-NqcLnE5gERKuS4PUFwlhMxf6vqYo7hXtbMFbViXcbVkbZ905AIVWhnSo0ZNBC2V127H1/2zP7RvVOVnyITFfBw==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@volar/language-service@2.4.28':
+ resolution: {integrity: sha512-Rh/wYCZJrI5vCwMk9xyw/Z+MsWxlJY1rmMZPsxUoJKfzIRjS/NF1NmnuEcrMbEVGja00aVpCsInJfixQTMdvLw==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@volar/source-map@2.4.28':
+ resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==}
+
+ '@volar/typescript@2.4.28':
+ resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@vscode/emmet-helper@2.11.0':
+ resolution: {integrity: sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==}
+
+ '@vscode/l10n@0.0.18':
+ resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==}
+
+ '@yuku-core/binding-android-arm64@0.19.0':
+ resolution: {integrity: sha512-wdL2XmTiZixAOaHk9Z0JJZ8u9Qm8d333Zjpc6gaxXFX+UR5BlsfxZ7a2+2Rm71pamppsPYGUGFq2oVLbh6DISw==}
+ cpu: [arm64]
+ os: [android]
+
+ '@yuku-core/binding-darwin-arm64@0.19.0':
+ resolution: {integrity: sha512-7sNJa6XJhyetnTy64jw3NEi4YgUcezHk1G5GH9FSvXAuC8wIlbaOwT9lgq5qo5sspjrZJQlBR1TWflP49uRpMw==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@yuku-core/binding-darwin-x64@0.19.0':
+ resolution: {integrity: sha512-pTC7dYEc59isdyizc3nsuyFNBu2LU5wMYZ1be6UMr59PY/AJNtO1aBLWV0MgL/z7C1nv/2sX4TNjGgKLoeevJw==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@yuku-core/binding-freebsd-x64@0.19.0':
+ resolution: {integrity: sha512-xrZjF6XOEwyLOiDIfrwDdRCS6ghxcJeirNC1KivQhVcUwlRonaU79S3/Uiyx3lBylPnT8qKJ3/aMT+H52nGTJQ==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@yuku-core/binding-linux-arm-gnu@0.19.0':
+ resolution: {integrity: sha512-0OyIH53C0I1CbADHn9FSdCfX5qbpeLwJhmOVhonFJNYheybOnkSJ50qGMfTJIb8y5zrwqvLs7FVBvz6Fk8maZg==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@yuku-core/binding-linux-arm-musl@0.19.0':
+ resolution: {integrity: sha512-z/iPfkCOCSXE34VXNt0uP1ORSW+4mEa9b5CiJcWZeqZH08g49bFU/jRM2T04f+Wpw5mJ6HIzNBiojxeAbrHjeg==}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
+
+ '@yuku-core/binding-linux-arm64-gnu@0.19.0':
+ resolution: {integrity: sha512-jLNTi+gzaRwBJ+gwhCJ+rDb5dmx/8hEBx93UWd7hjMSqPTL1D1wvVSvXVAldV5kALvju7b0/m51EHEhs4+I1fA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@yuku-core/binding-linux-arm64-musl@0.19.0':
+ resolution: {integrity: sha512-YwyPIdW/rN2Fmv0MTFWi47FaB3F9g+2b+sJISyAw83Yd60RWH4T3Nx6XkAHHVN+uKJNBAd6So7/BVihSiFYvuQ==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@yuku-core/binding-linux-x64-gnu@0.19.0':
+ resolution: {integrity: sha512-/vskaAXVX3ESR43FiTdfiYS7z1b9uoQJwVQ/0RgUHgN2sWoAyCZs+aX4CgU7eKGIHTVfp9VFwv+c+2XchYiEtQ==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@yuku-core/binding-linux-x64-musl@0.19.0':
+ resolution: {integrity: sha512-FuXL5igeSKib9Rye02TdH+D6SQezTyGxIATMu1Qsn/QywrzwpGl35OLpS2C1x5vpGLeVE2XgiLd5mVPFHmIglQ==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@yuku-core/binding-win32-arm64@0.19.0':
+ resolution: {integrity: sha512-YuJAe7zEKT1Xa7LuHlhGJpPnYQeVtvo0rZvNjJRqkH0hQGOw4C8QeP30Tg5VKGzZfo2x8CjhjEhKt4TmLwfmdg==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@yuku-core/binding-win32-x64@0.19.0':
+ resolution: {integrity: sha512-o8iY/mXwlRkedp0LNTr4+TEfXTPQwoNZmv6k+1WIAd1ZXEn8xJJTBCtB0osaVvdZo/x/cNmV16UrWrrGpM5Mvw==}
+ cpu: [x64]
+ os: [win32]
+
+ '@yuku-toolchain/types@0.19.0':
+ resolution: {integrity: sha512-tFkNbsP4WXX9UMUC5syJGdK9dPMLK0PPaIuXCcCYH75sZ8MrebfBZ2XvCILRTw8AgLivSSCfDa4R67iYBDa4Kg==}
+
+ ajv-draft-04@1.0.0:
+ resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==}
+ peerDependencies:
+ ajv: ^8.5.0
+ peerDependenciesMeta:
+ ajv:
+ optional: true
+
+ ajv-i18n@4.2.0:
+ resolution: {integrity: sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg==}
+ peerDependencies:
+ ajv: ^8.0.0-beta.0
+
+ ajv@8.20.0:
+ resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
+
+ am-i-vibing@0.4.0:
+ resolution: {integrity: sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg==}
+ hasBin: true
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-regex@6.4.0:
+ resolution: {integrity: sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==}
+ engines: {node: '>=12'}
+
+ ansi-styles@5.2.0:
+ resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
+ engines: {node: '>=10'}
+
+ ansi-styles@6.2.3:
+ resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
+ engines: {node: '>=12'}
+
+ anymatch@3.1.3:
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+ engines: {node: '>= 8'}
+
+ arg@5.0.2:
+ resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ aria-query@5.3.0:
+ resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+
+ aria-query@5.3.2:
+ resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
+ engines: {node: '>= 0.4'}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
+ astro-expressive-code@0.44.2:
+ resolution: {integrity: sha512-avop7nZcRwC7quvIxVguhwEZ9Tp6IoTzKUCLwP30Fb7+er+pWQgcysP01dcuzRIJo81kLGg30q/YAI2Yb1Sdzg==}
+ peerDependencies:
+ astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0
+
+ astro@7.3.2:
+ resolution: {integrity: sha512-ysTcdpGP61XZpHoMRYC/CK19DQ94qkBvzXMtXEo0gEqPNkmOU/Tv++CtWmzaI4nF2Ck8RXFdiWvdlTWa2oOr9w==}
+ engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'}
+ hasBin: true
+ peerDependencies:
+ '@astrojs/markdown-remark': ^7.3.0
+ peerDependenciesMeta:
+ '@astrojs/markdown-remark':
+ optional: true
+
+ axobject-query@4.1.0:
+ resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
+ engines: {node: '>= 0.4'}
+
+ bail@2.0.2:
+ resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
+
+ bcp-47-match@2.0.3:
+ resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==}
+
+ bcp-47@2.1.1:
+ resolution: {integrity: sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==}
+
+ blake3-wasm@2.1.5:
+ resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
+
+ boolbase@1.0.0:
+ resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+
+ ccount@2.0.1:
+ resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
+
+ chai@6.3.0:
+ resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==}
+ engines: {node: '>=18'}
+
+ character-entities-html4@2.1.0:
+ resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
+
+ character-entities-legacy@3.0.0:
+ resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
+
+ character-entities@2.0.2:
+ resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
+
+ character-reference-invalid@2.0.1:
+ resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
+
+ chokidar@4.0.3:
+ resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
+ engines: {node: '>= 14.16.0'}
+
+ chokidar@5.0.0:
+ resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
+ engines: {node: '>= 20.19.0'}
+
+ ci-info@4.4.0:
+ resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
+ engines: {node: '>=8'}
+
+ cliui@9.0.1:
+ resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
+ engines: {node: '>=20'}
+
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
+ comma-separated-tokens@2.0.3:
+ resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+
+ commander@11.1.0:
+ resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
+ engines: {node: '>=16'}
+
+ common-ancestor-path@2.0.0:
+ resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==}
+ engines: {node: '>= 18'}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ cookie-es@1.2.3:
+ resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==}
+
+ cookie@1.1.1:
+ resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
+ engines: {node: '>=18'}
+
+ cookie@2.0.1:
+ resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==}
+ engines: {node: '>=22'}
+
+ crelt@1.0.7:
+ resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==}
+
+ crossws@0.3.5:
+ resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==}
+
+ css-select@6.0.0:
+ resolution: {integrity: sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==}
+
+ css-selector-parser@3.3.0:
+ resolution: {integrity: sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==}
+
+ css-tree@2.2.1:
+ resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+
+ css-tree@3.2.1:
+ resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
+ css-what@7.0.0:
+ resolution: {integrity: sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==}
+ engines: {node: '>= 6'}
+
+ cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ csso@5.0.5:
+ resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ decode-named-character-reference@1.3.0:
+ resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
+
+ defu@6.1.7:
+ resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
+
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
+ destr@2.0.5:
+ resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ devalue@5.9.4:
+ resolution: {integrity: sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==}
+
+ devlop@1.1.0:
+ resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
+
+ diff@9.0.0:
+ resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==}
+ engines: {node: '>=0.3.1'}
+
+ direction@2.0.1:
+ resolution: {integrity: sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==}
+ hasBin: true
+
+ dom-accessibility-api@0.5.16:
+ resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+
+ dom-serializer@2.0.0:
+ resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+
+ domelementtype@2.3.0:
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+
+ domhandler@5.0.3:
+ resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
+ engines: {node: '>= 4'}
+
+ domutils@3.2.2:
+ resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+
+ dset@3.1.4:
+ resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
+ engines: {node: '>=4'}
+
+ emmet@2.4.11:
+ resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==}
+
+ emoji-regex@10.6.0:
+ resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
+
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
+ error-stack-parser-es@1.0.5:
+ resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
+
+ es-module-lexer@2.3.2:
+ resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
+
+ es-module-lexer@3.0.4:
+ resolution: {integrity: sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==}
+
+ esbuild@0.28.2:
+ resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ eventemitter3@5.0.4:
+ resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
+
+ expect-type@1.4.0:
+ resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
+ engines: {node: '>=12.0.0'}
+
+ expressive-code@0.44.2:
+ resolution: {integrity: sha512-MLNghkvhyFgVW4oCD5DxBuYnnTXIf0PygzGmfT6OZeOOYWh/D4sH1j931biElkA5RG/J9wY/iuSMGAPF5vzHOg==}
+
+ extend@3.0.2:
+ resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-string-truncated-width@3.0.3:
+ resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==}
+
+ fast-string-width@3.0.2:
+ resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
+
+ fast-uri@3.1.8:
+ resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==}
+
+ fast-wrap-ansi@0.2.2:
+ resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fflate@0.8.3:
+ resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==}
+
+ find-proc@0.1.0:
+ resolution: {integrity: sha512-OaOpEYv2PiQ7SQ5LIrl+deA1XaWcxEjnpM6VuWXTUvn+teIXxeFTLDmu18/zDQpFmHN4o3oDBX+BT0AGwEhemg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
+ flatted@3.4.4:
+ resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
+
+ flattie@1.1.1:
+ resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==}
+ engines: {node: '>=8'}
+
+ fontace@0.4.1:
+ resolution: {integrity: sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==}
+
+ fontkitten@1.0.3:
+ resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==}
+ engines: {node: '>=20'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
+ get-east-asian-width@1.7.0:
+ resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==}
+ engines: {node: '>=18'}
+
+ get-tsconfig@5.0.0-beta.4:
+ resolution: {integrity: sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==}
+ engines: {node: '>=20.20.0'}
+
+ github-slugger@2.0.0:
+ resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
+
+ h3@1.15.11:
+ resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==}
+
+ hast-util-embedded@3.0.0:
+ resolution: {integrity: sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==}
+
+ hast-util-format@1.1.0:
+ resolution: {integrity: sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==}
+
+ hast-util-has-property@3.0.0:
+ resolution: {integrity: sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==}
+
+ hast-util-is-body-ok-link@3.0.1:
+ resolution: {integrity: sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==}
+
+ hast-util-is-element@3.0.0:
+ resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
+
+ hast-util-minify-whitespace@1.0.1:
+ resolution: {integrity: sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==}
+
+ hast-util-parse-selector@4.0.0:
+ resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
+
+ hast-util-phrasing@3.0.1:
+ resolution: {integrity: sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==}
+
+ hast-util-select@6.0.4:
+ resolution: {integrity: sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==}
+
+ hast-util-to-html@9.0.5:
+ resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
+
+ hast-util-to-string@3.0.1:
+ resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==}
+
+ hast-util-to-text@4.0.2:
+ resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==}
+
+ hast-util-whitespace@3.0.0:
+ resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+
+ hastscript@9.0.1:
+ resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
+
+ html-escaper@3.0.3:
+ resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
+
+ html-void-elements@3.0.0:
+ resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+
+ html-whitespace-sensitive-tag-names@3.0.1:
+ resolution: {integrity: sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==}
+
+ http-cache-semantics@4.3.0:
+ resolution: {integrity: sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==}
+
+ i18next@26.5.2:
+ resolution: {integrity: sha512-6fIXt//2MZNok3tULyjnypxe3vbsy3pV1bVg8p2x3yKrzZvp7z3T+wXx/88LQsATRdhyJeAUAvUxEtxsHdJ29w==}
+ peerDependencies:
+ typescript: ^5 || ^6 || ^7
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ iron-webcrypto@1.2.1:
+ resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==}
+
+ is-alphabetical@2.0.1:
+ resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
+
+ is-alphanumerical@2.0.1:
+ resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
+
+ is-decimal@2.0.1:
+ resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
+
+ is-docker@4.0.0:
+ resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==}
+ engines: {node: '>=20'}
+ hasBin: true
+
+ is-hexadecimal@2.0.1:
+ resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
+
+ is-plain-obj@4.1.0:
+ resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
+ engines: {node: '>=12'}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@4.3.2:
+ resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
+ hasBin: true
+
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
+ jsonc-parser@2.3.1:
+ resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==}
+
+ jsonc-parser@3.3.1:
+ resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
+
+ kleur@4.1.5:
+ resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
+ engines: {node: '>=6'}
+
+ klona@2.0.6:
+ resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
+ engines: {node: '>= 8'}
+
+ lightningcss-android-arm64@1.33.0:
+ resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.33.0:
+ resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.33.0:
+ resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.33.0:
+ resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.33.0:
+ resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.33.0:
+ resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.33.0:
+ resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.33.0:
+ resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.33.0:
+ resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.33.0:
+ resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.33.0:
+ resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.33.0:
+ resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
+ engines: {node: '>= 12.0.0'}
+
+ longest-streak@3.1.0:
+ resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
+
+ lru-cache@11.5.3:
+ resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==}
+ engines: {node: 20 || >=22}
+
+ lz-string@1.5.0:
+ resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+ hasBin: true
+
+ magic-string@1.4.3:
+ resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==}
+
+ magicast@0.5.5:
+ resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==}
+
+ mdast-util-directive@3.1.1:
+ resolution: {integrity: sha512-Gqpa3MHorXYEZ0VfbroyvBlVKUfz371V6mIERqOYMcb0W1D3LwmFq1ZW4O2TDrbn+4ZmGLwMWeFRJgYZ+bc5fQ==}
+
+ mdast-util-from-markdown@2.1.0:
+ resolution: {integrity: sha512-KRLgmPaP2H1ul2bpoJkOS+9pAcDAAj9Nu1mqLnh8NLeJG5tlvj42TH80PIGBpROWlRXLk0lDrWNuZCFy/tsB4A==}
+
+ mdast-util-phrasing@4.1.0:
+ resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
+
+ mdast-util-to-hast@13.2.1:
+ resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
+
+ mdast-util-to-markdown@2.2.0:
+ resolution: {integrity: sha512-2Jn/ADrHFFyyniBMi0bncTbJqU78K3/TEYvzvwrWWzD9MYPUa/Gm4r3nOeKFQg/PDM5BsbDYdc8ackW4NTY6NA==}
+
+ mdast-util-to-string@4.0.0:
+ resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
+
+ mdn-data@2.0.28:
+ resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
+
+ mdn-data@2.27.1:
+ resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
+
+ micromark-core-commonmark@2.0.4:
+ resolution: {integrity: sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==}
+
+ micromark-extension-directive@4.0.0:
+ resolution: {integrity: sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==}
+
+ micromark-factory-destination@2.0.1:
+ resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
+
+ micromark-factory-label@2.0.1:
+ resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==}
+
+ micromark-factory-space@2.1.0:
+ resolution: {integrity: sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==}
+
+ micromark-factory-title@2.0.1:
+ resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==}
+
+ micromark-factory-whitespace@2.0.1:
+ resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==}
+
+ micromark-util-character@2.1.1:
+ resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
+
+ micromark-util-chunked@2.0.1:
+ resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==}
+
+ micromark-util-classify-character@2.0.1:
+ resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==}
+
+ micromark-util-combine-extensions@2.0.1:
+ resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==}
+
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==}
+
+ micromark-util-decode-string@2.0.1:
+ resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==}
+
+ micromark-util-edit-map@1.0.0:
+ resolution: {integrity: sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==}
+
+ micromark-util-encode@2.0.1:
+ resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
+
+ micromark-util-html-tag-name@2.0.1:
+ resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==}
+
+ micromark-util-normalize-identifier@2.0.1:
+ resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==}
+
+ micromark-util-resolve-all@2.0.1:
+ resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==}
+
+ micromark-util-sanitize-uri@2.0.1:
+ resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
+
+ micromark-util-subtokenize@2.1.0:
+ resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==}
+
+ micromark-util-symbol@2.0.1:
+ resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
+
+ micromark-util-types@2.0.3:
+ resolution: {integrity: sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==}
+
+ micromark@4.0.3:
+ resolution: {integrity: sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==}
+
+ miniflare@5.20261012.1-beta:
+ resolution: {integrity: sha512-8Canhy6g9+vAhqMc0o80YphqYIwpiA3aAp8aX+uDXU7J/qbUeoZzcII9c2V1P4uT/eHD4SB93qet1o1bRY3BuQ==}
+ engines: {node: '>=22.0.0'}
+
+ mrmime@2.0.1:
+ resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
+ engines: {node: '>=10'}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ muggle-string@0.4.1:
+ resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
+
+ nanoid@3.3.20:
+ resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ neotraverse@1.0.1:
+ resolution: {integrity: sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==}
+ engines: {node: '>= 10'}
+
+ nlcst-to-string@4.0.0:
+ resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==}
+
+ node-fetch-native@1.6.7:
+ resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
+
+ node-mock-http@1.0.5:
+ resolution: {integrity: sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==}
+
+ normalize-path@3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+
+ nth-check@2.1.1:
+ resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+
+ obug@2.2.1:
+ resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==}
+ engines: {node: '>=12.20.0'}
+
+ obug@3.0.0:
+ resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==}
+ engines: {node: '>=12.20.0'}
+
+ ofetch@1.5.1:
+ resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==}
+
+ ohash@2.0.12:
+ resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==}
+
+ oniguruma-parser@0.12.2:
+ resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
+
+ oniguruma-to-es@4.3.6:
+ resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==}
+
+ oxfmt@0.73.0:
+ resolution: {integrity: sha512-Ke4xbpuzqIBP4bkd48q815RhqWNJqOBzxVA9199M4qcF0y0LtpUv7UTg1dJ5ea6jw0RBUDZvX3qOUMNmmxfxbw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@astrojs/compiler-rs': ^0.4.0 || ^0.5.0
+ svelte: ^5.0.0
+ vite-plus: '*'
+ peerDependenciesMeta:
+ '@astrojs/compiler-rs':
+ optional: true
+ svelte:
+ optional: true
+ vite-plus:
+ optional: true
+
+ oxlint-tsgolint@7.0.2004:
+ resolution: {integrity: sha512-vQ9WDF3fiZJvRdSDosNOflerOCOpwol6vWzVDxW76RcU19cjTh7engvkHLvmv+tzedR8fAiJUVUGr6IVZdkzXQ==}
+ hasBin: true
+
+ oxlint@1.88.0:
+ resolution: {integrity: sha512-uIcFgw8BmqvFRGzH5+r7d2zJMEexwNSrEmM6WQNnGh3VkO0XYTEFoyxyNE6tjKMUib9Cue4XJA2DoWruO5D6aw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ oxlint-tsgolint: '>=7.0.2003'
+ vite-plus: '*'
+ peerDependenciesMeta:
+ oxlint-tsgolint:
+ optional: true
+ vite-plus:
+ optional: true
+
+ p-limit@7.3.3:
+ resolution: {integrity: sha512-SKgVvDU5TNIxQ4r30U75BNLC6a+6GOEjngrk/ysM5+Zu3oJuk1SC5ApzIzY/7PjCEjoVPMWJ0zho/4QUiEB7TQ==}
+ engines: {node: '>=20'}
+
+ p-queue@9.3.5:
+ resolution: {integrity: sha512-Lki/zhXFVmDhesBUn/yHRLW9Bte7mE7xoFHzTR8sIomV7a2z+LxYAPkGvdfidNiu4fKSfjtawixxSHcZci5QcQ==}
+ engines: {node: '>=20'}
+
+ p-timeout@7.0.2:
+ resolution: {integrity: sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==}
+ engines: {node: '>=20'}
+
+ package-manager-detector@1.9.0:
+ resolution: {integrity: sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==}
+
+ pagefind@1.5.2:
+ resolution: {integrity: sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==}
+ hasBin: true
+
+ parse-entities@4.0.2:
+ resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
+
+ path-browserify@1.0.1:
+ resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
+
+ path-to-regexp@6.3.0:
+ resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ piccolore@0.1.3:
+ resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.2:
+ resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
+ engines: {node: '>=8.6'}
+
+ picomatch@4.0.7:
+ resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
+ engines: {node: '>=12'}
+
+ playwright-core@1.64.1:
+ resolution: {integrity: sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==}
+ engines: {node: '>=20'}
+ hasBin: true
+
+ playwright@1.64.1:
+ resolution: {integrity: sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==}
+ engines: {node: '>=20'}
+ hasBin: true
+
+ pngjs@7.0.0:
+ resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
+ engines: {node: '>=14.19.0'}
+
+ postcss-nested@6.2.0:
+ resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.2.14
+
+ postcss-selector-parser@6.1.4:
+ resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==}
+ engines: {node: '>=4'}
+
+ postcss@8.5.30:
+ resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prettier-plugin-astro@1.1.1:
+ resolution: {integrity: sha512-ykymNFOinCFXY4z4yF0978GgAvg6GRtrzCYg3+R1Nv/dndACwuEsSrUpHkJohER9FiwYMPKuYT38FbjCLmwu9A==}
+ engines: {node: '>=22.12.0'}
+ peerDependencies:
+ prettier: ^3.5.3
+
+ prettier@3.9.10:
+ resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==}
+ engines: {node: '>=14'}
+ hasBin: true
+
+ pretty-format@27.5.1:
+ resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
+ engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+
+ prismjs@1.30.0:
+ resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
+ engines: {node: '>=6'}
+
+ process-ancestry@0.1.0:
+ resolution: {integrity: sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==}
+ engines: {node: '>=18.0.0'}
+
+ property-information@7.2.0:
+ resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==}
+
+ pyodide@314.0.7:
+ resolution: {integrity: sha512-0YvXxEhfEdpLfb/XkM2BFAeMROq0iMUX2bzzH9pOttyMcWkwq+HbE5uyuGD82LN7y2q+SNvi/6V5JEsOlD2R1A==}
+ engines: {node: '>=18.0.0'}
+
+ radix3@1.1.2:
+ resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
+
+ react-is@17.0.2:
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+
+ readdirp@4.1.2:
+ resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
+ engines: {node: '>= 14.18.0'}
+
+ readdirp@5.1.1:
+ resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==}
+ engines: {node: '>= 20.19.0'}
+
+ regex-recursion@6.0.2:
+ resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
+
+ regex-utilities@2.3.0:
+ resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
+
+ regex@6.1.0:
+ resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
+
+ rehype-expressive-code@0.44.2:
+ resolution: {integrity: sha512-jvjQpsfWKBquDrEl5YQA3CJybRRIuDOYsui1BJTs5oKDUK8pPkJSSxY8FjxNlIFIdtgrTjC1zuahuY4Cc1j9SQ==}
+
+ remark-directive@4.0.0:
+ resolution: {integrity: sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==}
+
+ request-light@0.5.8:
+ resolution: {integrity: sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==}
+
+ request-light@0.7.0:
+ resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==}
+
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ resolve-pkg-maps@1.0.0:
+ resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
+ retext-smartypants@6.2.0:
+ resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==}
+
+ s.color@0.0.15:
+ resolution: {integrity: sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==}
+
+ sass-formatter@0.8.0:
+ resolution: {integrity: sha512-Nf4eMGE2O5vnJG7KPCRyksWS9gVKSYoUWtPeMXRRxwZSPVQhSGtL602xVYS65LvMkHmkvKa7xpauF0+/5pPwrg==}
+ hasBin: true
+
+ satteri@0.10.5:
+ resolution: {integrity: sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==}
+
+ sax@1.6.1:
+ resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==}
+ engines: {node: '>=11.0.0'}
+
+ semver@7.8.5:
+ resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ sharp@0.35.5:
+ resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==}
+ engines: {node: '>=20.9.0'}
+ peerDependencies:
+ '@types/node': '*'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ shiki@4.5.0:
+ resolution: {integrity: sha512-OcRW72ngQvc+MSVWBRIovC/BTW589iUE/Vxav7VeM4RcY9MUjqB+IR6ILG1k9nuW6LiG/nct9uma4/MBtnZbmQ==}
+ engines: {node: '>=20'}
+
+ sirv@3.0.2:
+ resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
+ engines: {node: '>=18'}
+
+ sisteransi@1.0.5:
+ resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
+
+ sitemap@9.0.1:
+ resolution: {integrity: sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==}
+ engines: {node: '>=20.19.5', npm: '>=10.8.2'}
+ hasBin: true
+
+ smol-toml@1.9.1:
+ resolution: {integrity: sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==}
+ engines: {node: '>= 18'}
+
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
+ engines: {node: '>=0.10.0'}
+
+ space-separated-tokens@2.0.2:
+ resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
+
+ std-env@4.3.0:
+ resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==}
+
+ string-width@7.2.0:
+ resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
+ engines: {node: '>=18'}
+
+ string-width@8.3.0:
+ resolution: {integrity: sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==}
+ engines: {node: '>=20'}
+
+ stringify-entities@4.0.4:
+ resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+
+ strip-ansi@7.2.0:
+ resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
+ engines: {node: '>=12'}
+
+ style-mod@4.1.4:
+ resolution: {integrity: sha512-XXWIQt633/EpAFx8aZDOTjBzrCaGmhvEQlQo6MVPfa2OzO2cWo+4hV9h+6UkHYlXGfy+ODXKUdP7Pthmcu5ATw==}
+
+ suf-log@2.5.3:
+ resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==}
+
+ supports-color@10.2.2:
+ resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
+ engines: {node: '>=18'}
+
+ svgo@4.1.0:
+ resolution: {integrity: sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==}
+ engines: {node: '>=16'}
+ hasBin: true
+
+ tiny-inflate@1.0.3:
+ resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
+
+ tinybench@6.2.2:
+ resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==}
+ engines: {node: '>=20.0.0'}
+
+ tinyclip@0.1.15:
+ resolution: {integrity: sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==}
+ engines: {node: ^16.14.0 || >= 17.3.0}
+
+ tinyexec@1.3.1:
+ resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ tinypool@2.2.0:
+ resolution: {integrity: sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==}
+ engines: {node: ^20.0.0 || >=22.0.0}
+
+ tinyrainbow@3.2.0:
+ resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==}
+ engines: {node: '>=14.0.0'}
+
+ totalist@3.0.1:
+ resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
+ engines: {node: '>=6'}
+
+ trim-lines@3.0.1:
+ resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
+
+ trough@2.2.0:
+ resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ typesafe-path@0.2.2:
+ resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==}
+
+ typescript-auto-import-cache@0.3.6:
+ resolution: {integrity: sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==}
+
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ ufo@1.6.4:
+ resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
+
+ ultrahtml@1.7.0:
+ resolution: {integrity: sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==}
+
+ uncrypto@0.1.3:
+ resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
+
+ undici-types@7.24.6:
+ resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
+
+ undici-types@8.9.0:
+ resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
+
+ undici@7.29.1:
+ resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==}
+ engines: {node: '>=20.18.1'}
+
+ undici@8.12.2:
+ resolution: {integrity: sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==}
+ engines: {node: '>=22.19.0'}
+
+ unenv@2.0.0-rc.24:
+ resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==}
+
+ unified@11.0.5:
+ resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
+
+ unifont@0.7.5:
+ resolution: {integrity: sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg==}
+
+ unist-util-find-after@5.0.0:
+ resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==}
+
+ unist-util-is@6.0.1:
+ resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
+
+ unist-util-position@5.0.0:
+ resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+
+ unist-util-stringify-position@4.0.0:
+ resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
+
+ unist-util-visit-parents@6.0.2:
+ resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
+
+ unist-util-visit@5.1.0:
+ resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
+
+ unstorage@1.17.6:
+ resolution: {integrity: sha512-gjKtcmn/KwZuJZO9o2sc0esAQnhmTw0LviDHwevTXbJMYs0dRTp8ZEEWXHU0vTpzbN3cA0yxlTeyXssMp3zDqw==}
+ 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 || ^11.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
+ peerDependenciesMeta:
+ '@azure/app-configuration':
+ optional: true
+ '@azure/cosmos':
+ optional: true
+ '@azure/data-tables':
+ optional: true
+ '@azure/identity':
+ optional: true
+ '@azure/keyvault-secrets':
+ optional: true
+ '@azure/storage-blob':
+ optional: true
+ '@capacitor/preferences':
+ optional: true
+ '@deno/kv':
+ optional: true
+ '@netlify/blobs':
+ optional: true
+ '@planetscale/database':
+ optional: true
+ '@upstash/redis':
+ optional: true
+ '@vercel/blob':
+ optional: true
+ '@vercel/functions':
+ optional: true
+ '@vercel/kv':
+ optional: true
+ aws4fetch:
+ optional: true
+ db0:
+ optional: true
+ idb-keyval:
+ optional: true
+ ioredis:
+ optional: true
+ uploadthing:
+ optional: true
+
+ url-extras@0.1.0:
+ resolution: {integrity: sha512-8tzwTeXFPuX/5PHuCDQE5Dd9Ts4rwoq2t9aIT+HS4iAVpmj5l4Ao7Q+BuuFjvWRqrLswBhQDk8O96ZicgCqQqw==}
+ engines: {node: '>=20'}
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ vfile-message@4.0.3:
+ resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
+
+ vfile@6.0.3:
+ resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+
+ vite-plus@1.3.0:
+ resolution: {integrity: sha512-wZGsDeYfPcrCegqgY68pJJHGTpEZdsZvIMH2/mWXkGkBD72UY3M7z24Sv3ZeOL/FrqbbtyqBOfkKO6BvoQ8wMw==}
+ engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0}
+ hasBin: true
+ peerDependencies:
+ '@vitest/browser-playwright': 5.0.5
+ '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
+ playwright: '*'
+ peerDependenciesMeta:
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-webdriverio':
+ optional: true
+ playwright:
+ optional: true
+
+ vitefu@1.1.3:
+ resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==}
+
+ vitest@5.0.5:
+ resolution: {integrity: sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==}
+ engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@opentelemetry/api': ^1.9.0
+ '@types/node': ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 5.0.5
+ '@vitest/browser-preview': 5.0.5
+ '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
+ '@vitest/coverage-istanbul': 5.0.5
+ '@vitest/coverage-v8': 5.0.5
+ '@vitest/ui': 5.0.5
+ happy-dom: '*'
+ jsdom: '*'
+ vite: ^6.4.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-preview':
+ optional: true
+ '@vitest/browser-webdriverio':
+ optional: true
+ '@vitest/coverage-istanbul':
+ optional: true
+ '@vitest/coverage-v8':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ volar-service-css@0.0.71:
+ resolution: {integrity: sha512-wRRFt9BpjMKCazcgOh67MSjUjiWUCAh99DyYSDIOTuxaRjEtDC7PpB0k1Y1wbJIW/pVtMUSVbpPo3UGSm0Byxw==}
+ peerDependencies:
+ '@volar/language-service': ~2.4.0
+ peerDependenciesMeta:
+ '@volar/language-service':
+ optional: true
+
+ volar-service-emmet@0.0.71:
+ resolution: {integrity: sha512-zqjzt6bN95e3CUstBm0PBFAJnrfz0ZAARka87fart46/gNCLLuP3Vujy8V/J8HEziTFLnfkgIASLFYPUhonJcA==}
+ peerDependencies:
+ '@volar/language-service': ~2.4.0
+ peerDependenciesMeta:
+ '@volar/language-service':
+ optional: true
+
+ volar-service-html@0.0.71:
+ resolution: {integrity: sha512-e8tHPhgQ7ooLfudAEIku+kgd9pWkq3SSz8RbnQDI1+Eb8wbenkLGHqoirLqz5ORLV6wIMr2Iv08RWBG5eOcgpw==}
+ peerDependencies:
+ '@volar/language-service': ~2.4.0
+ peerDependenciesMeta:
+ '@volar/language-service':
+ optional: true
+
+ volar-service-prettier@0.0.71:
+ resolution: {integrity: sha512-Rz7JVH3qD108UCdmIEiZvOBNljMt2nLFdbN8AXcDfn7xD9F5I2aCIsDVqBbXw21PsnxG0b7MfwtNF+zPS/NKUg==}
+ peerDependencies:
+ '@volar/language-service': ~2.4.0
+ prettier: ^2.2 || ^3.0
+ peerDependenciesMeta:
+ '@volar/language-service':
+ optional: true
+ prettier:
+ optional: true
+
+ volar-service-typescript-twoslash-queries@0.0.71:
+ resolution: {integrity: sha512-9K2k72s4n7rV9s4bX0MyjbX9iBribvKZbBJKuEmTCZfeWJXs6Yh7bGpY4eoc7UufAjvpheBqwyZCOIPBvxCv0A==}
+ peerDependencies:
+ '@volar/language-service': ~2.4.0
+ typescript: '*'
+ peerDependenciesMeta:
+ '@volar/language-service':
+ optional: true
+ typescript:
+ optional: true
+
+ volar-service-typescript@0.0.71:
+ resolution: {integrity: sha512-yTtM/BVT6hoyEYnDtaCyAtNhdNeS/mhTTABlBOdw3NNiRBUin3IznFJpgfjer4c6RYopiPjjQjc9VFhxVl1mLw==}
+ peerDependencies:
+ '@volar/language-service': ~2.4.0
+ typescript: '*'
+ peerDependenciesMeta:
+ '@volar/language-service':
+ optional: true
+ typescript:
+ optional: true
+
+ volar-service-yaml@0.0.71:
+ resolution: {integrity: sha512-qYGWGuVpUTnZGu5P/CR4KLK4aIR8RrcVnmfZ2eRcj9q/I8VZCoC5yy9FtEvfNvnDp4MU17yhdJcvpQPIqhJS2Q==}
+ peerDependencies:
+ '@volar/language-service': ~2.4.0
+ peerDependenciesMeta:
+ '@volar/language-service':
+ optional: true
+
+ vscode-css-languageservice@6.3.10:
+ resolution: {integrity: sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA==}
+
+ vscode-html-languageservice@5.6.2:
+ resolution: {integrity: sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg==}
+
+ vscode-json-languageservice@4.1.8:
+ resolution: {integrity: sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==}
+ engines: {npm: '>=7.0.0'}
+
+ vscode-jsonrpc@8.2.0:
+ resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==}
+ engines: {node: '>=14.0.0'}
+
+ vscode-jsonrpc@9.0.3:
+ resolution: {integrity: sha512-qETYf9PCl1lVfwR+oBmMfc4vP2nqLNA35ABxorw3j06+xlKRgDVxbLgKm4Y/xPL0YWdeDjLoI4ImICIT3nB0zQ==}
+ engines: {node: '>=14.0.0'}
+
+ vscode-languageserver-protocol@3.17.5:
+ resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==}
+
+ vscode-languageserver-protocol@3.18.4:
+ resolution: {integrity: sha512-CCXbyr99dafRJzJH/0Ryk7ly5T8XPmSR0//OuF2w4m+9JiOin7zcAT5iET8GRE7Z9taFBejb3GbM0a0mbjdbrg==}
+
+ vscode-languageserver-textdocument@1.0.15:
+ resolution: {integrity: sha512-Gsfr9S8LU3S6885cS+NaleasJ0n3V5y/SOTqHCifQiXWFw3G9w9/YItDfwHrlCz+dSZWWZ8ilYVMcJe7Sz7Sxw==}
+
+ vscode-languageserver-types@3.17.5:
+ resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
+
+ vscode-languageserver-types@3.18.4:
+ resolution: {integrity: sha512-nQsacoiijvl0fRf2q28uFDrAVwRhdCa3QveiF7/CX0r18mLR5FU/1WskSsCKOjmEbtteKUnY8x8Yzxop+k0zwg==}
+
+ vscode-languageserver@9.0.1:
+ resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==}
+ hasBin: true
+
+ vscode-nls@5.2.0:
+ resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==}
+
+ vscode-uri@3.2.0:
+ resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==}
+
+ w3c-keyname@2.2.8:
+ resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
+
+ why-is-node-running@3.2.1:
+ resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==}
+ engines: {node: '>=20.11'}
+ hasBin: true
+
+ workerd@1.20261012.1:
+ resolution: {integrity: sha512-tEHoMPsMga0seVjxfsR8Pd3f8IRqYwkq++dTUKI5Gdgj6zuSDluYwisydq+dn90SsIACwBquJUQ1nBpcOqaWTA==}
+ engines: {node: '>=16'}
+ hasBin: true
+
+ wrangler@4.151.1:
+ resolution: {integrity: sha512-OAsa9UAHZshM6mq2PXcVoyxXgWThAanZMBc/IirtqEWd/BauRtmUKzAniOu/pU0pd3pxKuMZ2vsbF4eo6RiNDA==}
+ engines: {node: '>=22.0.0'}
+ hasBin: true
+ peerDependencies:
+ '@cloudflare/workers-types': ^5.20261006.1
+ peerDependenciesMeta:
+ '@cloudflare/workers-types':
+ optional: true
+
+ wrap-ansi@9.0.2:
+ resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
+ engines: {node: '>=18'}
+
+ ws@8.21.0:
+ resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
+ ws@8.22.0:
+ resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
+ xxhash-wasm@1.1.0:
+ resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==}
+
+ y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+
+ yaml-language-server@1.23.0:
+ resolution: {integrity: sha512-3qVyCOexLCWw06PQa5kRPwvMWMZ/eZeCRWUvgD6a0OkqL/4iCnxy2WumbWifa937Uo5xhyWJ0uxlU39ljhNh7A==}
+ hasBin: true
+
+ yaml@2.8.3:
+ resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==}
+ engines: {node: '>= 14.6'}
+ hasBin: true
+
+ yaml@2.9.1:
+ resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==}
+ engines: {node: '>= 14.6'}
+ hasBin: true
+
+ yargs-parser@22.0.0:
+ resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+
+ yargs@18.2.0:
+ resolution: {integrity: sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+
+ yocto-queue@1.2.2:
+ resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
+ engines: {node: '>=12.20'}
+
+ youch-core@0.3.3:
+ resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==}
+
+ youch@4.1.0-beta.10:
+ resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==}
+
+ yuku-core@0.19.0:
+ resolution: {integrity: sha512-kYK9e1XdfirNhkipzW0wmMcSWcVF/CWnuugodfVYRCUwvoBefweL9HzB96OiKEhsx78V5WbUxbJqlizMX869Qg==}
+ engines: {node: '>=14.18.0'}
+
+ yuku-parser@0.19.0:
+ resolution: {integrity: sha512-XrBh9ZiQK5mJVRxZBAvD6LwRyAYrK3MFtH3+onyuxFL5hFD44EC3QOmIC1FjTj6H2MMwq46/NAI+NZxLZPZgxg==}
+ engines: {node: '>=14.18.0'}
+
+ zod@4.6.7:
+ resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==}
+
+ zwitch@2.0.4:
+ resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
+
+snapshots:
+
+ '@astrojs/astro2tsx@0.1.3(@emnapi/runtime@1.11.3)':
+ dependencies:
+ '@emnapi/core': 1.11.3
+ '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)
+ transitivePeerDependencies:
+ - '@emnapi/runtime'
+
+ '@astrojs/check@0.9.10(@emnapi/runtime@1.11.3)(prettier-plugin-astro@1.1.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(prettier@3.9.10))(prettier@3.9.10)(typescript@6.0.3)':
+ dependencies:
+ '@astrojs/language-server': 2.17.2(@emnapi/runtime@1.11.3)(prettier-plugin-astro@1.1.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(prettier@3.9.10))(prettier@3.9.10)(typescript@6.0.3)
+ chokidar: 4.0.3
+ kleur: 4.1.5
+ typescript: 6.0.3
+ yargs: 18.2.0
+ transitivePeerDependencies:
+ - '@emnapi/runtime'
+ - prettier
+ - prettier-plugin-astro
+
+ '@astrojs/compiler-binding-android-arm64@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-darwin-arm64@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-darwin-x64@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-linux-arm64-gnu@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-linux-arm64-musl@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-linux-x64-gnu@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-linux-x64-musl@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-wasm32-wasi@0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)':
+ dependencies:
+ '@napi-rs/wasm-runtime': 1.2.5(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)
+ transitivePeerDependencies:
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ optional: true
+
+ '@astrojs/compiler-binding-win32-arm64-msvc@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-win32-x64-msvc@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding@0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)':
+ optionalDependencies:
+ '@astrojs/compiler-binding-android-arm64': 0.4.1
+ '@astrojs/compiler-binding-darwin-arm64': 0.4.1
+ '@astrojs/compiler-binding-darwin-x64': 0.4.1
+ '@astrojs/compiler-binding-linux-arm64-gnu': 0.4.1
+ '@astrojs/compiler-binding-linux-arm64-musl': 0.4.1
+ '@astrojs/compiler-binding-linux-x64-gnu': 0.4.1
+ '@astrojs/compiler-binding-linux-x64-musl': 0.4.1
+ '@astrojs/compiler-binding-wasm32-wasi': 0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)
+ '@astrojs/compiler-binding-win32-arm64-msvc': 0.4.1
+ '@astrojs/compiler-binding-win32-x64-msvc': 0.4.1
+ transitivePeerDependencies:
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+
+ '@astrojs/compiler-rs@0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)':
+ dependencies:
+ '@astrojs/compiler-binding': 0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)
+ transitivePeerDependencies:
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+
+ '@astrojs/internal-helpers@0.11.0':
+ dependencies:
+ '@types/hast': 3.0.5
+ '@types/mdast': 4.0.4
+ js-yaml: 4.3.2
+ picomatch: 4.0.7
+ retext-smartypants: 6.2.0
+ shiki: 4.5.0
+ smol-toml: 1.9.1
+ unified: 11.0.5
+
+ '@astrojs/internal-helpers@0.13.0':
+ dependencies:
+ '@types/hast': 3.0.5
+ '@types/mdast': 4.0.4
+ picomatch: 4.0.7
+ retext-smartypants: 6.2.0
+ shiki: 4.5.0
+ smol-toml: 1.9.1
+ unified: 11.0.5
+ yaml: 2.9.1
+
+ '@astrojs/language-server@2.17.2(@emnapi/runtime@1.11.3)(prettier-plugin-astro@1.1.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(prettier@3.9.10))(prettier@3.9.10)(typescript@6.0.3)':
+ dependencies:
+ '@astrojs/astro2tsx': 0.1.3(@emnapi/runtime@1.11.3)
+ '@astrojs/yaml2ts': 0.2.4
+ '@volar/kit': 2.4.28(typescript@6.0.3)
+ '@volar/language-core': 2.4.28
+ '@volar/language-server': 2.4.28(typescript@6.0.3)
+ '@volar/language-service': 2.4.28(typescript@6.0.3)
+ muggle-string: 0.4.1
+ tinyglobby: 0.2.17
+ volar-service-css: 0.0.71(@volar/language-service@2.4.28(typescript@6.0.3))
+ volar-service-emmet: 0.0.71(@volar/language-service@2.4.28(typescript@6.0.3))
+ volar-service-html: 0.0.71(@volar/language-service@2.4.28(typescript@6.0.3))
+ volar-service-prettier: 0.0.71(@volar/language-service@2.4.28(typescript@6.0.3))(prettier@3.9.10)
+ volar-service-typescript: 0.0.71(@volar/language-service@2.4.28(typescript@6.0.3))(typescript@6.0.3)
+ volar-service-typescript-twoslash-queries: 0.0.71(@volar/language-service@2.4.28(typescript@6.0.3))(typescript@6.0.3)
+ volar-service-yaml: 0.0.71(@volar/language-service@2.4.28(typescript@6.0.3))
+ vscode-html-languageservice: 5.6.2
+ vscode-uri: 3.2.0
+ optionalDependencies:
+ prettier: 3.9.10
+ prettier-plugin-astro: 1.1.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(prettier@3.9.10)
+ transitivePeerDependencies:
+ - '@emnapi/runtime'
+ - typescript
+
+ '@astrojs/markdown-satteri@0.4.1':
+ dependencies:
+ '@astrojs/internal-helpers': 0.11.0
+ '@astrojs/prism': 4.0.2
+ github-slugger: 2.0.0
+ satteri: 0.10.5
+
+ '@astrojs/markdown-satteri@0.4.4':
+ dependencies:
+ '@astrojs/internal-helpers': 0.13.0
+ '@astrojs/prism': 4.1.0
+ github-slugger: 2.0.0
+ satteri: 0.10.5
+
+ '@astrojs/mdx@8.1.0(@astrojs/markdown-satteri@0.4.4)(astro@7.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(typescript@6.0.3)(yaml@2.9.1))':
+ dependencies:
+ '@astrojs/internal-helpers': 0.13.0
+ '@astrojs/markdown-satteri': 0.4.4
+ astro: 7.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(typescript@6.0.3)(yaml@2.9.1)
+ es-module-lexer: 3.0.4
+
+ '@astrojs/prism@4.0.2':
+ dependencies:
+ prismjs: 1.30.0
+
+ '@astrojs/prism@4.1.0':
+ dependencies:
+ prismjs: 1.30.0
+
+ '@astrojs/sitemap@3.7.4':
+ dependencies:
+ sitemap: 9.0.1
+ zod: 4.6.7
+
+ '@astrojs/starlight@0.42.1(astro@7.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(typescript@6.0.3)(yaml@2.9.1))(supports-color@10.2.2)(typescript@6.0.3)':
+ dependencies:
+ '@astrojs/markdown-satteri': 0.4.4
+ '@astrojs/mdx': 8.1.0(@astrojs/markdown-satteri@0.4.4)(astro@7.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(typescript@6.0.3)(yaml@2.9.1))
+ '@astrojs/sitemap': 3.7.4
+ '@pagefind/default-ui': 1.5.2
+ '@types/hast': 3.0.5
+ '@types/js-yaml': 4.0.9
+ '@types/mdast': 4.0.4
+ astro: 7.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(typescript@6.0.3)(yaml@2.9.1)
+ astro-expressive-code: 0.44.2(astro@7.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(typescript@6.0.3)(yaml@2.9.1))
+ bcp-47: 2.1.1
+ hast-util-format: 1.1.0
+ hast-util-select: 6.0.4
+ hast-util-to-html: 9.0.5
+ hast-util-to-string: 3.0.1
+ hastscript: 9.0.1
+ i18next: 26.5.2(typescript@6.0.3)
+ js-yaml: 4.3.2
+ klona: 2.0.6
+ magic-string: 1.4.3
+ mdast-util-directive: 3.1.1(supports-color@10.2.2)
+ mdast-util-to-markdown: 2.2.0
+ mdast-util-to-string: 4.0.0
+ pagefind: 1.5.2
+ remark-directive: 4.0.0(supports-color@10.2.2)
+ satteri: 0.10.5
+ ultrahtml: 1.7.0
+ unified: 11.0.5
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@astrojs/telemetry@3.3.3':
+ dependencies:
+ ci-info: 4.4.0
+ dset: 3.1.4
+ is-docker: 4.0.0
+ package-manager-detector: 1.9.0
+
+ '@astrojs/yaml2ts@0.2.4':
+ dependencies:
+ yaml: 2.9.1
+
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/parser@7.29.9':
+ dependencies:
+ '@babel/types': 7.29.8
+
+ '@babel/runtime@7.29.10': {}
+
+ '@babel/types@7.29.8':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@blazediff/core@1.10.0': {}
+
+ '@bruits/satteri-darwin-arm64@0.10.5':
+ optional: true
+
+ '@bruits/satteri-darwin-x64@0.10.5':
+ optional: true
+
+ '@bruits/satteri-linux-arm64-gnu@0.10.5':
+ optional: true
+
+ '@bruits/satteri-linux-arm64-musl@0.10.5':
+ optional: true
+
+ '@bruits/satteri-linux-x64-gnu@0.10.5':
+ optional: true
+
+ '@bruits/satteri-linux-x64-musl@0.10.5':
+ optional: true
+
+ '@bruits/satteri-wasm32-wasi@0.10.5':
+ dependencies:
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.1
+ '@napi-rs/wasm-runtime': 1.2.5(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
+ optional: true
+
+ '@bruits/satteri-win32-arm64-msvc@0.10.5':
+ optional: true
+
+ '@bruits/satteri-win32-x64-msvc@0.10.5':
+ optional: true
+
+ '@capsizecss/unpack@4.0.1':
+ dependencies:
+ fontkitten: 1.0.3
+
+ '@clack/core@1.5.1':
+ dependencies:
+ fast-wrap-ansi: 0.2.2
+ sisteransi: 1.0.5
+
+ '@clack/prompts@1.8.1':
+ dependencies:
+ '@clack/core': 1.5.1
+ fast-string-width: 3.0.2
+ fast-wrap-ansi: 0.2.2
+ sisteransi: 1.0.5
+
+ '@cloudflare/kv-asset-handler@0.5.0': {}
+
+ '@cloudflare/unenv-preset@2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261012.1)':
+ dependencies:
+ unenv: 2.0.0-rc.24
+ optionalDependencies:
+ workerd: 1.20261012.1
+
+ '@cloudflare/workerd-darwin-64@1.20261012.1':
+ optional: true
+
+ '@cloudflare/workerd-darwin-arm64@1.20261012.1':
+ optional: true
+
+ '@cloudflare/workerd-linux-64@1.20261012.1':
+ optional: true
+
+ '@cloudflare/workerd-linux-arm64@1.20261012.1':
+ optional: true
+
+ '@cloudflare/workerd-windows-64@1.20261012.1':
+ optional: true
+
+ '@codemirror/autocomplete@6.20.3':
+ dependencies:
+ '@codemirror/language': 6.13.1
+ '@codemirror/state': 6.7.6
+ '@codemirror/view': 6.43.14
+ '@lezer/common': 1.5.3
+
+ '@codemirror/commands@6.11.1':
+ dependencies:
+ '@codemirror/language': 6.13.1
+ '@codemirror/state': 6.7.6
+ '@codemirror/view': 6.43.14
+ '@lezer/common': 1.5.3
+
+ '@codemirror/lang-json@6.0.2':
+ dependencies:
+ '@codemirror/language': 6.13.1
+ '@lezer/json': 1.0.3
+
+ '@codemirror/lang-python@6.2.1':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.3
+ '@codemirror/language': 6.13.1
+ '@codemirror/state': 6.7.6
+ '@lezer/common': 1.5.3
+ '@lezer/python': 1.1.19
+
+ '@codemirror/language@6.13.1':
+ dependencies:
+ '@codemirror/state': 6.7.6
+ '@codemirror/streamparser': 6.0.0
+ '@codemirror/view': 6.43.14
+ '@lezer/common': 1.5.3
+ '@lezer/highlight': 1.2.5
+ '@lezer/lr': 1.4.11
+ style-mod: 4.1.4
+
+ '@codemirror/state@6.7.6':
+ dependencies:
+ '@marijn/find-cluster-break': 1.0.4
+
+ '@codemirror/streamparser@6.0.0': {}
+
+ '@codemirror/view@6.43.14':
+ dependencies:
+ '@codemirror/state': 6.7.6
+ crelt: 1.0.7
+ style-mod: 4.1.4
+ w3c-keyname: 2.2.8
+
+ '@cspotcode/source-map-support@0.8.1':
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.9
+
+ '@ctrl/tinycolor@4.2.1': {}
+
+ '@emmetio/abbreviation@2.3.3':
+ dependencies:
+ '@emmetio/scanner': 1.0.4
+
+ '@emmetio/css-abbreviation@2.1.8':
+ dependencies:
+ '@emmetio/scanner': 1.0.4
+
+ '@emmetio/css-parser@0.4.1':
+ dependencies:
+ '@emmetio/stream-reader': 2.2.0
+ '@emmetio/stream-reader-utils': 0.1.0
+
+ '@emmetio/html-matcher@1.3.0':
+ dependencies:
+ '@emmetio/scanner': 1.0.4
+
+ '@emmetio/scanner@1.0.4': {}
+
+ '@emmetio/stream-reader-utils@0.1.0': {}
+
+ '@emmetio/stream-reader@2.2.0': {}
+
+ '@emnapi/core@1.11.1':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.2
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/core@1.11.3':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.3
+ tslib: 2.8.1
+
+ '@emnapi/runtime@1.11.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.11.3':
+ dependencies:
+ tslib: 2.8.1
+
+ '@emnapi/wasi-threads@1.2.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.3':
+ dependencies:
+ tslib: 2.8.1
+
+ '@esbuild/aix-ppc64@0.28.2':
+ optional: true
+
+ '@esbuild/android-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/android-arm@0.28.2':
+ optional: true
+
+ '@esbuild/android-x64@0.28.2':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/darwin-x64@0.28.2':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-arm@0.28.2':
+ optional: true
+
+ '@esbuild/linux-ia32@0.28.2':
+ optional: true
+
+ '@esbuild/linux-loong64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.28.2':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-s390x@0.28.2':
+ optional: true
+
+ '@esbuild/linux-x64@0.28.2':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.28.2':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.28.2':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/sunos-x64@0.28.2':
+ optional: true
+
+ '@esbuild/win32-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/win32-ia32@0.28.2':
+ optional: true
+
+ '@esbuild/win32-x64@0.28.2':
+ optional: true
+
+ '@expressive-code/core@0.44.2':
+ dependencies:
+ '@ctrl/tinycolor': 4.2.1
+ hast-util-select: 6.0.4
+ hast-util-to-html: 9.0.5
+ hast-util-to-text: 4.0.2
+ hastscript: 9.0.1
+ postcss: 8.5.30
+ postcss-nested: 6.2.0(postcss@8.5.30)
+ unist-util-visit: 5.1.0
+ unist-util-visit-parents: 6.0.2
+
+ '@expressive-code/plugin-frames@0.44.2':
+ dependencies:
+ '@expressive-code/core': 0.44.2
+
+ '@expressive-code/plugin-shiki@0.44.2':
+ dependencies:
+ '@expressive-code/core': 0.44.2
+ shiki: 4.5.0
+
+ '@expressive-code/plugin-text-markers@0.44.2':
+ dependencies:
+ '@expressive-code/core': 0.44.2
+
+ '@fontsource-variable/jetbrains-mono@5.3.0': {}
+
+ '@fontsource-variable/mona-sans@5.3.0': {}
+
+ '@img/colour@1.1.0': {}
+
+ '@img/sharp-darwin-arm64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-arm64': 1.3.4
+ optional: true
+
+ '@img/sharp-darwin-x64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-x64': 1.3.4
+ optional: true
+
+ '@img/sharp-freebsd-wasm32@0.35.5':
+ dependencies:
+ '@img/sharp-wasm32': 0.35.5
+ optional: true
+
+ '@img/sharp-libvips-darwin-arm64@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-darwin-x64@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm64@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-ppc64@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-riscv64@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-s390x@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-x64@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-x64@1.3.4':
+ optional: true
+
+ '@img/sharp-linux-arm64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm64': 1.3.4
+ optional: true
+
+ '@img/sharp-linux-arm@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm': 1.3.4
+ optional: true
+
+ '@img/sharp-linux-ppc64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-ppc64': 1.3.4
+ optional: true
+
+ '@img/sharp-linux-riscv64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-riscv64': 1.3.4
+ optional: true
+
+ '@img/sharp-linux-s390x@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-s390x': 1.3.4
+ optional: true
+
+ '@img/sharp-linux-x64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-x64': 1.3.4
+ optional: true
+
+ '@img/sharp-linuxmusl-arm64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.4
+ optional: true
+
+ '@img/sharp-linuxmusl-x64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.4
+ optional: true
+
+ '@img/sharp-wasm32@0.35.5':
+ dependencies:
+ '@emnapi/runtime': 1.11.3
+ optional: true
+
+ '@img/sharp-webcontainers-wasm32@0.35.5':
+ dependencies:
+ '@img/sharp-wasm32': 0.35.5
+ optional: true
+
+ '@img/sharp-win32-arm64@0.35.5':
+ optional: true
+
+ '@img/sharp-win32-ia32@0.35.5':
+ optional: true
+
+ '@img/sharp-win32-x64@0.35.5':
+ optional: true
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.6.0': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.6.0
+
+ '@jridgewell/trace-mapping@0.3.9':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.6.0
+
+ '@lezer/common@1.5.3': {}
+
+ '@lezer/highlight@1.2.5':
+ dependencies:
+ '@lezer/common': 1.5.3
+
+ '@lezer/json@1.0.3':
+ dependencies:
+ '@lezer/common': 1.5.3
+ '@lezer/highlight': 1.2.5
+ '@lezer/lr': 1.4.11
+
+ '@lezer/lr@1.4.11':
+ dependencies:
+ '@lezer/common': 1.5.3
+
+ '@lezer/python@1.1.19':
+ dependencies:
+ '@lezer/common': 1.5.3
+ '@lezer/highlight': 1.2.5
+ '@lezer/lr': 1.4.11
+
+ '@marijn/find-cluster-break@1.0.4': {}
+
+ '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)':
+ dependencies:
+ '@emnapi/core': 1.11.3
+ '@emnapi/runtime': 1.11.3
+ '@tybys/wasm-util': 0.10.4
+
+ '@napi-rs/wasm-runtime@1.2.5(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
+ dependencies:
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.1
+ '@tybys/wasm-util': 0.10.4
+ optional: true
+
+ '@napi-rs/wasm-runtime@1.2.5(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)':
+ dependencies:
+ '@emnapi/core': 1.11.3
+ '@emnapi/runtime': 1.11.3
+ '@tybys/wasm-util': 0.10.4
+ optional: true
+
+ '@oslojs/encoding@1.1.0': {}
+
+ '@oxc-project/runtime@0.154.0': {}
+
+ '@oxc-project/types@0.154.0': {}
+
+ '@oxfmt/binding-android-arm-eabi@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-android-arm64@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-darwin-arm64@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-darwin-x64@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-freebsd-x64@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-linux-arm-gnueabihf@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-linux-arm-musleabihf@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-linux-arm64-gnu@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-linux-arm64-musl@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-linux-ppc64-gnu@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-linux-riscv64-gnu@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-linux-riscv64-musl@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-linux-s390x-gnu@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-linux-x64-gnu@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-linux-x64-musl@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-openharmony-arm64@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-win32-arm64-msvc@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-win32-ia32-msvc@0.73.0':
+ optional: true
+
+ '@oxfmt/binding-win32-x64-msvc@0.73.0':
+ optional: true
+
+ '@oxlint-tsgolint/darwin-arm64@7.0.2004':
+ optional: true
+
+ '@oxlint-tsgolint/darwin-x64@7.0.2004':
+ optional: true
+
+ '@oxlint-tsgolint/linux-arm64@7.0.2004':
+ optional: true
+
+ '@oxlint-tsgolint/linux-x64@7.0.2004':
+ optional: true
+
+ '@oxlint-tsgolint/win32-arm64@7.0.2004':
+ optional: true
+
+ '@oxlint-tsgolint/win32-x64@7.0.2004':
+ optional: true
+
+ '@oxlint/binding-android-arm-eabi@1.88.0':
+ optional: true
+
+ '@oxlint/binding-android-arm64@1.88.0':
+ optional: true
+
+ '@oxlint/binding-darwin-arm64@1.88.0':
+ optional: true
+
+ '@oxlint/binding-darwin-x64@1.88.0':
+ optional: true
+
+ '@oxlint/binding-freebsd-x64@1.88.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm-gnueabihf@1.88.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm-musleabihf@1.88.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm64-gnu@1.88.0':
+ optional: true
+
+ '@oxlint/binding-linux-arm64-musl@1.88.0':
+ optional: true
+
+ '@oxlint/binding-linux-ppc64-gnu@1.88.0':
+ optional: true
+
+ '@oxlint/binding-linux-riscv64-gnu@1.88.0':
+ optional: true
+
+ '@oxlint/binding-linux-riscv64-musl@1.88.0':
+ optional: true
+
+ '@oxlint/binding-linux-s390x-gnu@1.88.0':
+ optional: true
+
+ '@oxlint/binding-linux-x64-gnu@1.88.0':
+ optional: true
+
+ '@oxlint/binding-linux-x64-musl@1.88.0':
+ optional: true
+
+ '@oxlint/binding-openharmony-arm64@1.88.0':
+ optional: true
+
+ '@oxlint/binding-win32-arm64-msvc@1.88.0':
+ optional: true
+
+ '@oxlint/binding-win32-ia32-msvc@1.88.0':
+ optional: true
+
+ '@oxlint/binding-win32-x64-msvc@1.88.0':
+ optional: true
+
+ '@oxlint/plugins@1.88.0': {}
+
+ '@pagefind/darwin-arm64@1.5.2':
+ optional: true
+
+ '@pagefind/darwin-x64@1.5.2':
+ optional: true
+
+ '@pagefind/default-ui@1.5.2': {}
+
+ '@pagefind/freebsd-x64@1.5.2':
+ optional: true
+
+ '@pagefind/linux-arm64@1.5.2':
+ optional: true
+
+ '@pagefind/linux-x64@1.5.2':
+ optional: true
+
+ '@pagefind/windows-arm64@1.5.2':
+ optional: true
+
+ '@pagefind/windows-x64@1.5.2':
+ optional: true
+
+ '@playwright/test@1.64.1':
+ dependencies:
+ playwright: 1.64.1
+
+ '@polka/url@1.0.0-next.29': {}
+
+ '@poppinss/colors@4.1.6':
+ dependencies:
+ kleur: 4.1.5
+
+ '@poppinss/dumper@0.6.5':
+ dependencies:
+ '@poppinss/colors': 4.1.6
+ '@sindresorhus/is': 7.2.0
+ supports-color: 10.2.2
+
+ '@poppinss/exception@1.2.3': {}
+
+ '@prettier/parse-srcset@3.1.0': {}
+
+ '@shikijs/core@4.5.0':
+ dependencies:
+ '@shikijs/primitive': 4.5.0
+ '@shikijs/types': 4.5.0
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.5
+ hast-util-to-html: 9.0.5
+
+ '@shikijs/engine-javascript@4.5.0':
+ dependencies:
+ '@shikijs/types': 4.5.0
+ '@shikijs/vscode-textmate': 10.0.2
+ oniguruma-to-es: 4.3.6
+
+ '@shikijs/engine-oniguruma@4.5.0':
+ dependencies:
+ '@shikijs/types': 4.5.0
+ '@shikijs/vscode-textmate': 10.0.2
+
+ '@shikijs/langs@4.5.0':
+ dependencies:
+ '@shikijs/types': 4.5.0
+
+ '@shikijs/primitive@4.5.0':
+ dependencies:
+ '@shikijs/types': 4.5.0
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.5
+
+ '@shikijs/themes@4.5.0':
+ dependencies:
+ '@shikijs/types': 4.5.0
+
+ '@shikijs/types@4.5.0':
+ dependencies:
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.5
+
+ '@shikijs/vscode-textmate@10.0.2': {}
+
+ '@sindresorhus/is@7.2.0': {}
+
+ '@speed-highlight/core@1.2.24': {}
+
+ '@testing-library/dom@10.4.2':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/runtime': 7.29.10
+ '@types/aria-query': 5.0.4
+ aria-query: 5.3.0
+ dom-accessibility-api: 0.5.16
+ lz-string: 1.5.0
+ picocolors: 1.1.1
+ pretty-format: 27.5.1
+
+ '@testing-library/user-event@14.6.8(@testing-library/dom@10.4.2)':
+ dependencies:
+ '@testing-library/dom': 10.4.2
+
+ '@tybys/wasm-util@0.10.4':
+ dependencies:
+ tslib: 2.8.1
+
+ '@types/aria-query@5.0.4': {}
+
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
+ '@types/debug@4.1.13':
+ dependencies:
+ '@types/ms': 2.1.0
+
+ '@types/deep-eql@4.0.2': {}
+
+ '@types/emscripten@1.41.6': {}
+
+ '@types/estree-jsx@1.0.5':
+ dependencies:
+ '@types/estree': 1.0.9
+
+ '@types/estree@1.0.9': {}
+
+ '@types/hast@3.0.5':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/js-yaml@4.0.9': {}
+
+ '@types/mdast@4.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/ms@2.1.0': {}
+
+ '@types/nlcst@2.0.3':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/node@24.19.2':
+ dependencies:
+ undici-types: 7.24.6
+
+ '@types/node@26.6.5':
+ dependencies:
+ undici-types: 8.9.0
+
+ '@types/sax@1.2.7':
+ dependencies:
+ '@types/node': 26.6.5
+
+ '@types/unist@2.0.11': {}
+
+ '@types/unist@3.0.3': {}
+
+ '@ungap/structured-clone@1.4.0': {}
+
+ '@vitest/browser-preview@5.0.5(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(vitest@5.0.5)(yaml@2.9.1)':
+ dependencies:
+ '@testing-library/dom': 10.4.2
+ '@testing-library/user-event': 14.6.8(@testing-library/dom@10.4.2)
+ '@vitest/browser': 5.0.5(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(vitest@5.0.5)(yaml@2.9.1)
+ vitest: 5.0.5(@types/node@26.6.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1))(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)
+ transitivePeerDependencies:
+ - '@arethetypeswrong/core'
+ - '@types/node'
+ - '@typescript/native-preview'
+ - '@vitejs/devtools'
+ - '@volar/typescript'
+ - '@vue/language-core'
+ - bufferutil
+ - esbuild
+ - jiti
+ - less
+ - msw
+ - publint
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - terser
+ - tsx
+ - typescript
+ - unplugin-unused
+ - unrun
+ - utf-8-validate
+ - vue-tsc
+ - yaml
+
+ '@vitest/browser@5.0.5(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(vitest@5.0.5)(yaml@2.9.1)':
+ dependencies:
+ '@blazediff/core': 1.10.0
+ '@vitest/mocker': 5.0.5(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)
+ '@vitest/ui': 5.0.5(vitest@5.0.5)
+ '@vitest/utils': 5.0.5
+ magic-string: 1.4.3
+ pngjs: 7.0.0
+ sirv: 3.0.2
+ tinyrainbow: 3.2.0
+ vitest: 5.0.5(@types/node@26.6.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1))(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)
+ ws: 8.22.0
+ transitivePeerDependencies:
+ - '@arethetypeswrong/core'
+ - '@types/node'
+ - '@typescript/native-preview'
+ - '@vitejs/devtools'
+ - '@volar/typescript'
+ - '@vue/language-core'
+ - bufferutil
+ - esbuild
+ - jiti
+ - less
+ - msw
+ - publint
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - terser
+ - tsx
+ - typescript
+ - unplugin-unused
+ - unrun
+ - utf-8-validate
+ - vue-tsc
+ - yaml
+
+ '@vitest/mocker@5.0.5(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)':
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ '@vitest/spy': 5.0.5
+ estree-walker: 3.0.3
+ magic-string: 1.4.3
+ vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)'
+ transitivePeerDependencies:
+ - '@arethetypeswrong/core'
+ - '@types/node'
+ - '@typescript/native-preview'
+ - '@vitejs/devtools'
+ - '@volar/typescript'
+ - '@vue/language-core'
+ - esbuild
+ - jiti
+ - less
+ - publint
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - terser
+ - tsx
+ - typescript
+ - unplugin-unused
+ - unrun
+ - vue-tsc
+ - yaml
+
+ '@vitest/pretty-format@5.0.5':
+ dependencies:
+ tinyrainbow: 3.2.0
+
+ '@vitest/snapshot@5.0.5':
+ dependencies:
+ '@vitest/pretty-format': 5.0.5
+ '@vitest/utils': 5.0.5
+ magic-string: 1.4.3
+ pathe: 2.0.3
+
+ '@vitest/spy@5.0.5': {}
+
+ '@vitest/ui@5.0.5(vitest@5.0.5)':
+ dependencies:
+ '@vitest/utils': 5.0.5
+ fflate: 0.8.3
+ flatted: 3.4.4
+ pathe: 2.0.3
+ sirv: 3.0.2
+ tinyrainbow: 3.2.0
+ vitest: 5.0.5(@types/node@26.6.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1))(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)
+
+ '@vitest/utils@5.0.5':
+ dependencies:
+ '@vitest/pretty-format': 5.0.5
+ convert-source-map: 2.0.0
+ tinyrainbow: 3.2.0
+
+ '@voidzero-dev/vite-plus-core@1.3.0(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)':
+ dependencies:
+ '@oxc-project/runtime': 0.154.0
+ '@oxc-project/types': 0.154.0
+ lightningcss: 1.33.0
+ postcss: 8.5.30
+ yuku-parser: 0.19.0
+ optionalDependencies:
+ '@types/node': 26.6.5
+ '@voidzero-dev/vite-plus-darwin-arm64': 1.3.0
+ '@voidzero-dev/vite-plus-darwin-x64': 1.3.0
+ '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.3.0
+ '@voidzero-dev/vite-plus-linux-arm64-musl': 1.3.0
+ '@voidzero-dev/vite-plus-linux-x64-gnu': 1.3.0
+ '@voidzero-dev/vite-plus-linux-x64-musl': 1.3.0
+ '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.3.0
+ '@voidzero-dev/vite-plus-win32-x64-msvc': 1.3.0
+ '@volar/typescript': 2.4.28(typescript@6.0.3)
+ esbuild: 0.28.2
+ fsevents: 2.3.3
+ typescript: 6.0.3
+ yaml: 2.9.1
+
+ '@voidzero-dev/vite-plus-darwin-arm64@1.3.0':
+ optional: true
+
+ '@voidzero-dev/vite-plus-darwin-x64@1.3.0':
+ optional: true
+
+ '@voidzero-dev/vite-plus-linux-arm64-gnu@1.3.0':
+ optional: true
+
+ '@voidzero-dev/vite-plus-linux-arm64-musl@1.3.0':
+ optional: true
+
+ '@voidzero-dev/vite-plus-linux-x64-gnu@1.3.0':
+ optional: true
+
+ '@voidzero-dev/vite-plus-linux-x64-musl@1.3.0':
+ optional: true
+
+ '@voidzero-dev/vite-plus-win32-arm64-msvc@1.3.0':
+ optional: true
+
+ '@voidzero-dev/vite-plus-win32-x64-msvc@1.3.0':
+ optional: true
+
+ '@volar/kit@2.4.28(typescript@6.0.3)':
+ dependencies:
+ '@volar/language-service': 2.4.28(typescript@6.0.3)
+ '@volar/typescript': 2.4.28(typescript@6.0.3)
+ typesafe-path: 0.2.2
+ typescript: 6.0.3
+ vscode-languageserver-textdocument: 1.0.15
+ vscode-uri: 3.2.0
+
+ '@volar/language-core@2.4.28':
+ dependencies:
+ '@volar/source-map': 2.4.28
+
+ '@volar/language-server@2.4.28(typescript@6.0.3)':
+ dependencies:
+ '@volar/language-core': 2.4.28
+ '@volar/language-service': 2.4.28(typescript@6.0.3)
+ '@volar/typescript': 2.4.28(typescript@6.0.3)
+ path-browserify: 1.0.1
+ request-light: 0.7.0
+ vscode-languageserver: 9.0.1
+ vscode-languageserver-protocol: 3.18.4
+ vscode-languageserver-textdocument: 1.0.15
+ vscode-uri: 3.2.0
+ optionalDependencies:
+ typescript: 6.0.3
+
+ '@volar/language-service@2.4.28(typescript@6.0.3)':
+ dependencies:
+ '@volar/language-core': 2.4.28
+ vscode-languageserver-protocol: 3.18.4
+ vscode-languageserver-textdocument: 1.0.15
+ vscode-uri: 3.2.0
+ optionalDependencies:
+ typescript: 6.0.3
+
+ '@volar/source-map@2.4.28': {}
+
+ '@volar/typescript@2.4.28(typescript@6.0.3)':
+ dependencies:
+ '@volar/language-core': 2.4.28
+ path-browserify: 1.0.1
+ vscode-uri: 3.2.0
+ optionalDependencies:
+ typescript: 6.0.3
+
+ '@vscode/emmet-helper@2.11.0':
+ dependencies:
+ emmet: 2.4.11
+ jsonc-parser: 2.3.1
+ vscode-languageserver-textdocument: 1.0.15
+ vscode-languageserver-types: 3.18.4
+ vscode-uri: 3.2.0
+
+ '@vscode/l10n@0.0.18': {}
+
+ '@yuku-core/binding-android-arm64@0.19.0':
+ optional: true
+
+ '@yuku-core/binding-darwin-arm64@0.19.0':
+ optional: true
+
+ '@yuku-core/binding-darwin-x64@0.19.0':
+ optional: true
+
+ '@yuku-core/binding-freebsd-x64@0.19.0':
+ optional: true
+
+ '@yuku-core/binding-linux-arm-gnu@0.19.0':
+ optional: true
+
+ '@yuku-core/binding-linux-arm-musl@0.19.0':
+ optional: true
+
+ '@yuku-core/binding-linux-arm64-gnu@0.19.0':
+ optional: true
+
+ '@yuku-core/binding-linux-arm64-musl@0.19.0':
+ optional: true
+
+ '@yuku-core/binding-linux-x64-gnu@0.19.0':
+ optional: true
+
+ '@yuku-core/binding-linux-x64-musl@0.19.0':
+ optional: true
+
+ '@yuku-core/binding-win32-arm64@0.19.0':
+ optional: true
+
+ '@yuku-core/binding-win32-x64@0.19.0':
+ optional: true
+
+ '@yuku-toolchain/types@0.19.0': {}
+
+ ajv-draft-04@1.0.0(ajv@8.20.0):
+ optionalDependencies:
+ ajv: 8.20.0
+
+ ajv-i18n@4.2.0(ajv@8.20.0):
+ dependencies:
+ ajv: 8.20.0
+
+ ajv@8.20.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-uri: 3.1.8
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+
+ am-i-vibing@0.4.0:
+ dependencies:
+ process-ancestry: 0.1.0
+
+ ansi-regex@5.0.1: {}
+
+ ansi-regex@6.4.0: {}
+
+ ansi-styles@5.2.0: {}
+
+ ansi-styles@6.2.3: {}
+
+ anymatch@3.1.3:
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.2
+
+ arg@5.0.2: {}
+
+ argparse@2.0.1: {}
+
+ aria-query@5.3.0:
+ dependencies:
+ dequal: 2.0.3
+
+ aria-query@5.3.2: {}
+
+ assertion-error@2.0.1: {}
+
+ astro-expressive-code@0.44.2(astro@7.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(typescript@6.0.3)(yaml@2.9.1)):
+ dependencies:
+ astro: 7.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(typescript@6.0.3)(yaml@2.9.1)
+ rehype-expressive-code: 0.44.2
+ url-extras: 0.1.0
+
+ astro@7.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(typescript@6.0.3)(yaml@2.9.1):
+ dependencies:
+ '@astrojs/compiler-rs': 0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)
+ '@astrojs/internal-helpers': 0.11.0
+ '@astrojs/markdown-satteri': 0.4.1
+ '@astrojs/telemetry': 3.3.3
+ '@capsizecss/unpack': 4.0.1
+ '@clack/prompts': 1.8.1
+ '@oslojs/encoding': 1.1.0
+ am-i-vibing: 0.4.0
+ aria-query: 5.3.2
+ axobject-query: 4.1.0
+ ci-info: 4.4.0
+ clsx: 2.1.1
+ common-ancestor-path: 2.0.0
+ cookie: 2.0.1
+ devalue: 5.9.4
+ diff: 9.0.0
+ dset: 3.1.4
+ es-module-lexer: 2.3.2
+ esbuild: 0.28.2
+ find-proc: 0.1.0
+ flattie: 1.1.1
+ fontace: 0.4.1
+ get-tsconfig: 5.0.0-beta.4
+ github-slugger: 2.0.0
+ html-escaper: 3.0.3
+ http-cache-semantics: 4.3.0
+ js-yaml: 4.3.2
+ jsonc-parser: 3.3.1
+ magic-string: 1.4.3
+ magicast: 0.5.5
+ mrmime: 2.0.1
+ neotraverse: 1.0.1
+ obug: 2.2.1
+ p-limit: 7.3.3
+ p-queue: 9.3.5
+ package-manager-detector: 1.9.0
+ piccolore: 0.1.3
+ picomatch: 4.0.7
+ semver: 7.8.5
+ shiki: 4.5.0
+ smol-toml: 1.9.1
+ svgo: 4.1.0
+ tinyclip: 0.1.15
+ tinyexec: 1.3.1
+ tinyglobby: 0.2.17
+ ultrahtml: 1.7.0
+ unifont: 0.7.5
+ unstorage: 1.17.6
+ vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)'
+ vitefu: 1.1.3(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)
+ xxhash-wasm: 1.1.0
+ yargs-parser: 22.0.0
+ zod: 4.6.7
+ optionalDependencies:
+ sharp: 0.35.5(@types/node@26.6.5)
+ transitivePeerDependencies:
+ - '@arethetypeswrong/core'
+ - '@azure/app-configuration'
+ - '@azure/cosmos'
+ - '@azure/data-tables'
+ - '@azure/identity'
+ - '@azure/keyvault-secrets'
+ - '@azure/storage-blob'
+ - '@capacitor/preferences'
+ - '@deno/kv'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@netlify/blobs'
+ - '@planetscale/database'
+ - '@types/node'
+ - '@typescript/native-preview'
+ - '@upstash/redis'
+ - '@vercel/blob'
+ - '@vercel/functions'
+ - '@vercel/kv'
+ - '@vitejs/devtools'
+ - '@volar/typescript'
+ - '@vue/language-core'
+ - aws4fetch
+ - db0
+ - idb-keyval
+ - ioredis
+ - jiti
+ - less
+ - publint
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - terser
+ - tsx
+ - typescript
+ - unplugin-unused
+ - unrun
+ - uploadthing
+ - vue-tsc
+ - yaml
+
+ axobject-query@4.1.0: {}
+
+ bail@2.0.2: {}
+
+ bcp-47-match@2.0.3: {}
+
+ bcp-47@2.1.1:
+ dependencies:
+ is-alphabetical: 2.0.1
+ is-alphanumerical: 2.0.1
+ is-decimal: 2.0.1
+
+ blake3-wasm@2.1.5: {}
+
+ boolbase@1.0.0: {}
+
+ ccount@2.0.1: {}
+
+ chai@6.3.0: {}
+
+ character-entities-html4@2.1.0: {}
+
+ character-entities-legacy@3.0.0: {}
+
+ character-entities@2.0.2: {}
+
+ character-reference-invalid@2.0.1: {}
+
+ chokidar@4.0.3:
+ dependencies:
+ readdirp: 4.1.2
+
+ chokidar@5.0.0:
+ dependencies:
+ readdirp: 5.1.1
+
+ ci-info@4.4.0: {}
+
+ cliui@9.0.1:
+ dependencies:
+ string-width: 7.2.0
+ strip-ansi: 7.2.0
+ wrap-ansi: 9.0.2
+
+ clsx@2.1.1: {}
+
+ comma-separated-tokens@2.0.3: {}
+
+ commander@11.1.0: {}
+
+ common-ancestor-path@2.0.0: {}
+
+ convert-source-map@2.0.0: {}
+
+ cookie-es@1.2.3: {}
+
+ cookie@1.1.1: {}
+
+ cookie@2.0.1: {}
+
+ crelt@1.0.7: {}
+
+ crossws@0.3.5:
+ dependencies:
+ uncrypto: 0.1.3
+
+ css-select@6.0.0:
+ dependencies:
+ boolbase: 1.0.0
+ css-what: 7.0.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ nth-check: 2.1.1
+
+ css-selector-parser@3.3.0: {}
+
+ css-tree@2.2.1:
+ dependencies:
+ mdn-data: 2.0.28
+ source-map-js: 1.2.2
+
+ css-tree@3.2.1:
+ dependencies:
+ mdn-data: 2.27.1
+ source-map-js: 1.2.2
+
+ css-what@7.0.0: {}
+
+ cssesc@3.0.0: {}
+
+ csso@5.0.5:
+ dependencies:
+ css-tree: 2.2.1
+
+ debug@4.4.3(supports-color@10.2.2):
+ dependencies:
+ ms: 2.1.3
+ optionalDependencies:
+ supports-color: 10.2.2
+
+ decode-named-character-reference@1.3.0:
+ dependencies:
+ character-entities: 2.0.2
+
+ defu@6.1.7: {}
+
+ dequal@2.0.3: {}
+
+ destr@2.0.5: {}
+
+ detect-libc@2.1.2: {}
+
+ devalue@5.9.4: {}
+
+ devlop@1.1.0:
+ dependencies:
+ dequal: 2.0.3
+
+ diff@9.0.0: {}
+
+ direction@2.0.1: {}
+
+ dom-accessibility-api@0.5.16: {}
+
+ dom-serializer@2.0.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ entities: 4.5.0
+
+ domelementtype@2.3.0: {}
+
+ domhandler@5.0.3:
+ dependencies:
+ domelementtype: 2.3.0
+
+ domutils@3.2.2:
+ dependencies:
+ dom-serializer: 2.0.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+
+ dset@3.1.4: {}
+
+ emmet@2.4.11:
+ dependencies:
+ '@emmetio/abbreviation': 2.3.3
+ '@emmetio/css-abbreviation': 2.1.8
+
+ emoji-regex@10.6.0: {}
+
+ entities@4.5.0: {}
+
+ error-stack-parser-es@1.0.5: {}
+
+ es-module-lexer@2.3.2: {}
+
+ es-module-lexer@3.0.4: {}
+
+ esbuild@0.28.2:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.28.2
+ '@esbuild/android-arm': 0.28.2
+ '@esbuild/android-arm64': 0.28.2
+ '@esbuild/android-x64': 0.28.2
+ '@esbuild/darwin-arm64': 0.28.2
+ '@esbuild/darwin-x64': 0.28.2
+ '@esbuild/freebsd-arm64': 0.28.2
+ '@esbuild/freebsd-x64': 0.28.2
+ '@esbuild/linux-arm': 0.28.2
+ '@esbuild/linux-arm64': 0.28.2
+ '@esbuild/linux-ia32': 0.28.2
+ '@esbuild/linux-loong64': 0.28.2
+ '@esbuild/linux-mips64el': 0.28.2
+ '@esbuild/linux-ppc64': 0.28.2
+ '@esbuild/linux-riscv64': 0.28.2
+ '@esbuild/linux-s390x': 0.28.2
+ '@esbuild/linux-x64': 0.28.2
+ '@esbuild/netbsd-arm64': 0.28.2
+ '@esbuild/netbsd-x64': 0.28.2
+ '@esbuild/openbsd-arm64': 0.28.2
+ '@esbuild/openbsd-x64': 0.28.2
+ '@esbuild/openharmony-arm64': 0.28.2
+ '@esbuild/sunos-x64': 0.28.2
+ '@esbuild/win32-arm64': 0.28.2
+ '@esbuild/win32-ia32': 0.28.2
+ '@esbuild/win32-x64': 0.28.2
+
+ escalade@3.2.0: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+
+ eventemitter3@5.0.4: {}
+
+ expect-type@1.4.0: {}
+
+ expressive-code@0.44.2:
+ dependencies:
+ '@expressive-code/core': 0.44.2
+ '@expressive-code/plugin-frames': 0.44.2
+ '@expressive-code/plugin-shiki': 0.44.2
+ '@expressive-code/plugin-text-markers': 0.44.2
+
+ extend@3.0.2: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-string-truncated-width@3.0.3: {}
+
+ fast-string-width@3.0.2:
+ dependencies:
+ fast-string-truncated-width: 3.0.3
+
+ fast-uri@3.1.8: {}
+
+ fast-wrap-ansi@0.2.2:
+ dependencies:
+ fast-string-width: 3.0.2
+
+ fdir@6.5.0(picomatch@4.0.7):
+ optionalDependencies:
+ picomatch: 4.0.7
+
+ fflate@0.8.3: {}
+
+ find-proc@0.1.0: {}
+
+ flatted@3.4.4: {}
+
+ flattie@1.1.1: {}
+
+ fontace@0.4.1:
+ dependencies:
+ fontkitten: 1.0.3
+
+ fontkitten@1.0.3:
+ dependencies:
+ tiny-inflate: 1.0.3
+
+ fsevents@2.3.3:
+ optional: true
+
+ get-caller-file@2.0.5: {}
+
+ get-east-asian-width@1.7.0: {}
+
+ get-tsconfig@5.0.0-beta.4:
+ dependencies:
+ resolve-pkg-maps: 1.0.0
+
+ github-slugger@2.0.0: {}
+
+ h3@1.15.11:
+ dependencies:
+ cookie-es: 1.2.3
+ crossws: 0.3.5
+ defu: 6.1.7
+ destr: 2.0.5
+ iron-webcrypto: 1.2.1
+ node-mock-http: 1.0.5
+ radix3: 1.1.2
+ ufo: 1.6.4
+ uncrypto: 0.1.3
+
+ hast-util-embedded@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.5
+ hast-util-is-element: 3.0.0
+
+ hast-util-format@1.1.0:
+ dependencies:
+ '@types/hast': 3.0.5
+ hast-util-embedded: 3.0.0
+ hast-util-minify-whitespace: 1.0.1
+ hast-util-phrasing: 3.0.1
+ hast-util-whitespace: 3.0.0
+ html-whitespace-sensitive-tag-names: 3.0.1
+ unist-util-visit-parents: 6.0.2
+
+ hast-util-has-property@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.5
+
+ hast-util-is-body-ok-link@3.0.1:
+ dependencies:
+ '@types/hast': 3.0.5
+
+ hast-util-is-element@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.5
+
+ hast-util-minify-whitespace@1.0.1:
+ dependencies:
+ '@types/hast': 3.0.5
+ hast-util-embedded: 3.0.0
+ hast-util-is-element: 3.0.0
+ hast-util-whitespace: 3.0.0
+ unist-util-is: 6.0.1
+
+ hast-util-parse-selector@4.0.0:
+ dependencies:
+ '@types/hast': 3.0.5
+
+ hast-util-phrasing@3.0.1:
+ dependencies:
+ '@types/hast': 3.0.5
+ hast-util-embedded: 3.0.0
+ hast-util-has-property: 3.0.0
+ hast-util-is-body-ok-link: 3.0.1
+ hast-util-is-element: 3.0.0
+
+ hast-util-select@6.0.4:
+ dependencies:
+ '@types/hast': 3.0.5
+ '@types/unist': 3.0.3
+ bcp-47-match: 2.0.3
+ comma-separated-tokens: 2.0.3
+ css-selector-parser: 3.3.0
+ devlop: 1.1.0
+ direction: 2.0.1
+ hast-util-has-property: 3.0.0
+ hast-util-to-string: 3.0.1
+ hast-util-whitespace: 3.0.0
+ nth-check: 2.1.1
+ property-information: 7.2.0
+ space-separated-tokens: 2.0.2
+ unist-util-visit: 5.1.0
+ zwitch: 2.0.4
+
+ hast-util-to-html@9.0.5:
+ dependencies:
+ '@types/hast': 3.0.5
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ comma-separated-tokens: 2.0.3
+ hast-util-whitespace: 3.0.0
+ html-void-elements: 3.0.0
+ mdast-util-to-hast: 13.2.1
+ property-information: 7.2.0
+ space-separated-tokens: 2.0.2
+ stringify-entities: 4.0.4
+ zwitch: 2.0.4
+
+ hast-util-to-string@3.0.1:
+ dependencies:
+ '@types/hast': 3.0.5
+
+ hast-util-to-text@4.0.2:
+ dependencies:
+ '@types/hast': 3.0.5
+ '@types/unist': 3.0.3
+ hast-util-is-element: 3.0.0
+ unist-util-find-after: 5.0.0
+
+ hast-util-whitespace@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.5
+
+ hastscript@9.0.1:
+ dependencies:
+ '@types/hast': 3.0.5
+ comma-separated-tokens: 2.0.3
+ hast-util-parse-selector: 4.0.0
+ property-information: 7.2.0
+ space-separated-tokens: 2.0.2
+
+ html-escaper@3.0.3: {}
+
+ html-void-elements@3.0.0: {}
+
+ html-whitespace-sensitive-tag-names@3.0.1: {}
+
+ http-cache-semantics@4.3.0: {}
+
+ i18next@26.5.2(typescript@6.0.3):
+ optionalDependencies:
+ typescript: 6.0.3
+
+ iron-webcrypto@1.2.1: {}
+
+ is-alphabetical@2.0.1: {}
+
+ is-alphanumerical@2.0.1:
+ dependencies:
+ is-alphabetical: 2.0.1
+ is-decimal: 2.0.1
+
+ is-decimal@2.0.1: {}
+
+ is-docker@4.0.0: {}
+
+ is-hexadecimal@2.0.1: {}
+
+ is-plain-obj@4.1.0: {}
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@4.3.2:
+ dependencies:
+ argparse: 2.0.1
+
+ json-schema-traverse@1.0.0: {}
+
+ jsonc-parser@2.3.1: {}
+
+ jsonc-parser@3.3.1: {}
+
+ kleur@4.1.5: {}
+
+ klona@2.0.6: {}
+
+ lightningcss-android-arm64@1.33.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.33.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.33.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.33.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.33.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.33.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.33.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.33.0:
+ optional: true
+
+ lightningcss@1.33.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.33.0
+ lightningcss-darwin-arm64: 1.33.0
+ lightningcss-darwin-x64: 1.33.0
+ lightningcss-freebsd-x64: 1.33.0
+ lightningcss-linux-arm-gnueabihf: 1.33.0
+ lightningcss-linux-arm64-gnu: 1.33.0
+ lightningcss-linux-arm64-musl: 1.33.0
+ lightningcss-linux-x64-gnu: 1.33.0
+ lightningcss-linux-x64-musl: 1.33.0
+ lightningcss-win32-arm64-msvc: 1.33.0
+ lightningcss-win32-x64-msvc: 1.33.0
+
+ longest-streak@3.1.0: {}
+
+ lru-cache@11.5.3: {}
+
+ lz-string@1.5.0: {}
+
+ magic-string@1.4.3:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.6.0
+
+ magicast@0.5.5:
+ dependencies:
+ '@babel/parser': 7.29.9
+ '@babel/types': 7.29.8
+ source-map-js: 1.2.2
+
+ mdast-util-directive@3.1.1(supports-color@10.2.2):
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.1.0(supports-color@10.2.2)
+ mdast-util-to-markdown: 2.2.0
+ micromark-util-character: 2.1.1
+ parse-entities: 4.0.2
+ stringify-entities: 4.0.4
+ unist-util-visit-parents: 6.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-from-markdown@2.1.0(supports-color@10.2.2):
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ mdast-util-to-string: 4.0.0
+ micromark: 4.0.3(supports-color@10.2.2)
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-decode-string: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.3
+ unist-util-stringify-position: 4.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-phrasing@4.1.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ unist-util-is: 6.0.1
+
+ mdast-util-to-hast@13.2.1:
+ dependencies:
+ '@types/hast': 3.0.5
+ '@types/mdast': 4.0.4
+ '@ungap/structured-clone': 1.4.0
+ devlop: 1.1.0
+ micromark-util-sanitize-uri: 2.0.1
+ trim-lines: 3.0.1
+ unist-util-position: 5.0.0
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+
+ mdast-util-to-markdown@2.2.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ longest-streak: 3.1.0
+ mdast-util-phrasing: 4.1.0
+ mdast-util-to-string: 4.0.0
+ micromark-util-character: 2.1.1
+ micromark-util-classify-character: 2.0.1
+ micromark-util-decode-string: 2.0.1
+ micromark-util-html-tag-name: 2.0.1
+ unist-util-visit: 5.1.0
+ zwitch: 2.0.4
+
+ mdast-util-to-string@4.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+
+ mdn-data@2.0.28: {}
+
+ mdn-data@2.27.1: {}
+
+ micromark-core-commonmark@2.0.4:
+ dependencies:
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ micromark-factory-destination: 2.0.1
+ micromark-factory-label: 2.0.1
+ micromark-factory-space: 2.1.0
+ micromark-factory-title: 2.0.1
+ micromark-factory-whitespace: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-classify-character: 2.0.1
+ micromark-util-edit-map: 1.0.0
+ micromark-util-html-tag-name: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.3
+
+ micromark-extension-directive@4.0.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-factory-space: 2.1.0
+ micromark-factory-whitespace: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.3
+ parse-entities: 4.0.2
+
+ micromark-factory-destination@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.3
+
+ micromark-factory-label@2.0.1:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.3
+
+ micromark-factory-space@2.1.0:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-types: 2.0.3
+
+ micromark-factory-title@2.0.1:
+ dependencies:
+ micromark-factory-space: 2.1.0
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.3
+
+ micromark-factory-whitespace@2.0.1:
+ dependencies:
+ micromark-factory-space: 2.1.0
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.3
+
+ micromark-util-character@2.1.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.3
+
+ micromark-util-chunked@2.0.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-classify-character@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.3
+
+ micromark-util-combine-extensions@2.0.1:
+ dependencies:
+ micromark-util-chunked: 2.0.1
+ micromark-util-types: 2.0.3
+
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-decode-string@2.0.1:
+ dependencies:
+ decode-named-character-reference: 1.3.0
+ micromark-util-character: 2.1.1
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-edit-map@1.0.0:
+ dependencies:
+ micromark-util-types: 2.0.3
+
+ micromark-util-encode@2.0.1: {}
+
+ micromark-util-html-tag-name@2.0.1: {}
+
+ micromark-util-normalize-identifier@2.0.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-resolve-all@2.0.1:
+ dependencies:
+ micromark-util-types: 2.0.3
+
+ micromark-util-sanitize-uri@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-encode: 2.0.1
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-subtokenize@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-chunked: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.3
+
+ micromark-util-symbol@2.0.1: {}
+
+ micromark-util-types@2.0.3: {}
+
+ micromark@4.0.3(supports-color@10.2.2):
+ dependencies:
+ '@types/debug': 4.1.13
+ debug: 4.4.3(supports-color@10.2.2)
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.4
+ micromark-factory-space: 2.1.0
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-edit-map: 1.0.0
+ micromark-util-encode: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ miniflare@5.20261012.1-beta(@types/node@26.6.5):
+ dependencies:
+ '@cspotcode/source-map-support': 0.8.1
+ sharp: 0.35.5(@types/node@26.6.5)
+ undici: 7.29.1
+ workerd: 1.20261012.1
+ ws: 8.21.0
+ youch: 4.1.0-beta.10
+ transitivePeerDependencies:
+ - '@types/node'
+ - bufferutil
+ - utf-8-validate
+
+ mrmime@2.0.1: {}
+
+ ms@2.1.3: {}
+
+ muggle-string@0.4.1: {}
+
+ nanoid@3.3.20: {}
+
+ neotraverse@1.0.1: {}
+
+ nlcst-to-string@4.0.0:
+ dependencies:
+ '@types/nlcst': 2.0.3
+
+ node-fetch-native@1.6.7: {}
+
+ node-mock-http@1.0.5: {}
+
+ normalize-path@3.0.0: {}
+
+ nth-check@2.1.1:
+ dependencies:
+ boolbase: 1.0.0
+
+ obug@2.2.1: {}
+
+ obug@3.0.0: {}
+
+ ofetch@1.5.1:
+ dependencies:
+ destr: 2.0.5
+ node-fetch-native: 1.6.7
+ ufo: 1.6.4
+
+ ohash@2.0.12: {}
+
+ oniguruma-parser@0.12.2: {}
+
+ oniguruma-to-es@4.3.6:
+ dependencies:
+ oniguruma-parser: 0.12.2
+ regex: 6.1.0
+ regex-recursion: 6.0.2
+
+ oxfmt@0.73.0(@astrojs/compiler-rs@0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3))(vite-plus@1.3.0(@astrojs/compiler-rs@0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3))(@types/node@26.6.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(playwright@1.64.1)(typescript@6.0.3)(yaml@2.9.1)):
+ dependencies:
+ tinypool: 2.2.0
+ optionalDependencies:
+ '@astrojs/compiler-rs': 0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)
+ '@oxfmt/binding-android-arm-eabi': 0.73.0
+ '@oxfmt/binding-android-arm64': 0.73.0
+ '@oxfmt/binding-darwin-arm64': 0.73.0
+ '@oxfmt/binding-darwin-x64': 0.73.0
+ '@oxfmt/binding-freebsd-x64': 0.73.0
+ '@oxfmt/binding-linux-arm-gnueabihf': 0.73.0
+ '@oxfmt/binding-linux-arm-musleabihf': 0.73.0
+ '@oxfmt/binding-linux-arm64-gnu': 0.73.0
+ '@oxfmt/binding-linux-arm64-musl': 0.73.0
+ '@oxfmt/binding-linux-ppc64-gnu': 0.73.0
+ '@oxfmt/binding-linux-riscv64-gnu': 0.73.0
+ '@oxfmt/binding-linux-riscv64-musl': 0.73.0
+ '@oxfmt/binding-linux-s390x-gnu': 0.73.0
+ '@oxfmt/binding-linux-x64-gnu': 0.73.0
+ '@oxfmt/binding-linux-x64-musl': 0.73.0
+ '@oxfmt/binding-openharmony-arm64': 0.73.0
+ '@oxfmt/binding-win32-arm64-msvc': 0.73.0
+ '@oxfmt/binding-win32-ia32-msvc': 0.73.0
+ '@oxfmt/binding-win32-x64-msvc': 0.73.0
+ vite-plus: 1.3.0(@astrojs/compiler-rs@0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3))(@types/node@26.6.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(playwright@1.64.1)(typescript@6.0.3)(yaml@2.9.1)
+
+ oxlint-tsgolint@7.0.2004:
+ optionalDependencies:
+ '@oxlint-tsgolint/darwin-arm64': 7.0.2004
+ '@oxlint-tsgolint/darwin-x64': 7.0.2004
+ '@oxlint-tsgolint/linux-arm64': 7.0.2004
+ '@oxlint-tsgolint/linux-x64': 7.0.2004
+ '@oxlint-tsgolint/win32-arm64': 7.0.2004
+ '@oxlint-tsgolint/win32-x64': 7.0.2004
+
+ oxlint@1.88.0(oxlint-tsgolint@7.0.2004)(vite-plus@1.3.0(@astrojs/compiler-rs@0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3))(@types/node@26.6.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(playwright@1.64.1)(typescript@6.0.3)(yaml@2.9.1)):
+ optionalDependencies:
+ '@oxlint/binding-android-arm-eabi': 1.88.0
+ '@oxlint/binding-android-arm64': 1.88.0
+ '@oxlint/binding-darwin-arm64': 1.88.0
+ '@oxlint/binding-darwin-x64': 1.88.0
+ '@oxlint/binding-freebsd-x64': 1.88.0
+ '@oxlint/binding-linux-arm-gnueabihf': 1.88.0
+ '@oxlint/binding-linux-arm-musleabihf': 1.88.0
+ '@oxlint/binding-linux-arm64-gnu': 1.88.0
+ '@oxlint/binding-linux-arm64-musl': 1.88.0
+ '@oxlint/binding-linux-ppc64-gnu': 1.88.0
+ '@oxlint/binding-linux-riscv64-gnu': 1.88.0
+ '@oxlint/binding-linux-riscv64-musl': 1.88.0
+ '@oxlint/binding-linux-s390x-gnu': 1.88.0
+ '@oxlint/binding-linux-x64-gnu': 1.88.0
+ '@oxlint/binding-linux-x64-musl': 1.88.0
+ '@oxlint/binding-openharmony-arm64': 1.88.0
+ '@oxlint/binding-win32-arm64-msvc': 1.88.0
+ '@oxlint/binding-win32-ia32-msvc': 1.88.0
+ '@oxlint/binding-win32-x64-msvc': 1.88.0
+ oxlint-tsgolint: 7.0.2004
+ vite-plus: 1.3.0(@astrojs/compiler-rs@0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3))(@types/node@26.6.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(playwright@1.64.1)(typescript@6.0.3)(yaml@2.9.1)
+
+ p-limit@7.3.3:
+ dependencies:
+ yocto-queue: 1.2.2
+
+ p-queue@9.3.5:
+ dependencies:
+ eventemitter3: 5.0.4
+ p-timeout: 7.0.2
+
+ p-timeout@7.0.2: {}
+
+ package-manager-detector@1.9.0: {}
+
+ pagefind@1.5.2:
+ optionalDependencies:
+ '@pagefind/darwin-arm64': 1.5.2
+ '@pagefind/darwin-x64': 1.5.2
+ '@pagefind/freebsd-x64': 1.5.2
+ '@pagefind/linux-arm64': 1.5.2
+ '@pagefind/linux-x64': 1.5.2
+ '@pagefind/windows-arm64': 1.5.2
+ '@pagefind/windows-x64': 1.5.2
+
+ parse-entities@4.0.2:
+ dependencies:
+ '@types/unist': 2.0.11
+ character-entities-legacy: 3.0.0
+ character-reference-invalid: 2.0.1
+ decode-named-character-reference: 1.3.0
+ is-alphanumerical: 2.0.1
+ is-decimal: 2.0.1
+ is-hexadecimal: 2.0.1
+
+ path-browserify@1.0.1: {}
+
+ path-to-regexp@6.3.0: {}
+
+ pathe@2.0.3: {}
+
+ piccolore@0.1.3: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.2: {}
+
+ picomatch@4.0.7: {}
+
+ playwright-core@1.64.1: {}
+
+ playwright@1.64.1:
+ dependencies:
+ playwright-core: 1.64.1
+
+ pngjs@7.0.0: {}
+
+ postcss-nested@6.2.0(postcss@8.5.30):
+ dependencies:
+ postcss: 8.5.30
+ postcss-selector-parser: 6.1.4
+
+ postcss-selector-parser@6.1.4:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ postcss@8.5.30:
+ dependencies:
+ nanoid: 3.3.20
+ picocolors: 1.1.1
+ source-map-js: 1.2.2
+
+ prettier-plugin-astro@1.1.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(prettier@3.9.10):
+ dependencies:
+ '@astrojs/compiler-rs': 0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)
+ '@prettier/parse-srcset': 3.1.0
+ prettier: 3.9.10
+ sass-formatter: 0.8.0
+ transitivePeerDependencies:
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+
+ prettier@3.9.10: {}
+
+ pretty-format@27.5.1:
+ dependencies:
+ ansi-regex: 5.0.1
+ ansi-styles: 5.2.0
+ react-is: 17.0.2
+
+ prismjs@1.30.0: {}
+
+ process-ancestry@0.1.0: {}
+
+ property-information@7.2.0: {}
+
+ pyodide@314.0.7:
+ dependencies:
+ '@types/emscripten': 1.41.6
+ ws: 8.22.0
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
+ radix3@1.1.2: {}
+
+ react-is@17.0.2: {}
+
+ readdirp@4.1.2: {}
+
+ readdirp@5.1.1: {}
+
+ regex-recursion@6.0.2:
+ dependencies:
+ regex-utilities: 2.3.0
+
+ regex-utilities@2.3.0: {}
+
+ regex@6.1.0:
+ dependencies:
+ regex-utilities: 2.3.0
+
+ rehype-expressive-code@0.44.2:
+ dependencies:
+ expressive-code: 0.44.2
+
+ remark-directive@4.0.0(supports-color@10.2.2):
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-directive: 3.1.1(supports-color@10.2.2)
+ micromark-extension-directive: 4.0.0
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ request-light@0.5.8: {}
+
+ request-light@0.7.0: {}
+
+ require-from-string@2.0.2: {}
+
+ resolve-pkg-maps@1.0.0: {}
+
+ retext-smartypants@6.2.0:
+ dependencies:
+ '@types/nlcst': 2.0.3
+ nlcst-to-string: 4.0.0
+ unist-util-visit: 5.1.0
+
+ s.color@0.0.15: {}
+
+ sass-formatter@0.8.0:
+ dependencies:
+ suf-log: 2.5.3
+
+ satteri@0.10.5:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.5
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ optionalDependencies:
+ '@bruits/satteri-darwin-arm64': 0.10.5
+ '@bruits/satteri-darwin-x64': 0.10.5
+ '@bruits/satteri-linux-arm64-gnu': 0.10.5
+ '@bruits/satteri-linux-arm64-musl': 0.10.5
+ '@bruits/satteri-linux-x64-gnu': 0.10.5
+ '@bruits/satteri-linux-x64-musl': 0.10.5
+ '@bruits/satteri-wasm32-wasi': 0.10.5
+ '@bruits/satteri-win32-arm64-msvc': 0.10.5
+ '@bruits/satteri-win32-x64-msvc': 0.10.5
+
+ sax@1.6.1: {}
+
+ semver@7.8.5: {}
+
+ sharp@0.35.5(@types/node@26.6.5):
+ dependencies:
+ '@img/colour': 1.1.0
+ detect-libc: 2.1.2
+ semver: 7.8.5
+ optionalDependencies:
+ '@img/sharp-darwin-arm64': 0.35.5
+ '@img/sharp-darwin-x64': 0.35.5
+ '@img/sharp-freebsd-wasm32': 0.35.5
+ '@img/sharp-libvips-darwin-arm64': 1.3.4
+ '@img/sharp-libvips-darwin-x64': 1.3.4
+ '@img/sharp-libvips-linux-arm': 1.3.4
+ '@img/sharp-libvips-linux-arm64': 1.3.4
+ '@img/sharp-libvips-linux-ppc64': 1.3.4
+ '@img/sharp-libvips-linux-riscv64': 1.3.4
+ '@img/sharp-libvips-linux-s390x': 1.3.4
+ '@img/sharp-libvips-linux-x64': 1.3.4
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.4
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.4
+ '@img/sharp-linux-arm': 0.35.5
+ '@img/sharp-linux-arm64': 0.35.5
+ '@img/sharp-linux-ppc64': 0.35.5
+ '@img/sharp-linux-riscv64': 0.35.5
+ '@img/sharp-linux-s390x': 0.35.5
+ '@img/sharp-linux-x64': 0.35.5
+ '@img/sharp-linuxmusl-arm64': 0.35.5
+ '@img/sharp-linuxmusl-x64': 0.35.5
+ '@img/sharp-webcontainers-wasm32': 0.35.5
+ '@img/sharp-win32-arm64': 0.35.5
+ '@img/sharp-win32-ia32': 0.35.5
+ '@img/sharp-win32-x64': 0.35.5
+ '@types/node': 26.6.5
+
+ shiki@4.5.0:
+ dependencies:
+ '@shikijs/core': 4.5.0
+ '@shikijs/engine-javascript': 4.5.0
+ '@shikijs/engine-oniguruma': 4.5.0
+ '@shikijs/langs': 4.5.0
+ '@shikijs/themes': 4.5.0
+ '@shikijs/types': 4.5.0
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.5
+
+ sirv@3.0.2:
+ dependencies:
+ '@polka/url': 1.0.0-next.29
+ mrmime: 2.0.1
+ totalist: 3.0.1
+
+ sisteransi@1.0.5: {}
+
+ sitemap@9.0.1:
+ dependencies:
+ '@types/node': 24.19.2
+ '@types/sax': 1.2.7
+ arg: 5.0.2
+ sax: 1.6.1
+
+ smol-toml@1.9.1: {}
+
+ source-map-js@1.2.2: {}
+
+ space-separated-tokens@2.0.2: {}
+
+ std-env@4.3.0: {}
+
+ string-width@7.2.0:
+ dependencies:
+ emoji-regex: 10.6.0
+ get-east-asian-width: 1.7.0
+ strip-ansi: 7.2.0
+
+ string-width@8.3.0:
+ dependencies:
+ get-east-asian-width: 1.7.0
+ strip-ansi: 7.2.0
+
+ stringify-entities@4.0.4:
+ dependencies:
+ character-entities-html4: 2.1.0
+ character-entities-legacy: 3.0.0
+
+ strip-ansi@7.2.0:
+ dependencies:
+ ansi-regex: 6.4.0
+
+ style-mod@4.1.4: {}
+
+ suf-log@2.5.3:
+ dependencies:
+ s.color: 0.0.15
+
+ supports-color@10.2.2: {}
+
+ svgo@4.1.0:
+ dependencies:
+ commander: 11.1.0
+ css-select: 6.0.0
+ css-tree: 3.2.1
+ css-what: 7.0.0
+ csso: 5.0.5
+ picocolors: 1.1.1
+ sax: 1.6.1
+
+ tiny-inflate@1.0.3: {}
+
+ tinybench@6.2.2: {}
+
+ tinyclip@0.1.15: {}
+
+ tinyexec@1.3.1: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.7)
+ picomatch: 4.0.7
+
+ tinypool@2.2.0: {}
+
+ tinyrainbow@3.2.0: {}
+
+ totalist@3.0.1: {}
+
+ trim-lines@3.0.1: {}
+
+ trough@2.2.0: {}
+
+ tslib@2.8.1: {}
+
+ typesafe-path@0.2.2: {}
+
+ typescript-auto-import-cache@0.3.6:
+ dependencies:
+ semver: 7.8.5
+
+ typescript@6.0.3: {}
+
+ ufo@1.6.4: {}
+
+ ultrahtml@1.7.0: {}
+
+ uncrypto@0.1.3: {}
+
+ undici-types@7.24.6: {}
+
+ undici-types@8.9.0: {}
+
+ undici@7.29.1: {}
+
+ undici@8.12.2: {}
+
+ unenv@2.0.0-rc.24:
+ dependencies:
+ pathe: 2.0.3
+
+ unified@11.0.5:
+ dependencies:
+ '@types/unist': 3.0.3
+ bail: 2.0.2
+ devlop: 1.1.0
+ extend: 3.0.2
+ is-plain-obj: 4.1.0
+ trough: 2.2.0
+ vfile: 6.0.3
+
+ unifont@0.7.5:
+ dependencies:
+ css-tree: 3.2.1
+ ohash: 2.0.12
+ undici: 8.12.2
+
+ unist-util-find-after@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+
+ unist-util-is@6.0.1:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-position@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-stringify-position@4.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-visit-parents@6.0.2:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+
+ unist-util-visit@5.1.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
+
+ unstorage@1.17.6:
+ dependencies:
+ anymatch: 3.1.3
+ chokidar: 5.0.0
+ destr: 2.0.5
+ h3: 1.15.11
+ lru-cache: 11.5.3
+ node-fetch-native: 1.6.7
+ ofetch: 1.5.1
+ ufo: 1.6.4
+
+ url-extras@0.1.0: {}
+
+ util-deprecate@1.0.2: {}
+
+ vfile-message@4.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-stringify-position: 4.0.0
+
+ vfile@6.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ vfile-message: 4.0.3
+
+ vite-plus@1.3.0(@astrojs/compiler-rs@0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3))(@types/node@26.6.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(playwright@1.64.1)(typescript@6.0.3)(yaml@2.9.1):
+ dependencies:
+ '@oxc-project/types': 0.154.0
+ '@oxlint/plugins': 1.88.0
+ '@vitest/browser': 5.0.5(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(vitest@5.0.5)(yaml@2.9.1)
+ '@vitest/browser-preview': 5.0.5(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(vitest@5.0.5)(yaml@2.9.1)
+ '@vitest/mocker': 5.0.5(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)
+ '@vitest/pretty-format': 5.0.5
+ '@vitest/snapshot': 5.0.5
+ '@vitest/spy': 5.0.5
+ '@vitest/utils': 5.0.5
+ oxfmt: 0.73.0(@astrojs/compiler-rs@0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3))(vite-plus@1.3.0(@astrojs/compiler-rs@0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3))(@types/node@26.6.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(playwright@1.64.1)(typescript@6.0.3)(yaml@2.9.1))
+ oxlint: 1.88.0(oxlint-tsgolint@7.0.2004)(vite-plus@1.3.0(@astrojs/compiler-rs@0.4.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3))(@types/node@26.6.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(playwright@1.64.1)(typescript@6.0.3)(yaml@2.9.1))
+ oxlint-tsgolint: 7.0.2004
+ vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)'
+ vitest: 5.0.5(@types/node@26.6.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1))(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)
+ optionalDependencies:
+ '@voidzero-dev/vite-plus-darwin-arm64': 1.3.0
+ '@voidzero-dev/vite-plus-darwin-x64': 1.3.0
+ '@voidzero-dev/vite-plus-linux-arm64-gnu': 1.3.0
+ '@voidzero-dev/vite-plus-linux-arm64-musl': 1.3.0
+ '@voidzero-dev/vite-plus-linux-x64-gnu': 1.3.0
+ '@voidzero-dev/vite-plus-linux-x64-musl': 1.3.0
+ '@voidzero-dev/vite-plus-win32-arm64-msvc': 1.3.0
+ '@voidzero-dev/vite-plus-win32-x64-msvc': 1.3.0
+ playwright: 1.64.1
+ transitivePeerDependencies:
+ - '@arethetypeswrong/core'
+ - '@astrojs/compiler-rs'
+ - '@edge-runtime/vm'
+ - '@opentelemetry/api'
+ - '@types/node'
+ - '@typescript/native-preview'
+ - '@vitejs/devtools'
+ - '@vitest/coverage-istanbul'
+ - '@vitest/coverage-v8'
+ - '@vitest/ui'
+ - '@volar/typescript'
+ - '@vue/language-core'
+ - bufferutil
+ - esbuild
+ - happy-dom
+ - jiti
+ - jsdom
+ - less
+ - msw
+ - publint
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - svelte
+ - terser
+ - tsx
+ - typescript
+ - unplugin-unused
+ - unrun
+ - utf-8-validate
+ - vue-tsc
+ - yaml
+
+ vitefu@1.1.3(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1):
+ dependencies:
+ vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)'
+ transitivePeerDependencies:
+ - '@arethetypeswrong/core'
+ - '@types/node'
+ - '@typescript/native-preview'
+ - '@vitejs/devtools'
+ - '@volar/typescript'
+ - '@vue/language-core'
+ - esbuild
+ - jiti
+ - less
+ - publint
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - terser
+ - tsx
+ - typescript
+ - unplugin-unused
+ - unrun
+ - vue-tsc
+ - yaml
+
+ vitest@5.0.5(@types/node@26.6.5)(@vitest/browser-preview@5.0.5)(@vitest/ui@5.0.5)(@voidzero-dev/vite-plus-core@1.3.0(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1))(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1):
+ dependencies:
+ '@types/chai': 5.2.3
+ '@vitest/mocker': 5.0.5(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)
+ chai: 6.3.0
+ es-module-lexer: 2.3.2
+ expect-type: 1.4.0
+ magic-string: 1.4.3
+ obug: 3.0.0
+ picomatch: 4.0.7
+ std-env: 4.3.0
+ tinybench: 6.2.2
+ tinyexec: 1.3.1
+ tinyglobby: 0.2.17
+ vite: '@voidzero-dev/vite-plus-core@1.3.0(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(yaml@2.9.1)'
+ why-is-node-running: 3.2.1
+ optionalDependencies:
+ '@types/node': 26.6.5
+ '@vitest/browser-preview': 5.0.5(@types/node@26.6.5)(@volar/typescript@2.4.28(typescript@6.0.3))(esbuild@0.28.2)(typescript@6.0.3)(vitest@5.0.5)(yaml@2.9.1)
+ '@vitest/ui': 5.0.5(vitest@5.0.5)
+ transitivePeerDependencies:
+ - '@arethetypeswrong/core'
+ - '@typescript/native-preview'
+ - '@vitejs/devtools'
+ - '@volar/typescript'
+ - '@vue/language-core'
+ - esbuild
+ - jiti
+ - less
+ - msw
+ - publint
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - terser
+ - tsx
+ - typescript
+ - unplugin-unused
+ - unrun
+ - vue-tsc
+ - yaml
+
+ volar-service-css@0.0.71(@volar/language-service@2.4.28(typescript@6.0.3)):
+ dependencies:
+ vscode-css-languageservice: 6.3.10
+ vscode-languageserver-textdocument: 1.0.15
+ vscode-uri: 3.2.0
+ optionalDependencies:
+ '@volar/language-service': 2.4.28(typescript@6.0.3)
+
+ volar-service-emmet@0.0.71(@volar/language-service@2.4.28(typescript@6.0.3)):
+ dependencies:
+ '@emmetio/css-parser': 0.4.1
+ '@emmetio/html-matcher': 1.3.0
+ '@vscode/emmet-helper': 2.11.0
+ vscode-uri: 3.2.0
+ optionalDependencies:
+ '@volar/language-service': 2.4.28(typescript@6.0.3)
+
+ volar-service-html@0.0.71(@volar/language-service@2.4.28(typescript@6.0.3)):
+ dependencies:
+ vscode-html-languageservice: 5.6.2
+ vscode-languageserver-textdocument: 1.0.15
+ vscode-uri: 3.2.0
+ optionalDependencies:
+ '@volar/language-service': 2.4.28(typescript@6.0.3)
+
+ volar-service-prettier@0.0.71(@volar/language-service@2.4.28(typescript@6.0.3))(prettier@3.9.10):
+ dependencies:
+ vscode-uri: 3.2.0
+ optionalDependencies:
+ '@volar/language-service': 2.4.28(typescript@6.0.3)
+ prettier: 3.9.10
+
+ volar-service-typescript-twoslash-queries@0.0.71(@volar/language-service@2.4.28(typescript@6.0.3))(typescript@6.0.3):
+ dependencies:
+ vscode-uri: 3.2.0
+ optionalDependencies:
+ '@volar/language-service': 2.4.28(typescript@6.0.3)
+ typescript: 6.0.3
+
+ volar-service-typescript@0.0.71(@volar/language-service@2.4.28(typescript@6.0.3))(typescript@6.0.3):
+ dependencies:
+ path-browserify: 1.0.1
+ semver: 7.8.5
+ typescript-auto-import-cache: 0.3.6
+ vscode-languageserver-textdocument: 1.0.15
+ vscode-nls: 5.2.0
+ vscode-uri: 3.2.0
+ optionalDependencies:
+ '@volar/language-service': 2.4.28(typescript@6.0.3)
+ typescript: 6.0.3
+
+ volar-service-yaml@0.0.71(@volar/language-service@2.4.28(typescript@6.0.3)):
+ dependencies:
+ vscode-uri: 3.2.0
+ yaml-language-server: 1.23.0
+ optionalDependencies:
+ '@volar/language-service': 2.4.28(typescript@6.0.3)
+
+ vscode-css-languageservice@6.3.10:
+ dependencies:
+ '@vscode/l10n': 0.0.18
+ vscode-languageserver-textdocument: 1.0.15
+ vscode-languageserver-types: 3.17.5
+ vscode-uri: 3.2.0
+
+ vscode-html-languageservice@5.6.2:
+ dependencies:
+ '@vscode/l10n': 0.0.18
+ vscode-languageserver-textdocument: 1.0.15
+ vscode-languageserver-types: 3.18.4
+ vscode-uri: 3.2.0
+
+ vscode-json-languageservice@4.1.8:
+ dependencies:
+ jsonc-parser: 3.3.1
+ vscode-languageserver-textdocument: 1.0.15
+ vscode-languageserver-types: 3.18.4
+ vscode-nls: 5.2.0
+ vscode-uri: 3.2.0
+
+ vscode-jsonrpc@8.2.0: {}
+
+ vscode-jsonrpc@9.0.3: {}
+
+ vscode-languageserver-protocol@3.17.5:
+ dependencies:
+ vscode-jsonrpc: 8.2.0
+ vscode-languageserver-types: 3.17.5
+
+ vscode-languageserver-protocol@3.18.4:
+ dependencies:
+ vscode-jsonrpc: 9.0.3
+ vscode-languageserver-types: 3.18.4
+
+ vscode-languageserver-textdocument@1.0.15: {}
+
+ vscode-languageserver-types@3.17.5: {}
+
+ vscode-languageserver-types@3.18.4: {}
+
+ vscode-languageserver@9.0.1:
+ dependencies:
+ vscode-languageserver-protocol: 3.17.5
+
+ vscode-nls@5.2.0: {}
+
+ vscode-uri@3.2.0: {}
+
+ w3c-keyname@2.2.8: {}
+
+ why-is-node-running@3.2.1: {}
+
+ workerd@1.20261012.1:
+ optionalDependencies:
+ '@cloudflare/workerd-darwin-64': 1.20261012.1
+ '@cloudflare/workerd-darwin-arm64': 1.20261012.1
+ '@cloudflare/workerd-linux-64': 1.20261012.1
+ '@cloudflare/workerd-linux-arm64': 1.20261012.1
+ '@cloudflare/workerd-windows-64': 1.20261012.1
+
+ wrangler@4.151.1(@types/node@26.6.5):
+ dependencies:
+ '@cloudflare/kv-asset-handler': 0.5.0
+ '@cloudflare/unenv-preset': 2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261012.1)
+ blake3-wasm: 2.1.5
+ esbuild: 0.28.2
+ miniflare: 5.20261012.1-beta(@types/node@26.6.5)
+ path-to-regexp: 6.3.0
+ unenv: 2.0.0-rc.24
+ workerd: 1.20261012.1
+ optionalDependencies:
+ fsevents: 2.3.3
+ transitivePeerDependencies:
+ - '@types/node'
+ - bufferutil
+ - utf-8-validate
+
+ wrap-ansi@9.0.2:
+ dependencies:
+ ansi-styles: 6.2.3
+ string-width: 7.2.0
+ strip-ansi: 7.2.0
+
+ ws@8.21.0: {}
+
+ ws@8.22.0: {}
+
+ xxhash-wasm@1.1.0: {}
+
+ y18n@5.0.8: {}
+
+ yaml-language-server@1.23.0:
+ dependencies:
+ '@vscode/l10n': 0.0.18
+ ajv: 8.20.0
+ ajv-draft-04: 1.0.0(ajv@8.20.0)
+ ajv-i18n: 4.2.0(ajv@8.20.0)
+ prettier: 3.9.10
+ request-light: 0.5.8
+ vscode-json-languageservice: 4.1.8
+ vscode-languageserver: 9.0.1
+ vscode-languageserver-textdocument: 1.0.15
+ vscode-languageserver-types: 3.18.4
+ vscode-uri: 3.2.0
+ yaml: 2.8.3
+
+ yaml@2.8.3: {}
+
+ yaml@2.9.1: {}
+
+ yargs-parser@22.0.0: {}
+
+ yargs@18.2.0:
+ dependencies:
+ cliui: 9.0.1
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ string-width: 8.3.0
+ y18n: 5.0.8
+ yargs-parser: 22.0.0
+
+ yocto-queue@1.2.2: {}
+
+ youch-core@0.3.3:
+ dependencies:
+ '@poppinss/exception': 1.2.3
+ error-stack-parser-es: 1.0.5
+
+ youch@4.1.0-beta.10:
+ dependencies:
+ '@poppinss/colors': 4.1.6
+ '@poppinss/dumper': 0.6.5
+ '@speed-highlight/core': 1.2.24
+ cookie: 1.1.1
+ youch-core: 0.3.3
+
+ yuku-core@0.19.0:
+ dependencies:
+ '@yuku-toolchain/types': 0.19.0
+ optionalDependencies:
+ '@yuku-core/binding-android-arm64': 0.19.0
+ '@yuku-core/binding-darwin-arm64': 0.19.0
+ '@yuku-core/binding-darwin-x64': 0.19.0
+ '@yuku-core/binding-freebsd-x64': 0.19.0
+ '@yuku-core/binding-linux-arm-gnu': 0.19.0
+ '@yuku-core/binding-linux-arm-musl': 0.19.0
+ '@yuku-core/binding-linux-arm64-gnu': 0.19.0
+ '@yuku-core/binding-linux-arm64-musl': 0.19.0
+ '@yuku-core/binding-linux-x64-gnu': 0.19.0
+ '@yuku-core/binding-linux-x64-musl': 0.19.0
+ '@yuku-core/binding-win32-arm64': 0.19.0
+ '@yuku-core/binding-win32-x64': 0.19.0
+
+ yuku-parser@0.19.0:
+ dependencies:
+ '@yuku-toolchain/types': 0.19.0
+ yuku-core: 0.19.0
+
+ zod@4.6.7: {}
+
+ zwitch@2.0.4: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 0000000..03c7697
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,17 @@
+packages:
+ - landing
+ - docs
+ - playground
+
+allowBuilds:
+ esbuild: true
+ sharp: true
+ workerd: true
+
+overrides:
+ vite: npm:@voidzero-dev/vite-plus-core@1.3.0
+
+# The alias reports the Vite+ version; it bundles compatible Vite 8.3.4.
+peerDependencyRules:
+ allowedVersions:
+ vite: '1.3.0'
diff --git a/pyproject.toml b/pyproject.toml
index 4655c57..881253c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "mlbundle"
-version = "0.1.0a3"
+version = "0.2.0"
description = "Pickle-free, verifiable model bundles with a manifest, schema and structure."
readme = "README.md"
license = "MIT"
@@ -20,7 +20,6 @@ keywords = [
"skops",
]
classifiers = [
- "Development Status :: 2 - Pre-Alpha",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"Natural Language :: English",
@@ -46,12 +45,12 @@ xgboost = ["xgboost>=3.4.1", "scikit-learn>=1.2"]
# Audited CBM schema and Python 3.14 wheels on Windows/Linux.
catboost = ["catboost>=1.2.10"]
onnx = ["onnx>=1.23.0", "onnxruntime>=1.30.0"]
-# The reader validates the 0.2 inferred vocabulary; 0.2.1 requires Python <3.15.
-schema = ["mlschema>=0.2.1,<0.3; python_version == '3.14'"]
+# The reader validates the 0.3 vocabulary; schema inference requires Python 3.14.
+schema = ["mlschema>=0.3.0,<0.4; python_version == '3.14'"]
all = [
"scikit-learn>=1.6", "skops>=0.16.0", "xgboost>=3.4.1", "catboost>=1.2.10",
"onnx>=1.23.0", "onnxruntime>=1.30.0",
- "mlschema>=0.2.1,<0.3; python_version == '3.14'",
+ "mlschema>=0.3.0,<0.4; python_version == '3.14'",
]
[project.urls]
diff --git a/scripts/package_smoke.py b/scripts/package_smoke.py
index 5578ac5..01c733c 100644
--- a/scripts/package_smoke.py
+++ b/scripts/package_smoke.py
@@ -52,7 +52,7 @@ def _base(path: Path) -> None:
import mlbundle
- assert mlbundle.__version__ == "0.1.0a3"
+ assert mlbundle.__version__ == "0.2.0"
view = mlbundle.inspect(path)
assert view.manifest.model.library == "sklearn"
assert "cannot attest" in _console("--help")
diff --git a/scripts/publish_check.py b/scripts/publish_check.py
index e778ab2..68f5536 100644
--- a/scripts/publish_check.py
+++ b/scripts/publish_check.py
@@ -1,6 +1,8 @@
-"""Read-only preflight for the owner's manual PyPI workflow."""
+"""Check a main-branch version change before the automatic PyPI workflow."""
+import argparse
import os
+import subprocess
import sys
import tomllib
from pathlib import Path
@@ -8,10 +10,13 @@
from urllib.request import urlopen
-def check(ref: str, project: dict) -> None:
+def check(ref: str, project: dict, previous_version: str | None = None) -> bool:
if ref != "refs/heads/main":
raise SystemExit("Publishing requires refs/heads/main; select main.")
version = project["version"]
+ if version == previous_version:
+ sys.stdout.write(f"Version remains {version}; skipping PyPI publication.\n")
+ return False
url = f"https://pypi.org/pypi/mlbundle/{version}/json"
try:
with urlopen(url, timeout=30):
@@ -25,10 +30,36 @@ def check(ref: str, project: dict) -> None:
else:
raise SystemExit(f"mlbundle {version} already exists on PyPI; refusing upload.")
sys.stdout.write(f"mlbundle {version} is not on PyPI (HTTP 404); main ref OK.\n")
+ return True
+
+
+def version_at(ref: str) -> str | None:
+ if not ref or set(ref) == {"0"}:
+ return None
+ result = subprocess.run(
+ ["git", "show", f"{ref}:pyproject.toml"],
+ check=False,
+ capture_output=True,
+ text=True,
+ )
+ if result.returncode:
+ raise SystemExit("Could not read the previous pyproject.toml version.")
+ return tomllib.loads(result.stdout)["project"]["version"]
+
+
+def write_output(path: str, publish: bool) -> None:
+ with Path(path).open("a", encoding="utf-8") as output:
+ output.write(f"publish={str(publish).lower()}\n")
if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--previous-ref")
+ args = parser.parse_args()
project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))[
"project"
]
- check(os.environ["GITHUB_REF"], project)
+ previous_version = version_at(args.previous_ref) if args.previous_ref else None
+ publish = check(os.environ["GITHUB_REF"], project, previous_version)
+ if args.previous_ref and os.environ.get("GITHUB_OUTPUT"):
+ write_output(os.environ["GITHUB_OUTPUT"], publish)
diff --git a/src/mlbundle/_schema_inference.py b/src/mlbundle/_schema_inference.py
deleted file mode 100644
index dd27903..0000000
--- a/src/mlbundle/_schema_inference.py
+++ /dev/null
@@ -1,28 +0,0 @@
-"""The temporary adaptation between local mlschema inference and MLForm."""
-
-
-def mlform_fields(fields: list[dict]) -> list[dict]:
- # Adapt mlschema 0.2.1 to MLForm 0.1.25 until mlschema emits series columns.
- return [
- {
- **{
- key: value
- for key, value in field.items()
- if key not in ("field1", "field2")
- },
- "columns": [
- {
- **{
- key: value
- for key, value in field[axis].items()
- if key != "mappedTo"
- },
- "id": axis,
- }
- for axis in ("field1", "field2")
- ],
- }
- if field.get("kind") == "series"
- else field
- for field in fields
- ]
diff --git a/src/mlbundle/schema.py b/src/mlbundle/schema.py
index fc3f616..6a5b9de 100644
--- a/src/mlbundle/schema.py
+++ b/src/mlbundle/schema.py
@@ -76,21 +76,6 @@ def reports_for(claims: ModelClaims) -> list[dict[str, JsonValue]]:
return reports
-def positional_fields(fields: list[dict], names: list[str]) -> None:
- """Rewrite inferred name mappings, retaining mlschema's labels and grouping."""
- positions = {name: index for index, name in enumerate(names)}
-
- def rewrite(field: dict) -> None:
- if "mappedTo" in field:
- field["mappedTo"] = positions[field["mappedTo"]]
- for option in field.get("options", []):
- if isinstance(option, dict):
- rewrite(option)
-
- for field in fields:
- rewrite(field)
-
-
def _mapping_index(
mapping: str | int, positions: dict[str, int], origin: FeatureOrigin
) -> int:
diff --git a/src/mlbundle/training.py b/src/mlbundle/training.py
index 4909c9d..b927d87 100644
--- a/src/mlbundle/training.py
+++ b/src/mlbundle/training.py
@@ -9,7 +9,6 @@
from pydantic import ValidationError
-from ._schema_inference import mlform_fields
from .canonical import canonical_dumps, canonical_value
from .claims import ModelClaims
from .contract import FeatureOrigin, MemberName
@@ -23,7 +22,7 @@
from .limits import ArchiveLimits
from .manifest import FeatureInfo
from .samples import sample_bytes
-from .schema import positional_fields, reports_for, validate_schema, validate_separator
+from .schema import reports_for, validate_schema, validate_separator
if TYPE_CHECKING:
from pandas import DataFrame
@@ -125,9 +124,9 @@ def prepare_training(
dtypes = [None] * claims.n_features
cap = limits.member_bytes(MemberName.SCHEMA)
try:
- fields = mlform_fields(mlschema.infer_schema(frame, onehot_separator=separator))
+ fields = mlschema.infer_schema(frame, onehot_separator=separator)
if origin in (FeatureOrigin.DATAFRAME, FeatureOrigin.DECLARED):
- positional_fields(fields, names)
+ fields = mlschema.with_positional_mappings(fields, names)
schema = canonical_value(
{
"schema_version": "2",
diff --git a/tests/test_publish.py b/tests/test_publish.py
index b8f4bae..d195811 100644
--- a/tests/test_publish.py
+++ b/tests/test_publish.py
@@ -1,4 +1,4 @@
-"""Manual publish preflight rejects other refs, existing versions and outages."""
+"""Publish preflight gates version changes, duplicate releases and outages."""
import runpy
from contextlib import nullcontext
@@ -36,7 +36,7 @@ def response(url, timeout):
monkeypatch.setitem(preflight.__globals__, "urlopen", response)
if code == 404:
- preflight("refs/heads/main", {"version": "0.1.0a3"})
+ assert preflight("refs/heads/main", {"version": "0.1.0a3"}) is True
assert "HTTP 404" in capsys.readouterr().out
else:
message = "already exists" if code == 200 else f"HTTP {code}"
@@ -44,6 +44,26 @@ def response(url, timeout):
preflight("refs/heads/main", {"version": "0.1.0a3"})
+def test_unchanged_version_skips_network(preflight, monkeypatch, capsys):
+ def unexpected(*args, **kwargs):
+ raise AssertionError("An unchanged version must not query PyPI")
+
+ monkeypatch.setitem(preflight.__globals__, "urlopen", unexpected)
+ assert (
+ preflight("refs/heads/main", {"version": "0.2.0"}, previous_version="0.2.0")
+ is False
+ )
+ assert "skipping" in capsys.readouterr().out
+
+
+def test_output_records_publish_decision(preflight, tmp_path):
+ write_output = preflight.__globals__["write_output"]
+ output = tmp_path / "github-output"
+ write_output(str(output), True)
+ write_output(str(output), False)
+ assert output.read_text(encoding="utf-8") == "publish=true\npublish=false\n"
+
+
@pytest.mark.parametrize("error", [URLError("offline"), TimeoutError("timeout")])
def test_network_failure_is_closed(preflight, monkeypatch, error):
def response(*args, **kwargs):
diff --git a/tests/test_readme.py b/tests/test_readme.py
index c7cf74b..1f38482 100644
--- a/tests/test_readme.py
+++ b/tests/test_readme.py
@@ -1,4 +1,4 @@
-"""Execute the actual fenced README examples, including the console commands."""
+"""Execute the README quick start and its mlbundle CLI commands."""
import re
import shlex
@@ -8,8 +8,6 @@
import pytest
-from onnx_helpers import softmax
-
README = Path(__file__).resolve().parents[1] / "README.md"
BLOCKS = re.findall(
r"```(\w+)\n(.*?)\n```", README.read_text(encoding="utf-8"), re.DOTALL
@@ -17,32 +15,20 @@
PYTHON = [code for language, code in BLOCKS if language == "python"]
-@pytest.mark.parametrize(
- "code",
- PYTHON,
- ids=[
- "sklearn",
- "records",
- "xgboost",
- "catboost",
- "composite",
- "onnx",
- "schema-samples",
- ],
-)
+@pytest.mark.parametrize("code", PYTHON)
def test_python_example(code, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
- if 'record = {"0"' in code:
- exec(compile(PYTHON[0], str(README), "exec"), {})
- if 'Path("trained.onnx")' in code:
- # The user's prerequisite file: a self-contained, two-column classifier.
- Path("trained.onnx").write_bytes(softmax().SerializeToString())
- exec(compile(code, str(README), "exec"), {})
+ scope = {}
+ exec(compile(code, str(README), "exec"), scope)
+ assert scope["metadata"].samples is None
+ assert scope["bundle"].feature_names == ("temperature", "humidity")
+ assert scope["result"].predicted_label == "cold"
+ assert scope["result"].probabilities == (1.0, 0.0)
def test_console_examples(tmp_path, monkeypatch):
- assert len(PYTHON) == 7
- assert all(language in {"python", "shell"} for language, _ in BLOCKS)
+ assert PYTHON
+ assert all(language in {"python", "shell", "bash"} for language, _ in BLOCKS)
monkeypatch.chdir(tmp_path)
exec(compile(PYTHON[0], str(README), "exec"), {})
executable = Path(sys.executable).with_name(
diff --git a/tests/test_series_schema.py b/tests/test_series_schema.py
index 3145c77..acdbcc4 100644
--- a/tests/test_series_schema.py
+++ b/tests/test_series_schema.py
@@ -1,4 +1,4 @@
-"""The sole mlschema shape adaptation and its closed reader contract."""
+"""Native mlschema series output and its closed reader contract."""
import copy
from typing import get_args
@@ -15,7 +15,7 @@
from mlbundle.writing import write_container
-def test_field_vocabulary_matches_mlschema_except_series():
+def test_field_vocabulary_matches_mlschema():
from mlschema.strategies.app import builtin_kinds
from mlschema.strategies.domain.onehot_category_field import OneHotCategoryOption
@@ -27,12 +27,7 @@ def test_field_vocabulary_matches_mlschema_except_series():
theirs = {item.name: set(item.model.model_fields) for item in builtin_kinds()}
assert ours.keys() == theirs.keys()
for kind, keys in ours.items():
- if kind == "series":
- # The MLForm columns adaptation is the only inferred shape difference.
- assert keys - theirs[kind] == {"columns"}
- assert {"field1", "field2"} <= theirs[kind] - keys
- else:
- assert keys <= theirs[kind], kind
+ assert keys <= theirs[kind], kind
assert set(OneHotOption.model_fields) <= set(OneHotCategoryOption.model_fields)
@@ -45,25 +40,7 @@ def series_bundle(tmp_path):
)
original = infer_schema(frame)[0]
field = view.schema["fields"][0]
- assert field == {
- **{
- key: value
- for key, value in original.items()
- if key not in ("field1", "field2", "mappedTo")
- },
- "mappedTo": 0,
- "columns": [
- {
- **{
- key: value
- for key, value in original[axis].items()
- if key != "mappedTo"
- },
- "id": axis,
- }
- for axis in ("field1", "field2")
- ],
- }
+ assert field == {**original, "mappedTo": 0}
assert view.schema["schema_version"] == "2"
assert mlbundle.inspect(path).schema == mlbundle.load(path).schema == view.schema
return path, field
@@ -76,15 +53,9 @@ def test_series_export(series_bundle):
]
-def old_shape(field):
- for column in field.pop("columns"):
- field[column.pop("id")] = {**column, "mappedTo": field["mappedTo"]}
-
-
@pytest.mark.parametrize(
"change",
[
- old_shape,
lambda f: f["columns"].reverse(),
lambda f: f["columns"].pop(),
lambda f: f["columns"].append(copy.deepcopy(f["columns"][0])),
diff --git a/uv.lock b/uv.lock
index c777b7d..2e6358f 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,5 +1,5 @@
version = 1
-revision = 3
+revision = 5
requires-python = ">=3.14"
resolution-markers = [
"sys_platform == 'win32'",
@@ -459,7 +459,7 @@ wheels = [
[[package]]
name = "mlbundle"
-version = "0.1.0a3"
+version = "0.2.0"
source = { editable = "." }
dependencies = [
{ name = "pydantic" },
@@ -509,8 +509,8 @@ dev = [
requires-dist = [
{ name = "catboost", marker = "extra == 'all'", specifier = ">=1.2.10" },
{ name = "catboost", marker = "extra == 'catboost'", specifier = ">=1.2.10" },
- { name = "mlschema", marker = "python_full_version == '3.14.*' and extra == 'all'", specifier = ">=0.2.1,<0.3" },
- { name = "mlschema", marker = "python_full_version == '3.14.*' and extra == 'schema'", specifier = ">=0.2.1,<0.3" },
+ { name = "mlschema", marker = "python_full_version == '3.14.*' and extra == 'all'", specifier = ">=0.3.0,<0.4" },
+ { name = "mlschema", marker = "python_full_version == '3.14.*' and extra == 'schema'", specifier = ">=0.3.0,<0.4" },
{ name = "onnx", marker = "extra == 'all'", specifier = ">=1.23.0" },
{ name = "onnx", marker = "extra == 'onnx'", specifier = ">=1.23.0" },
{ name = "onnxruntime", marker = "extra == 'all'", specifier = ">=1.30.0" },
@@ -539,15 +539,15 @@ dev = [
[[package]]
name = "mlschema"
-version = "0.2.1"
+version = "0.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pandas" },
{ name = "pydantic" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a2/97/b3295985c4250525069b86bf0eee31c211b3d35cebee5b995f64a35dcb58/mlschema-0.2.1.tar.gz", hash = "sha256:59f819d677fa662f9c620938a6bef7fd093a11dad5872e507e5035da723cdb9e", size = 56763, upload-time = "2026-06-18T11:21:12.225Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/70/6d5c5e76dd5aff506364acf1e52b76cf93c72f3720a5767e1d96d50ad4e0/mlschema-0.3.0.tar.gz", hash = "sha256:6f201a71ec926f7891e149368f5ddd537652086cedcb2ae0e6cd68bee83800cc", size = 755411, upload-time = "2026-10-19T10:44:58.591Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/56/4a/c4b8f535f3ea8cb60af7ab65f30c8dd2a7363768465a63fcccee72cc265f/mlschema-0.2.1-py3-none-any.whl", hash = "sha256:26ae50b7861d7bd907c5117498d384d003d8e2925dcaa14335add3d0bd7101ba", size = 36852, upload-time = "2026-06-18T11:21:11.292Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e4/4230a97e095738c461dece0c2b2829e29291130892588bddd32d0b6cdf05/mlschema-0.3.0-py3-none-any.whl", hash = "sha256:49982ff201781a60b8e63951de6feac03fca1ce4b4c1b9e5a1b6a8895085438d", size = 37017, upload-time = "2026-10-19T10:44:57.276Z" },
]
[[package]]