diff --git a/.github/screenshots/studio-concepts.jpg b/.github/screenshots/studio-concepts.jpg new file mode 100644 index 0000000..9b3a6ca Binary files /dev/null and b/.github/screenshots/studio-concepts.jpg differ diff --git a/.github/screenshots/studio-releases.jpg b/.github/screenshots/studio-releases.jpg new file mode 100644 index 0000000..bc9ec2e Binary files /dev/null and b/.github/screenshots/studio-releases.jpg differ diff --git a/.github/screenshots/studio-review.jpg b/.github/screenshots/studio-review.jpg new file mode 100644 index 0000000..24b0a1e Binary files /dev/null and b/.github/screenshots/studio-review.jpg differ diff --git a/.github/screenshots/studio-runtime.jpg b/.github/screenshots/studio-runtime.jpg new file mode 100644 index 0000000..98b5c65 Binary files /dev/null and b/.github/screenshots/studio-runtime.jpg differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7d1b4a..a1ccc87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,11 +27,23 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.11" + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.14" - run: uv sync --all-extras --locked + - run: bun install --frozen-lockfile + working-directory: apps/studio - run: uv run ruff check . - run: uv run mypy src - run: uv run python scripts/export_schemas.py --check - run: uv run pytest --cov --cov-report=xml + - run: bun run check + working-directory: apps/studio + - run: bun run test + working-directory: apps/studio + - run: bun run build + working-directory: apps/studio + - run: uv run python scripts/embed_studio_assets.py --check - run: uv build - name: Rehearse example pack run: | diff --git a/.gitignore b/.gitignore index 2e9b89c..cfcef33 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,8 @@ site/ *.db *.sqlite* .termkit/ +.obsidian/ +apps/studio/node_modules/ +apps/studio/dist/ +apps/studio/*.tsbuildinfo +*.termdb diff --git a/CHANGELOG.md b/CHANGELOG.md index 1203f2d..6e4b4fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,30 @@ versions are tracked independently. ### Added -- Established the concept-first TermSpec 0.1 foundation. -- Added deterministic TermPack source loading, cross-record validation, - license gating, reproducible archive building, and safe verification. -- Added provider-neutral resolution, longest-match selection, explicit - ambiguity warnings, TermPlan output, and terminology checking. -- Added Forge, Registry, exchange, provider, domain, and sense-resolution ports. -- Added lazy entry-point discovery with third-party loading disabled by default. -- Added CLI, optional HTTP adapter, offline tests, CI, schemas, example pack, - architecture decisions, governance, security, and contribution guidance. +- Added TermSpec 0.2 domain schemes, external identifiers, concept-first + candidates, ChangeSets, review decisions, revisions, source revisions and corpora. +- Added an Alembic-backed local workspace with canonical indexing, draft + isolation, optimistic conflicts, separate language/domain review, atomic + deterministic writes, history and compensating undo. +- Added TermKit Studio, a dense local-first React/Vite workbench with Concepts, + Candidates, Review, Sources, Relations, Exchange, Runtime and Releases. +- Added CSV and XLSX mapping, TBX and SKOS normalization, identity + reconciliation, conflict previews and atomic import ChangeSet generation. +- Added an OpenRefine Reconciliation API v0.2 adapter for external concept + matching without canonical writes. +- Added dependency-closed runtime compilation, exact locks, a separate SQLite + FTS5 index, serialized Aho–Corasick scanning and offline benchmark metrics. +- Added concept-first Forge batches and separated persistent candidates from + bounded, reproducible Forge run metadata. +- Added Studio/API/CLI documentation, security guidance, CI frontend gates and + embedded wheel assets, plus the independent original-data `TermKit-Packs` + repository scaffold. + +### Changed + +- Upgraded the software pre-release to `0.2.0a0` and canonical Studio writes to + TermSpec 0.2 while retaining a documented TermSpec 0.1 read path. +- Deprecated pair-shaped `CandidateTerm` as a compatibility input; Forge now + emits concept, lexical, relation and alignment records. +- Source summaries now derive revision, corpus segment, candidate and referenced + concept counts from normalized workspace identity instead of naming patterns. diff --git a/README.md b/README.md index 1c8d765..047aa5f 100644 --- a/README.md +++ b/README.md @@ -2,48 +2,94 @@ # TermKit -**Concept-first infrastructure for professional multilingual terminology.** +**Concept-first infrastructure and a local-first workbench for professional terminology.** -Build, validate, version, install, resolve, and audit domain terminology packs -without coupling terminology data to one translation provider. +Curate evidence, review semantic changes, build immutable terminology packs, +and compile deterministic runtime indexes—without tying your data to one model +or translation provider. -[简体中文](README.zh-CN.md) · [Architecture](docs/architecture.md) · -[TermPack format](docs/termpack-format.md) · [Plugin system](docs/plugin-system.md) +[简体中文](README.zh-CN.md) · [Studio architecture](TERM_KIT_STUDIO_ARCHITECTURE.md) · +[TermSpec](docs/termspec.md) · [TermPack format](docs/termpack-format.md) + +[![CI](https://github.com/ZilaiWang/TermKit/actions/workflows/ci.yml/badge.svg)](https://github.com/ZilaiWang/TermKit/actions/workflows/ci.yml) +[![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/) +[![TermSpec 0.2](https://img.shields.io/badge/TermSpec-0.2-315C43)](docs/termspec.md) +[![License: MIT](https://img.shields.io/badge/License-MIT-2F2F2F.svg)](LICENSE) > [!IMPORTANT] -> TermKit is a working project name and the repository is at a pre-alpha -> foundation stage. Public contracts are intentionally small; compatibility is -> not guaranteed until the first stable TermSpec release. +> TermKit is pre-alpha. TermSpec 0.2 and the local Studio are implemented as the +> long-term architecture, but public compatibility is not guaranteed until 1.0. + +![TermKit Studio concept workspace](.github/screenshots/studio-concepts.jpg) + +TermKit Studio keeps concept identity, multilingual lexical entries, evidence, +relations and revision state visible in one dense workspace. Canonical files do +not change until an explicit ChangeSet passes validation and is applied. ## What TermKit is -TermKit is infrastructure for a future ecosystem of professional language -resources. Its model is concept-first: a hydraulic pump is one concept with -definitions, relations, multilingual lexical entries, evidence, review state, -and provenance—not a collection of unrelated bilingual string pairs. +TermKit treats a termbase as governed knowledge rather than a table of bilingual +word pairs. One language-neutral concept owns definitions, domain membership, +relations, provenance and lifecycle state; each language-specific name is a +separate lexical entry with its own status, policy, sense and evidence. + +```text +Concept → Evidence → Curation → Review → Package → Runtime +``` + +The repository contains three product surfaces: + +- **TermKit Core** — TermSpec 0.2, deterministic TermPack tooling, Forge + contracts, Exchange planning, dependency locks, and provider-neutral runtime. +- **TermKit Studio** — the official local-first React/FastAPI workbench for + concepts, candidates, review, sources, relations, exchange, runtime and releases. +- **TermRegistry** — immutable package discovery and verified local installation; + a hosted registry is deliberately not required. -The first repository contains four logical components: +Studio is not a SaaS dashboard. It opens a terminology repository on your own +machine. YAML and JSONL remain canonical; the workspace SQLite database is a +derived editing/index layer, and every semantic edit is an auditable ChangeSet. -- **TermSpec** defines concepts, lexical entries, evidence, sources, quality, - manifests, dependencies, and lock files. -- **TermRuntime** performs normalized longest-match discovery and produces - provider-neutral `TermPlan` decisions for translation and validation. -- **TermForge** defines an auditable candidate-production and review pipeline; - extraction and alignment algorithms are plugins. -- **TermRegistry** defines immutable package discovery and installation ports; - the first implementation is a verified local installer. +## Highlights -Terminology data belongs in separate repositories. Every pack declares its own -license, sources, maintainers, and redistribution policy. +- **Concept-first modeling** — represent one meaning once, then attach any + number of BCP 47 language entries, definitions, relations and evidence. +- **Governed curation** — draft semantic operations, review language and domain + decisions independently, and keep rationale plus revision history. +- **Standards-aware exchange** — map CSV/XLSX, normalize TBX and SKOS, preview + identity conflicts, and expose OpenRefine Reconciliation API v0.2. +- **Deterministic runtime** — resolve dependencies, write an exact lock, compile + FTS5 plus an Aho–Corasick matcher, and explain every terminology decision. +- **Local by default** — canonical data stays Git-friendly and Studio binds to + loopback; no hosted account or proprietary database is required. ## Quick start +From a source checkout, requirements are Python 3.11+, +[uv](https://docs.astral.sh/uv/), Bun 1.3+, and Git: + ```bash uv sync --all-extras +cd apps/studio && bun install --frozen-lockfile && bun run build && cd ../.. +uv run termkit studio examples/packs/demo-hydraulics +``` + +Studio opens at `http://127.0.0.1:8765/studio`. It binds to loopback by default; +non-loopback hosting requires an explicit override because Studio has no hosted +authentication boundary. Release wheels embed the production Studio build, so +installed users only need `termkit[studio]`; Bun is a contributor dependency. + +Core workflows remain available without the UI: + +```bash uv run termkit validate examples/packs/demo-hydraulics uv run termkit pack build examples/packs/demo-hydraulics --output dist/ +uv run termkit runtime compile \ + --pack examples/packs/demo-hydraulics \ + --output dist/demo-hydraulics.termdb \ + --lock-output dist/termkit.lock uv run termkit resolve \ "The boom cylinder is controlled by the pilot valve." \ --pack examples/packs/demo-hydraulics \ @@ -51,54 +97,132 @@ uv run termkit resolve \ --target-language zh-Hans ``` -## Repository map +## Studio workflow + +| Workspace | Purpose | +| --- | --- | +| Concepts | Faceted, virtualized concept and multilingual lexical editing | +| Candidates | Context-first reconciliation of Forge output into concepts | +| Review | PR-style semantic diffs with separate language/domain decisions | +| Sources | Provenance, authority, content identity and rights inspection | +| Relations | Hierarchy-first browsing plus governed relation ChangeSets | +| Exchange | CSV/XLSX mapping wizard and normalized TBX/SKOS import planning | +| Runtime | Explainable resolve/check plus independent `.termdb` compilation | +| Releases | Validation gates and deterministic build/verification | + +`⌘K` / `Ctrl+K` opens the command palette. The interface is intentionally dense, +quiet and keyboard-friendly; machine assistance belongs behind Forge and +reconciliation capabilities, not at the center of the product. + +### Review semantic changes + +![TermKit Studio review workspace](.github/screenshots/studio-review.jpg) + +A draft is an ordered semantic diff rather than an opaque form submission. +Language and domain reviewers decide separately, provide rationale, and approve +the same ChangeSet before canonical source is updated. + +### Explain runtime decisions + +![TermKit Studio runtime lab](.github/screenshots/studio-runtime.jpg) + +Runtime Lab shows source offsets, concept identity, selected pack, target term, +policy and evidence. The same bounded result can be consumed through the Python +API, local HTTP API or a compiled `.termdb` index. + +### Validate and build immutable releases + +![TermKit Studio release workspace](.github/screenshots/studio-releases.jpg) + +Release gates validate schema, references, languages, evidence and rights before +building a deterministic `.termpack`; the produced archive is verified in the +same workflow and reported with its SHA-256 digest. + +## Curation lifecycle + +```text +source revision → corpus → Forge candidates → reconciliation + → ChangeSet → language/domain review → canonical source + → TermPack + lock → compiled runtime index +``` + +Machine or plugin output never becomes approved terminology directly. It first +produces candidates with occurrence context and quality signals; a curator then +matches an existing concept, creates a concept, attaches a lexical entry, +defers, merges or rejects the proposal. See [Studio](docs/studio.md), +[Exchange](docs/exchange.md) and [Runtime](docs/runtime.md) for the complete +workflows. + +## Architecture ```text -src/termkit/ - spec/ Stable concept, lexical, evidence, and manifest contracts - pack/ Safe, deterministic TermPack build/load/verify operations - runtime/ Matching, resolution, term plans, and translation checking - plugins/ Entry-point discovery, descriptors, and typed plugin protocols - forge/ Candidate pipeline and review workflow ports - registry/ Package registry and installer ports - server/ Optional HTTP adapter - cli.py Thin command-line adapter -examples/ Original, redistributable demonstration packs -tests/ Offline contract, boundary, pack, runtime, and plugin tests -docs/ Architecture, governance, security, format, and ADRs +canonical YAML + JSONL + │ open / hash / index + ▼ +workspace.sqlite ── draft ChangeSet ── review ── validated atomic write + │ │ + │ candidate/review/history ▼ + └──────────────────────────────────── canonical source + │ + build TermPack / resolve dependencies / lock + ▼ + immutable .termpack derived .termdb + FTS5 + matcher ``` -Dependency direction is enforced in tests: `spec` is the center, while CLI, -server, providers, storage, and third-party algorithms stay at the edges. +The two SQLite files have different ownership and lifecycles: + +- `.termkit/workspace.sqlite` contains local editing state, candidates, review + decisions and derived canonical indexes; +- `.termkit/runtime/*.termdb` contains only resolved pack data, FTS5 search and + a serialized deterministic Aho–Corasick matcher. + +See [architecture.md](docs/architecture.md) and the accepted +[Studio architecture](TERM_KIT_STUDIO_ARCHITECTURE.md) for invariants. -## Standards strategy +## Repository map -TermKit does not replace terminology standards. The internal JSONL format is -optimized for Git review, streaming, and reproducible builds. Conversion -adapters will target TBX-Basic first, then SKOS where concept graphs benefit; -BCP 47 language tags are mandatory from day one. TMX and XLIFF remain adjacent -translation-workflow formats rather than being embedded into TermSpec. +```text +apps/studio/ React + TypeScript + Vite workbench +src/termkit/spec/ TermSpec 0.2 semantic contracts and JSON Schema +src/termkit/workspace/ Alembic-backed local application engine +src/termkit/forge/ Reproducible candidate pipeline and plugin ports +src/termkit/exchange/ CSV/XLSX/TBX/SKOS parsing and reconciliation plans +src/termkit/runtime/ Resolution, QA, dependency locks, FTS and matcher compiler +src/termkit/pack/ Canonical loading, validation and deterministic archives +src/termkit/registry/ Verified immutable package installation +src/termkit/server/ Local FastAPI and Studio API adapters +examples/ Original test fixtures, never the production data catalog +``` + +Official original seed data lives in the separate sibling **TermKit-Packs** +repository. Its dependency hierarchy covers mechanical engineering/hydraulics/ +construction machinery and machine learning/computer vision/object detection. ## Development ```bash uv run ruff check . uv run mypy src +uv run python scripts/export_schemas.py --check uv run pytest --cov -uv build +cd apps/studio && bun run check && bun run test && bun run build +cd ../.. && uv run python scripts/embed_studio_assets.py --check && uv build ``` -Read [CONTRIBUTING.md](CONTRIBUTING.md) and [AGENTS.md](AGENTS.md) before making -structural changes. +Read [CONTRIBUTING.md](CONTRIBUTING.md), [AGENTS.md](AGENTS.md), and +[development.md](docs/development.md) before structural changes. + +## Trust and non-goals -## Current non-goals +TermPacks are untrusted data and never contain executable plugins. Reference-only +or restricted sources cannot ship excerpts. Studio is local-only by default and +must not be exposed directly to the public internet. -- a terminology-management SaaS or marketplace UI; -- bundled proprietary or scraped terminology data; -- an LLM-generated glossary without evidence and review state; -- arbitrary code execution from `.termpack` archives; -- a custom TBX dialect, graph database, or vector database; -- production multi-tenant hosting. +TermKit does not bundle scraped/proprietary termbases, automatically approve +machine proposals, invent a custom TBX dialect, or turn terminology curation +into a chat-first AI product. Multi-tenant SaaS and a plugin marketplace remain +outside the current security boundary. ## License diff --git a/README.zh-CN.md b/README.zh-CN.md index 400f95e..e31720e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,42 +2,86 @@ # TermKit -**面向专业领域的、概念优先的多语言术语基础设施。** +**面向专业术语工程的 Concept-first 基础设施与 Local-first 工作台。** -以可安装、可依赖、可版本化、可追溯的方式生产和使用专业术语资源。 +在不绑定某个模型或翻译供应商的前提下,整理证据、审核语义变更、构建不可变术语包, +并编译可复现的本地运行时索引。 -[English](README.md) · [架构](docs/architecture.md) · -[TermPack 格式](docs/termpack-format.md) · [插件系统](docs/plugin-system.md) +[English](README.md) · [Studio 架构](TERM_KIT_STUDIO_ARCHITECTURE.md) · +[TermSpec](docs/termspec.md) · [TermPack 格式](docs/termpack-format.md) + +[![CI](https://github.com/ZilaiWang/TermKit/actions/workflows/ci.yml/badge.svg)](https://github.com/ZilaiWang/TermKit/actions/workflows/ci.yml) +[![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/) +[![TermSpec 0.2](https://img.shields.io/badge/TermSpec-0.2-315C43)](docs/termspec.md) +[![License: MIT](https://img.shields.io/badge/License-MIT-2F2F2F.svg)](LICENSE) > [!IMPORTANT] -> TermKit 目前是工作名,项目仍处于 pre-alpha 地基阶段。第一版优先稳定数据 -> 契约、插件边界和可复现构建,不提前承诺稳定 API,也不急于制作网站和大词库。 +> TermKit 仍处于 pre-alpha。TermSpec 0.2 与本地 Studio 已按长期架构实现, +> 但在 1.0 之前暂不承诺公共接口完全稳定。 + +![TermKit Studio 概念工作区](.github/screenshots/studio-concepts.jpg) + +Concept、各语言词项、证据、关系和版本状态会同时出现在一个高密度工作区中。 +只有经过明确 Apply 的 ChangeSet 才会修改 canonical files。 ## 项目定位 -TermKit 不是“中英词对数据库”。它把一个专业概念与定义、概念关系、不同语言的 -名称、使用说明、来源证据、审核状态和版本历史联系起来,再把这些纯数据资源打包成 -可安装的 TermPack,供翻译器、PaperLens、OCR、ASR、搜索、RAG 和技术写作工具 -调用。 +TermKit 不把术语库理解为“中英词对表”。一个语言无关的 `Concept` 负责含义、定义、 +领域、关系、证据和生命周期;不同语言的名称是独立 `LexicalEntry`,各自具有状态、 +术语策略、词义说明和证据。 + +```text +Concept → Evidence → Curation → Review → Package → Runtime +``` + +项目包含三个产品面: + +- **TermKit Core:**TermSpec 0.2、TermPack、Forge、Exchange、依赖 lock 与运行时; +- **TermKit Studio:**官方 local-first Web 工作台,覆盖 Concepts、Candidates、 + Review、Sources、Relations、Exchange、Runtime、Releases; +- **TermRegistry:**不可变包的发现与校验安装;本地使用不依赖托管服务。 + +Studio 不是 SaaS 管理后台。它在本机打开术语数据仓库,YAML + JSONL 始终是 canonical +source;SQLite 只负责索引和编辑运行态。每次语义修改必须先形成可审核的 ChangeSet。 -第一阶段只有四个逻辑组件: +## 核心能力 -- **TermSpec:**概念、词项、证据、来源、质量、manifest、依赖和 lock 文件规范; -- **TermRuntime:**最长匹配、词义解析端口、`TermPlan` 和译后术语检查; -- **TermForge:**候选抽取、对齐、证据聚合和双维度审核流程; -- **TermRegistry:**不可变版本的发现、安装、校验和锁定协议。 +- **Concept-first:**一个含义只建模一次,再关联任意数量的 BCP 47 语言词项、定义、 + 关系与证据; +- **受治理的整理流程:**把修改保存为语义操作,语言审核与领域审核分别决策,并保留 + rationale 和 revision history; +- **标准交换层:**支持 CSV/XLSX 映射、TBX/SKOS 规范化、身份冲突预览,以及 + OpenRefine Reconciliation API v0.2; +- **确定性运行时:**解析依赖、生成精确 lock、编译 FTS5 与 Aho–Corasick matcher, + 并解释每个术语决策; +- **Local-first:**canonical 数据保持 Git-friendly,Studio 默认只监听本机回环地址, + 不需要托管账号或专有数据库。 -软件和数据从第一天分离。TermKit 仓库使用 MIT;每个术语包必须独立声明许可证、 -来源、维护者与再分发策略。 +## 快速启动 -## 快速开始 +从源码运行需要 Python 3.11+、uv、Bun 1.3+ 与 Git: ```bash uv sync --all-extras +cd apps/studio && bun install --frozen-lockfile && bun run build && cd ../.. +uv run termkit studio examples/packs/demo-hydraulics +``` + +浏览器会打开 `http://127.0.0.1:8765/studio`。Studio 默认只允许回环地址;它没有 +面向公网的认证与多租户安全边界。正式 wheel 已内嵌 Studio 生产构建,安装用户只需 +安装 `termkit[studio]`;Bun 只是贡献者依赖。 + +命令行能力可以独立使用: + +```bash uv run termkit validate examples/packs/demo-hydraulics uv run termkit pack build examples/packs/demo-hydraulics --output dist/ +uv run termkit runtime compile \ + --pack examples/packs/demo-hydraulics \ + --output dist/demo-hydraulics.termdb \ + --lock-output dist/termkit.lock uv run termkit resolve \ "The boom cylinder is controlled by the pilot valve." \ --pack examples/packs/demo-hydraulics \ @@ -45,35 +89,93 @@ uv run termkit resolve \ --target-language zh-Hans ``` -## 关键工程原则 +## Studio 能做什么 + +| 工作区 | 作用 | +| --- | --- | +| Concepts | Facet 筛选、虚拟列表、概念与多语言词项编辑 | +| Candidates | 结合上下文把 Forge 输出协调为概念,而非接受“词对” | +| Review | 类 PR 语义 diff,语言审核与领域审核独立留痕 | +| Sources | 查看来源身份、权威级别、内容摘要与再分发权限 | +| Relations | 默认树形浏览,并通过 ChangeSet 修改概念关系 | +| Exchange | CSV/XLSX 映射向导,以及 TBX/SKOS 的规范化导入计划 | +| Runtime | 可解释 resolve/check,编译独立 `.termdb` | +| Releases | Schema、引用、语言、许可证、证据等发布门禁 | -- Concept first,language second; -- 语言标识统一使用 BCP 47; -- Git 友好的 JSONL 是内部维护格式,TBX 是交换格式; -- TermPack 只能包含数据,不能夹带可执行插件; -- 插件通过 Python entry points 显式安装和发现; -- 所有发布包不可变、带校验和,可由 lock 文件精确复现; -- 测试默认离线,示例数据必须原创或可再分发; -- 不把“未找到证据”伪装成“已确认术语”。 +`⌘K` / `Ctrl+K` 打开命令面板。界面采用 dense、quiet、structural、 +keyboard-friendly 的专业工具风格。机器/LLM 能力只作为 Forge、消歧、定义建议等后台 +capability 暴露,不把产品做成 AI 聊天项目。 -完整边界见[架构文档](docs/architecture.md)和[安全模型](docs/security.md)。 +### 审核语义变更 -## 当前不做 +![TermKit Studio 审核工作区](.github/screenshots/studio-review.jpg) -- 术语管理 SaaS、应用市场 UI 或多租户云服务; -- 携带版权风险的厂商手册和爬取词库; -- 没有来源和审核状态的批量 AI 词对; -- 自创 TBX dialect、图数据库或默认向量数据库; -- 直接实现所有翻译供应商、抽取器和小语种形态学算法。 +草稿是一组有顺序的语义 diff,而不是不可追踪的表单提交。语言审核和领域审核分别 +给出 rationale,对同一个 ChangeSet 完成决策后,才允许写回 canonical source。 + +### 解释运行时决策 + +![TermKit Studio Runtime Lab](.github/screenshots/studio-runtime.jpg) + +Runtime Lab 展示匹配区间、Concept 身份、来源 pack、目标词项、术语策略和证据。 +同一份受约束结果也可以通过 Python API、本地 HTTP API 或编译后的 `.termdb` 使用。 + +### 验证并构建不可变发布物 + +![TermKit Studio 发布工作区](.github/screenshots/studio-releases.jpg) + +发布门禁先检查 Schema、引用、语言、证据和来源权限,再构建确定性的 `.termpack`; +同一流程会立即校验归档,并显示 SHA-256 摘要。 + +## 整理生命周期 + +```text +source revision → corpus → Forge candidates → reconciliation + → ChangeSet → language/domain review → canonical source + → TermPack + lock → compiled runtime index +``` + +机器或插件输出不会直接成为正式术语,而是先形成带语境与质量信号的 Candidate。 +整理者随后决定匹配已有 Concept、新建 Concept、挂接 LexicalEntry、合并、延后或拒绝。 +完整流程见 [Studio](docs/studio.md)、[Exchange](docs/exchange.md) 和 +[Runtime](docs/runtime.md)。 + +## 数据与运行时边界 + +```text +canonical YAML + JSONL + │ + ▼ +workspace.sqlite ── ChangeSet ── Review ── 原子写回 canonical source + │ + TermPack + dependency lock + ▼ + 独立 .termdb(FTS5 + matcher) +``` + +- `.termkit/workspace.sqlite` 保存本地草稿、候选、审核、历史与派生索引; +- `.termkit/runtime/*.termdb` 只保存已解析 pack 集合、FTS5 与序列化的确定性 + Aho–Corasick matcher; +- 删除 `.termkit/` 不会删除 canonical 术语数据,但会删除尚未应用的本地草稿。 + +正式原创种子数据放在独立同级仓库 **TermKit-Packs**,从第一天按长期依赖层级维护: +机械基础 → 液压/工程机械 → 挖掘机,以及 ML 基础 → 计算机视觉 → 目标检测。 ## 开发验证 ```bash uv run ruff check . uv run mypy src +uv run python scripts/export_schemas.py --check uv run pytest --cov -uv build +cd apps/studio && bun run check && bun run test && bun run build +cd ../.. && uv run python scripts/embed_studio_assets.py --check && uv build ``` -提交结构性改动前请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 与 -[AGENTS.md](AGENTS.md)。 +## 安全与非目标 + +TermPack 只能包含数据,不能携带可执行插件;受限来源不能发布摘录;Studio 默认不得 +直接暴露公网。项目不捆绑爬取/私有词库,不自动批准机器候选,不自创 TBX 方言,也不 +把术语工程做成 chat-first AI 产品。当前不建设多租户 SaaS 或远程插件市场。 + +软件代码使用 [MIT License](LICENSE)。每个术语包及其来源必须独立声明授权。 diff --git a/TERM_KIT_STUDIO_ARCHITECTURE.md b/TERM_KIT_STUDIO_ARCHITECTURE.md new file mode 100644 index 0000000..c69cc94 --- /dev/null +++ b/TERM_KIT_STUDIO_ARCHITECTURE.md @@ -0,0 +1,389 @@ +# TermKit Studio architecture + +Status: Accepted for implementation +Target: TermSpec 0.2 / TermKit 0.2 pre-alpha +Last reviewed: 2026-08-12 + +## 1. Product boundary + +TermKit Studio is the official local-first terminology engineering workbench. +It is not a hosted multi-tenant SaaS, a registry marketplace, a chat client, or +the canonical terminology store. It provides a professional interface over the +same public TermSpec, TermPack, Forge, Runtime, and Registry contracts used by +other applications. + +The product has three surfaces: + +```text +TermKit Core schemas, workflows, packages, runtime, plugin contracts +TermKit Studio local curation, review, evidence, testing, and release UI +TermRegistry package discovery, immutable distribution, and locking +``` + +Studio is a browser application served by a local FastAPI process. A native +wrapper may be added later, but desktop packaging remains an adapter. + +## 2. Non-negotiable invariants + +1. Concepts remain language-neutral. Language and script belong to lexical + entries. +2. YAML and deterministic JSONL in a terminology repository remain canonical. + SQLite never silently replaces or outranks those files. +3. Every edit is first represented by a draft `ChangeSet`; a UI save operation + never writes canonical files directly. +4. Candidate production and governed approval are separate. Machine-produced + records cannot advance themselves to `APPROVED` or `STANDARDIZED`. +5. Source rights, evidence state, and review state are visible domain data, not + optional UI metadata. +6. Studio workspace storage and compiled Runtime storage are separate files, + schemas, APIs, and lifecycle concepts. +7. TermPacks contain data only. Installed Python plugins are explicitly trusted + code and are never loaded by inspecting a pack or opening a workspace. +8. Applying a ChangeSet is transactional from the user's perspective: either a + validated deterministic canonical snapshot is written, or no source files + change. + +## 3. Repository and deployment layout + +```text +src/termkit/ + spec/ public semantic contracts + pack/ canonical load, validation, and release archives + workspace/ local application engine and SQLite adapters + forge/ reproducible pipeline orchestration and ports + runtime/ resolution and compiled runtime index + exchange/ standards-based exchange adapters + server/routes/ HTTP adapters grouped by user capability + studio_assets/ packaged production frontend, generated at release + +apps/studio/ React + TypeScript + Vite source +alembic/ workspace schema migration environment +examples/ original fixtures only, never production datasets +``` + +The Python package can be installed without Studio dependencies. The `studio` +extra provides FastAPI, Uvicorn, SQLAlchemy, Alembic, and spreadsheet support. +The frontend is a static SPA in production and a Vite development server during +frontend work. + +## 4. Dependency direction + +```text + apps/studio + | + server/routes + | + +---------------+---------------+ + | | | + workspace service registry adapters + / | \ | | + exchange forge pack runtime registry + \ | / / \ + TermSpec ---- pack plugin protocols +``` + +- `spec` imports no application, persistence, plugin, server, or provider code. +- `workspace` is an application layer. It may consume `spec`, `pack`, `forge`, + and `exchange` ports, but none of those modules may import `workspace`. +- `server` and `apps/studio` translate requests and events; they do not decide + evidence sufficiency, review completion, or release validity. +- Runtime compiled storage is owned by `runtime`, not `workspace`. +- SQLAlchemy entities are persistence records and never become TermSpec models. + +The architecture check rejects both absolute and relative forbidden imports. + +## 5. TermSpec 0.2 semantic model + +TermSpec 0.2 adds explicit long-lived entities rather than hiding them in UI or +database fields. + +### Knowledge organization + +- `DomainScheme`: a versioned domain taxonomy and its maintainers. +- `DomainNode`: a stable node with labels, parent links, notation, and scope. +- `ExternalIdentifier`: a typed link to an external concept system with match + type, confidence, and evidence. + +### Concept-first candidate pool + +- `CandidateConcept`: a meaning hypothesis, domain hypotheses, source + occurrences, definitions, and possible existing-concept matches. +- `CandidateLexicalEntry`: one language-specific surface linked to a candidate + concept; it is never a source-to-target pair. +- `CandidateRelation`: a proposed semantic relation. +- `CandidateAlignment`: evidence that two candidate lexical entries express the + same candidate concept in different languages. + +`CandidateTerm` remains only as a deprecated compatibility input during the +0.2 cycle and is converted at the Forge boundary. + +### Governed change and review + +- `ChangeSet`: author, base revision, reason, source run, ordered operations, + review references, and lifecycle state. +- `ChangeOperation`: typed create/update/remove operation with before/after + snapshots and an explicit subject identity. +- `ReviewTask`: required review dimensions and assigned roles. +- `ReviewDecision`: approve, request changes, reject, or defer with rationale. +- `ConceptRevision` and `LexicalRevision`: immutable revision metadata and + content digests. + +Language review and domain review are distinct requirements. A complete review +policy may require both without assuming one person fills both roles. + +### Sources and corpora + +- `SourceRevision`: immutable retrieval/content identity for a source. +- `Corpus`: segmentation configuration and source revision set. +- `CorpusSegment`: language, structural location, text digest, optional parallel + group, and rights-aware excerpt state. + +Missing extraction, unattested proposals, rejected proposals, and confirmed +absence remain different states. + +## 6. Canonical source and workspace lifecycle + +Opening a terminology repository follows this sequence: + +```text +validate path and manifest + -> hash canonical release files + -> migrate workspace.sqlite + -> compare imported source digest + -> rebuild derived canonical tables when source changed + -> preserve drafts, reviews, candidate pool, UI state, and history +``` + +The default local layout is: + +```text +/.termkit/workspace.sqlite +/.termkit/artifacts/ +/.termkit/runtime/.termdb +``` + +`.termkit/` is disposable and ignored by Git. Rebuilding it must not lose +canonical terminology, but may lose explicitly documented local-only drafts if +the user deletes it. + +### Workspace database responsibilities + +The workspace database stores: + +- indexed copies of canonical concepts, lexical entries, relations, evidence, + sources, and domain nodes; +- draft ChangeSets and ordered operations; +- review tasks and decisions; +- candidate concepts, lexical entries, relations, alignments, and occurrences; +- Forge run metadata, stage state, artifact references, and statistics; +- corpus segments, operation history, UI state, and search indices. + +It does not store a released TermPack as its authoritative representation. + +### Runtime compiled database responsibilities + +A `.termdb` contains only data derived from a resolved TermLock: + +- exact and normalized lexical indices; +- SQLite FTS5 search tables; +- deterministic scan automata metadata; +- pack coordinates and digests; +- no drafts, reviews, UI state, or Forge runs. + +## 7. ChangeSet transaction model + +Draft editing is append-oriented: + +```text +create draft ChangeSet + -> append validated operations + -> preview aggregate diff + -> submit for review when policy requires + -> apply in one workspace transaction + -> validate prospective PackData + -> render deterministic files to sibling temporary files + -> atomically replace canonical files + -> append immutable history event +``` + +Operations have stable IDs and optimistic `base_revision` checks. Conflicts +produce a structured response; last-writer-wins is forbidden. + +Undo creates a new compensating ChangeSet from stored `before` snapshots. Redo +creates another forward ChangeSet. History is never rewritten. + +The initial operation vocabulary is: + +```text +CREATE_CONCEPT UPDATE_CONCEPT REMOVE_CONCEPT +SET_CONCEPT_STATUS +ADD_LEXICAL_ENTRY UPDATE_LEXICAL_ENTRY REMOVE_LEXICAL_ENTRY +ADD_RELATION REMOVE_RELATION +ADD_EVIDENCE UPDATE_EVIDENCE REMOVE_EVIDENCE +LINK_EXTERNAL_ID UNLINK_EXTERNAL_ID +``` + +## 8. Forge reproducibility + +`ForgeRun` stores metadata, never an unbounded embedded candidate list: + +- input source revision IDs and content digests; +- ordered stages and stage results; +- plugin descriptors and versions; +- model/provider identity and prompt/config revision where applicable; +- deterministic seed; +- artifact references and their digests; +- counts, timings, warnings, and errors. + +Candidates live in separate workspace tables. The fixed logical flow is: + +```text +source intake -> normalize/segment -> candidate mining -> deduplicate/cluster +-> concept reconciliation -> cross-language alignment -> evidence aggregation +-> automated QA -> candidate review -> language review -> domain review +-> ChangeSet -> canonical source -> release +``` + +Algorithms remain plugins. Plausible extractor implementations include rules, +statistical termhood, model-backed proposals, and hybrid ensembles. They all +emit the same concept-first candidate contracts. + +## 9. Exchange boundary + +Supported capability targets are native JSONL/TermPack, TBX, CSV, XLSX, and +SKOS. Exchange adapters produce an intermediate import plan: + +```text +read -> normalize -> map -> preview -> identity strategy -> reconcile +-> conflict decisions -> ChangeSet +``` + +Spreadsheet mapping is explicit and saved as reusable mapping profiles. TBX and +SKOS adapters preserve source identifiers and record unsupported constructs as +diagnostics; they never silently flatten semantics into canonical files. + +## 10. Studio HTTP API + +All Studio endpoints live under `/studio/api/v1`. Public resolver/checker APIs +remain under `/v1` and do not acquire workspace behavior. + +```text +GET /workspace +GET /concepts search, facets, cursor/page +GET /concepts/{id} +POST /changesets +POST /changesets/{id}/operations +POST /changesets/{id}/submit +POST /changesets/{id}/apply +POST /history/{id}/revert +GET /candidates +POST /candidates/{id}/decision +GET /reviews +POST /reviews/{id}/decisions +GET /sources +GET /forge/runs +POST /forge/runs +POST /exchange/preview +POST /runtime/resolve +POST /runtime/check +POST /releases/validate +POST /releases/build +``` + +The API is local-only by default. It rejects non-loopback binding unless the +operator supplies an explicit unsafe/hosted configuration. The local API has no +multi-user authorization and must never be presented as an internet service. + +## 11. Studio information architecture + +Navigation follows user work rather than source modules: + +```text +Concepts Candidates Review Sources Relations +Exchange Runtime Releases +``` + +The product shell is dense, quiet, structural, keyboard-friendly, and designed +for resizable panes and large collections. It does not use landing heroes, +metric-card dashboards, chat-first layouts, decorative gradients, AI sparkle, +or fake controls. + +### Concepts workspace + +The first complete work surface is: + +```text +facets -> virtualized concept list -> concept editor -> contextual inspector +``` + +It supports definitions, domains, BCP 47 languages, lexical status/policy, +morphology, relations, evidence, external identifiers, review state, history, +and pinned side-by-side languages. Every mutation creates or updates a draft +ChangeSet. + +### Remaining work surfaces + +- Candidates: evidence context and concept reconciliation decisions. +- Review: ChangeSet diff, separate language/domain decisions, rationale. +- Sources: authority, rights, revision, corpus, candidates, and evidence usage. +- Relations: tree-first editing with an optional graph inspection view. +- Exchange: mapping, preview, reconciliation, and conflicts. +- Runtime: explainable resolve/check routes and match traces. +- Releases: validation, semantic diff, build, verify, install, and publish ports. + +Unavailable capabilities are labeled as unavailable; the UI never simulates +successful backend work. + +## 12. Frontend implementation + +`apps/studio` uses React, TypeScript, and Vite with: + +- TanStack Query for server state; +- TanStack Table and Virtual for dense/large lists; +- Radix primitives for accessible menus, dialogs, and resizable interactions; +- CSS variables and a small project-owned component layer; +- a command palette and documented keyboard shortcuts; +- frontend unit tests plus a production build in CI. + +The frontend consumes API boundary types. It does not copy evidence or review +policy logic from Python. + +## 13. Security and rights + +- Workspace paths are resolved and tied to the CLI-opened project root. +- Canonical writes use an allowlist and atomic replacement. +- Import size, XML entity behavior, spreadsheet formulas, archives, and file + names are treated as untrusted input. +- Source excerpts are redacted from publishable output according to + redistribution policy. +- Plugin loading remains off for third-party distributions by default. +- Git actions are explicit user operations through `GitWorkspaceAdapter`; no + automatic push or credential discovery is part of the workspace engine. + +## 14. Migration and delivery order + +1. Add TermSpec 0.2 models while retaining explicit 0.1 compatibility readers. +2. Replace Forge's pair-oriented candidate storage with concept-first records. +3. Add Alembic-managed workspace storage and deterministic ChangeSet apply. +4. Add Studio API and CLI composition. +5. Build the final Studio shell and complete Concepts. +6. Add candidates, review, sources, Forge runs, relations, Runtime, releases. +7. Add exchange adapters, runtime compiler, lock resolution, and benchmarks. +8. Establish the separate official pack hierarchy using original/licensed data. + +Each step keeps offline tests green, regenerates schemas, updates compatibility +notes, and avoids claiming deferred capability as complete. + +## 15. Architecture acceptance checks + +- Deleting `workspace.sqlite` and reopening recreates canonical indexed data. +- A draft edit does not change any canonical source file. +- Applying an invalid ChangeSet changes neither DB canonical state nor files. +- Applying a valid ChangeSet produces deterministic, reviewable Git diffs. +- Undo/redo are new audited operations, not history mutation. +- A Forge run can reference 50,000 candidates without embedding them in one row. +- The Runtime compiler never reads Studio draft/review tables. +- A TermPack inspection never imports plugin code. +- Rights-restricted excerpts cannot enter a release. +- Studio production build, Python 3.11–3.13 tests, schemas, and package build pass + in CI. diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..ce18c9e --- /dev/null +++ b/alembic.ini @@ -0,0 +1,38 @@ +[alembic] +script_location = src/termkit/workspace/migrations +prepend_sys_path = . +path_separator = os + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/apps/studio/bun.lock b/apps/studio/bun.lock new file mode 100644 index 0000000..452ad80 --- /dev/null +++ b/apps/studio/bun.lock @@ -0,0 +1,381 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@termkit/studio", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@tanstack/react-query": "^5.90.21", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.18", + "lucide-react": "^0.545.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + }, + "devDependencies": { + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.0.4", + "typescript": "^5.9.3", + "vite": "^7.1.12", + "vitest": "^4.0.4", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="], + + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], + + "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.2.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.23", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-effect-event": "0.0.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.6", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.16", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.4", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.17", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.10", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.10", "", { "dependencies": { "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.5", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.4", "", { "os": "android", "cpu": "arm" }, "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.4", "", { "os": "android", "cpu": "arm64" }, "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.4", "", { "os": "none", "cpu": "arm64" }, "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.101.4", "", {}, "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.101.4", "", { "dependencies": { "@tanstack/query-core": "5.101.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA=="], + + "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="], + + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.9", "", { "dependencies": { "@tanstack/virtual-core": "3.17.7" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-qZyr0FZDP8rDC4WBhsryIZmAd9bveJvFGUJJtskWaew6/0dTRS6wZxnR6VQ5bY2KwL3LjerrHqQLk3a0GKcPXQ=="], + + "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], + + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.7", "", {}, "sha512-bp+v10y65sp2H7WpWfIMyxTNfl8ZVfxFTLRjPIFRryi6FV/J33z4IS53WO4pTk36KlvJ4iLiQz+oaydDC1xbcA=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], + + "@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], + + "@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + + "@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="], + + "@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], + + "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.13", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ=="], + + "browserslist": ["browserslist@4.28.8", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.404", "", {}, "sha512-3WJtd7/lVq2Jnuz6wed1l9+1ZD2u2Tet1/1NBc4Iedkmgbu+I7YuAqdAQ8T+VZtnwysMsAf3IqSq9D1gyZjA2g=="], + + "es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], + + "esbuild": ["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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "lucide-react": ["lucide-react@0.545.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + + "node-releases": ["node-releases@2.0.53", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="], + + "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + + "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], + + "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], + + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "rollup": ["rollup@4.62.4", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.4", "@rollup/rollup-android-arm64": "4.62.4", "@rollup/rollup-darwin-arm64": "4.62.4", "@rollup/rollup-darwin-x64": "4.62.4", "@rollup/rollup-freebsd-arm64": "4.62.4", "@rollup/rollup-freebsd-x64": "4.62.4", "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", "@rollup/rollup-linux-arm-musleabihf": "4.62.4", "@rollup/rollup-linux-arm64-gnu": "4.62.4", "@rollup/rollup-linux-arm64-musl": "4.62.4", "@rollup/rollup-linux-loong64-gnu": "4.62.4", "@rollup/rollup-linux-loong64-musl": "4.62.4", "@rollup/rollup-linux-ppc64-gnu": "4.62.4", "@rollup/rollup-linux-ppc64-musl": "4.62.4", "@rollup/rollup-linux-riscv64-gnu": "4.62.4", "@rollup/rollup-linux-riscv64-musl": "4.62.4", "@rollup/rollup-linux-s390x-gnu": "4.62.4", "@rollup/rollup-linux-x64-gnu": "4.62.4", "@rollup/rollup-linux-x64-musl": "4.62.4", "@rollup/rollup-openbsd-x64": "4.62.4", "@rollup/rollup-openharmony-arm64": "4.62.4", "@rollup/rollup-win32-arm64-msvc": "4.62.4", "@rollup/rollup-win32-ia32-msvc": "4.62.4", "@rollup/rollup-win32-x64-gnu": "4.62.4", "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "update-browserslist-db": ["update-browserslist-db@1.3.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "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", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="], + + "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + } +} diff --git a/apps/studio/index.html b/apps/studio/index.html new file mode 100644 index 0000000..42e2490 --- /dev/null +++ b/apps/studio/index.html @@ -0,0 +1,13 @@ + + + + + + + TermKit Studio + + +
+ + + diff --git a/apps/studio/package.json b/apps/studio/package.json new file mode 100644 index 0000000..630d74c --- /dev/null +++ b/apps/studio/package.json @@ -0,0 +1,30 @@ +{ + "name": "@termkit/studio", + "version": "0.2.0-alpha.0", + "private": true, + "type": "module", + "packageManager": "bun@1.3.14", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "check": "tsc -b --pretty false", + "test": "vitest run" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@tanstack/react-query": "^5.90.21", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.18", + "lucide-react": "^0.545.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.0.4", + "typescript": "^5.9.3", + "vite": "^7.1.12", + "vitest": "^4.0.4" + } +} diff --git a/apps/studio/src/App.tsx b/apps/studio/src/App.tsx new file mode 100644 index 0000000..11f0e96 --- /dev/null +++ b/apps/studio/src/App.tsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; + +import { api } from "./api/client"; +import { CommandPalette } from "./components/CommandPalette"; +import { Shell } from "./components/Shell"; +import { ErrorState, LoadingState } from "./components/ui"; +import { pageFromHash, type PageId } from "./navigation"; +import { CandidatesPage } from "./pages/CandidatesPage"; +import { ConceptsPage } from "./pages/ConceptsPage"; +import { ExchangePage } from "./pages/ExchangePage"; +import { RelationsPage } from "./pages/RelationsPage"; +import { ReleasesPage } from "./pages/ReleasesPage"; +import { ReviewPage } from "./pages/ReviewPage"; +import { RuntimePage } from "./pages/RuntimePage"; +import { SourcesPage } from "./pages/SourcesPage"; + +const pages: Record React.JSX.Element> = { + concepts: ConceptsPage, + candidates: CandidatesPage, + review: ReviewPage, + sources: SourcesPage, + relations: RelationsPage, + exchange: ExchangePage, + runtime: RuntimePage, + releases: ReleasesPage +}; + +export function App() { + const [page, setPage] = useState(pageFromHash()); + const [commandOpen, setCommandOpen] = useState(false); + const workspace = useQuery({ queryKey: ["workspace"], queryFn: api.workspace }); + + useEffect(() => { + const update = () => setPage(pageFromHash()); + window.addEventListener("hashchange", update); + return () => window.removeEventListener("hashchange", update); + }, []); + + useEffect(() => { + const keyboard = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { + event.preventDefault(); + setCommandOpen(true); + } + }; + window.addEventListener("keydown", keyboard); + return () => window.removeEventListener("keydown", keyboard); + }, []); + + const Page = pages[page]; + return ( + setCommandOpen(true)}> + {workspace.isPending ? ( + + ) : workspace.isError ? ( + + ) : ( + + )} + + + ); +} diff --git a/apps/studio/src/api/client.ts b/apps/studio/src/api/client.ts new file mode 100644 index 0000000..a14834c --- /dev/null +++ b/apps/studio/src/api/client.ts @@ -0,0 +1,142 @@ +import type { + CandidateConcept, + CandidateDetail, + ChangeSet, + ConceptDetail, + ConceptListResult, + Relation, + ResolutionResult, + RuntimeCheckResult, + ReviewDecision, + ReviewTask, + SourceSummary, + ExchangePreview, + ExchangeRequest, + WorkspaceSummary +} from "./types"; + +export class ApiError extends Error { + constructor( + message: string, + readonly status: number, + readonly code = "API_ERROR" + ) { + super(message); + } +} + +async function request(path: string, init?: RequestInit): Promise { + const response = await fetch(`/studio/api/v1${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + ...init?.headers + } + }); + if (!response.ok) { + const body = (await response.json().catch(() => ({}))) as { error?: string; code?: string }; + throw new ApiError(body.error ?? response.statusText, response.status, body.code); + } + return (await response.json()) as T; +} + +function queryString(values: Record): string { + const params = new URLSearchParams(); + Object.entries(values).forEach(([key, value]) => { + if (value !== undefined && value !== "") params.set(key, String(value)); + }); + const encoded = params.toString(); + return encoded ? `?${encoded}` : ""; +} + +export const api = { + workspace: () => request("/workspace"), + concepts: (filters: Record) => + request(`/concepts${queryString(filters)}`), + concept: (id: string) => request(`/concepts/${encodeURIComponent(id)}`), + changesets: (status = "") => request(`/changesets${queryString({ status })}`), + createChangeset: (body: { author: string; reason: string; source_run_id?: string }) => + request("/changesets", { method: "POST", body: JSON.stringify(body) }), + addOperation: ( + changesetId: string, + body: { + kind: string; + subject_kind: string; + subject_id: string; + after: Record | null; + } + ) => + request(`/changesets/${encodeURIComponent(changesetId)}/operations`, { + method: "POST", + body: JSON.stringify(body) + }), + applyChangeset: (id: string) => + request<{ changeset: ChangeSet; source_revision: string }>( + `/changesets/${encodeURIComponent(id)}/apply`, + { method: "POST" } + ), + submitChangeset: (id: string, title: string, dimensions: string[]) => + request(`/changesets/${encodeURIComponent(id)}/submit`, { + method: "POST", + body: JSON.stringify({ title, required_dimensions: dimensions }) + }), + reviews: (status = "") => request(`/reviews${queryString({ status })}`), + reviewDecision: ( + id: string, + body: Omit + ) => + request(`/reviews/${encodeURIComponent(id)}/decisions`, { + method: "POST", + body: JSON.stringify(body) + }), + candidates: (state = "") => + request(`/candidates${queryString({ state })}`), + candidate: (id: string) => + request(`/candidates/${encodeURIComponent(id)}`), + decideCandidate: ( + id: string, + body: { decision: string; actor: string; target_id?: string; rationale?: string } + ) => + request<{ candidate_concept_id: string; changeset_id: string; state: string }>( + `/candidates/${encodeURIComponent(id)}/decision`, + { method: "POST", body: JSON.stringify(body) } + ), + sources: () => request("/sources"), + relations: () => request("/relations"), + forgeRuns: () => request>>("/forge/runs"), + inspectExchange: (body: ExchangeRequest) => + request("/exchange/preview", { method: "POST", body: JSON.stringify(body) }), + createExchangeChangeset: (body: ExchangeRequest) => + request("/exchange/changeset", { method: "POST", body: JSON.stringify(body) }), + resolve: (body: { + text: string; + source_language: string; + target_language: string; + domain_hints?: string[]; + }) => request("/runtime/resolve", { method: "POST", body: JSON.stringify(body) }), + check: (body: { + source: string; + translation: string; + source_language: string; + target_language: string; + domain_hints?: string[]; + }) => + request( + "/runtime/check", + { method: "POST", body: JSON.stringify(body) } + ), + compileRuntime: () => + request<{ database_path: string; lexical_count: number; matcher_nodes: number }>( + "/runtime/compile", + { method: "POST" } + ), + validateRelease: () => + request<{ coordinate: string; issues: Array<{ code: string; message: string; severity: string }> }>( + "/releases/validate", + { method: "POST" } + ), + buildRelease: () => + request<{ archive: string; verification: Record }>("/releases/build", { + method: "POST" + }) +}; diff --git a/apps/studio/src/api/types.ts b/apps/studio/src/api/types.ts new file mode 100644 index 0000000..4a01e1e --- /dev/null +++ b/apps/studio/src/api/types.ts @@ -0,0 +1,327 @@ +export type CandidateState = + | "PROPOSED" + | "ATTESTED" + | "LINGUIST_REVIEWED" + | "DOMAIN_REVIEWED" + | "APPROVED" + | "STANDARDIZED" + | "DEPRECATED" + | "REJECTED"; + +export interface Definition { + language: string; + text: string; + source_ids: string[]; +} + +export interface ExternalIdentifier { + scheme: string; + identifier: string; + uri: string; + match_type: "EXACT" | "CLOSE" | "BROADER" | "NARROWER" | "RELATED"; + confidence: number; + evidence_ids: string[]; +} + +export interface Concept { + concept_id: string; + domain_ids: string[]; + definitions: Definition[]; + notes: string[]; + external_identifiers: ExternalIdentifier[]; + revision: number; + status: CandidateState; +} + +export interface Morphology { + lemma: string; + part_of_speech: string; + grammatical_gender: string; + inflection_class: string; + forms: Record; +} + +export interface LexicalEntry { + term_id: string; + concept_id: string; + language: string; + text: string; + normalized: string; + script: string; + region: string; + status: "PREFERRED" | "ADMITTED" | "DEPRECATED" | "REJECTED"; + policy: "KEEP" | "MUST" | "PREFER" | "ALLOW" | "AVOID"; + sense: string; + usage_note: string; + abbreviation: boolean; + transliteration: string; + aliases: string[]; + morphology: Morphology; + evidence_ids: string[]; + candidate_state: CandidateState; +} + +export interface Relation { + relation_id: string; + source_concept_id: string; + target_concept_id: string; + relation_type: "BROADER" | "NARROWER" | "RELATED" | "PART_OF"; + evidence_ids: string[]; +} + +export interface Evidence { + evidence_id: string; + source_id: string; + concept_id: string; + term_ids: string[]; + location: string; + source_text: string; + target_text: string; + evidence_type: string; + retrieved_at: string; + publishable_excerpt: boolean; +} + +export interface Source { + source_id: string; + title: string; + publisher: string; + languages: string[]; + domain_ids: string[]; + source_type: string; + uri: string; + published_at: string; + retrieved_at: string; + license: string; + redistribution_policy: + | "REDISTRIBUTABLE" + | "DERIVATION_ALLOWED" + | "REFERENCE_ONLY" + | "RESTRICTED"; + content_sha256: string; + authority_level: string; +} + +export interface FacetValue { + value: string; + count: number; +} + +export interface ConceptSummary { + concept_id: string; + display_label: string; + status: CandidateState; + revision: number; + domain_ids: string[]; + languages: string[]; + lexical_preview: string[]; + evidence_count: number; +} + +export interface ConceptListResult { + items: ConceptSummary[]; + total: number; + limit: number; + offset: number; + facets: { + statuses: FacetValue[]; + domains: FacetValue[]; + languages: FacetValue[]; + }; +} + +export interface ChangeOperation { + operation_id: string; + kind: string; + subject_kind: string; + subject_id: string; + before: Record | null; + after: Record | null; + sequence: number; +} + +export interface ChangeSet { + changeset_id: string; + author: string; + created_at: string; + updated_at: string; + base_revision: string; + reason: string; + source_run_id: string; + operations: ChangeOperation[]; + review_ids: string[]; + status: "DRAFT" | "IN_REVIEW" | "APPROVED" | "APPLIED" | "REJECTED" | "REVERTED"; +} + +export interface ConceptDetail { + concept: Concept; + lexical_entries: LexicalEntry[]; + relations: Relation[]; + evidence: Evidence[]; + history: Record[]; + draft_changesets: ChangeSet[]; +} + +export interface WorkspaceSummary { + root: string; + database_path: string; + coordinate: string; + source_revision: string; + concept_count: number; + lexical_count: number; + source_count: number; + evidence_count: number; + languages: string[]; + dirty_changesets: number; + validation_ok: boolean; + validation_errors: number; +} + +export interface CandidateConcept { + candidate_concept_id: string; + meaning_hypothesis: string; + domain_ids: string[]; + definitions: Definition[]; + occurrence_ids: string[]; + existing_matches: Array<{ + concept_id: string; + confidence: number; + rationale: string; + evidence_ids: string[]; + }>; + state: CandidateState; + quality: Record; + derivation: string[]; +} + +export interface CandidateLexicalEntry { + candidate_lexical_id: string; + candidate_concept_id: string; + language: string; + text: string; + suggested_status: string; + suggested_policy: string; + occurrence_ids: string[]; + evidence_ids: string[]; + state: CandidateState; +} + +export interface CorpusSegment { + segment_id: string; + corpus_id: string; + source_revision_id: string; + language: string; + location: string; + structure: string; + text: string; + excerpt_state: string; +} + +export interface CandidateDetail { + candidate: CandidateConcept; + lexical_entries: CandidateLexicalEntry[]; + occurrences: CorpusSegment[]; +} + +export interface ReviewDecision { + decision_id: string; + task_id: string; + dimension: "LANGUAGE" | "DOMAIN" | "EVIDENCE" | "RIGHTS"; + reviewer: string; + decision: "APPROVE" | "REQUEST_CHANGES" | "REJECT" | "DEFER"; + rationale: string; + created_at: string; +} + +export interface ReviewTask { + task_id: string; + changeset_id: string; + title: string; + required_dimensions: ReviewDecision["dimension"][]; + assigned_reviewers: Partial>; + decisions: ReviewDecision[]; + status: "OPEN" | "CHANGES_REQUESTED" | "APPROVED" | "REJECTED"; + created_at: string; + updated_at: string; +} + +export interface SourceSummary { + source: Source; + evidence_count: number; + concept_count: number; + revision_count: number; + candidate_count: number; + segment_count: number; +} + +export interface ResolutionResult { + source_language: string; + target_language: string; + pack_coordinates: string[]; + warnings: Array<{ code: string; message: string; source_start: number; source_end: number }>; + matches: Array<{ + source_start: number; + source_end: number; + source_text: string; + source_term_id: string; + concept_id: string; + target_term_id: string; + target_text: string; + policy: string; + sense: string; + confidence: number; + pack_id: string; + pack_version: string; + evidence_ids: string[]; + }>; +} + +export interface RuntimeCheckResult { + resolution: ResolutionResult; + check: { + violations: Array<{ + code: string; + concept_id: string; + expected: string; + found: string; + severity: "ERROR" | "WARNING"; + message: string; + }>; + }; +} + +export interface ExchangeMapping { + column: string; + target: + | "IGNORE" + | "LEXICAL_TEXT" + | "DEFINITION" + | "CONCEPT_ID" + | "SOURCE" + | "DOMAIN" + | "STATUS" + | "BROADER" + | "RELATED"; + language: string; +} + +export interface ExchangeRequest { + filename: string; + format: "csv" | "xlsx" | "tbx" | "skos"; + content_base64: string; + mappings: ExchangeMapping[]; + identity_strategy: "CREATE" | "RECONCILE"; +} + +export interface ExchangePreview { + filename: string; + format: string; + columns: string[]; + rows: Array>; + row_count: number; + suggested_mappings: ExchangeMapping[]; + proposed_concepts: number; + proposed_lexicals: number; + reconciled_concepts: number; + conflicts: string[]; +} diff --git a/apps/studio/src/components/CommandPalette.tsx b/apps/studio/src/components/CommandPalette.tsx new file mode 100644 index 0000000..e1d8c4f --- /dev/null +++ b/apps/studio/src/components/CommandPalette.tsx @@ -0,0 +1,66 @@ +import { useEffect, useMemo, useState } from "react"; +import * as Dialog from "@radix-ui/react-dialog"; +import { Command, Search, X } from "lucide-react"; + +import { navigate, navigation, type PageId } from "../navigation"; + +interface CommandPaletteProps { + open: boolean; + onOpenChange: (value: boolean) => void; +} + +export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) { + const [query, setQuery] = useState(""); + useEffect(() => { + if (!open) setQuery(""); + }, [open]); + const commands = useMemo( + () => navigation.filter((item) => item.label.toLowerCase().includes(query.toLowerCase())), + [query] + ); + + const select = (page: PageId) => { + navigate(page); + onOpenChange(false); + }; + + return ( + + + + + Command palette +
+ + setQuery(event.target.value)} + placeholder="Search commands…" + /> + + + +
+
+
Open workspace
+ {commands.map((item) => { + const Icon = item.icon; + return ( + + ); + })} + {commands.length === 0 &&
No matching command
} +
+
+ Keyboard-first workspace navigation +
+
+
+
+ ); +} diff --git a/apps/studio/src/components/Shell.tsx b/apps/studio/src/components/Shell.tsx new file mode 100644 index 0000000..d3843e2 --- /dev/null +++ b/apps/studio/src/components/Shell.tsx @@ -0,0 +1,78 @@ +import type { ReactNode } from "react"; +import { Command, Search } from "lucide-react"; + +import type { WorkspaceSummary } from "../api/types"; +import { navigate, navigation, productIcon, type PageId } from "../navigation"; +import { Badge } from "./ui"; + +interface ShellProps { + activePage: PageId; + workspace?: WorkspaceSummary; + onOpenCommand: () => void; + children: ReactNode; +} + +export function Shell({ activePage, workspace, onOpenCommand, children }: ShellProps) { + const ProductIcon = productIcon; + const coordinate = workspace?.coordinate ?? "Opening workspace"; + const branchState = workspace?.dirty_changesets ? `${workspace.dirty_changesets} drafts` : "clean"; + + return ( +
+
+
+ + TermKit Studio +
+
+ {coordinate} + / + main + {branchState} +
+ +
+ + + +
{children}
+ +
+ {workspace?.concept_count.toLocaleString() ?? "—"} concepts + {workspace?.languages.join(" · ") ?? "—"} + {workspace?.evidence_count.toLocaleString() ?? "—"} evidence records + + + {workspace?.source_revision.slice(0, 10) ?? "no revision"} + +
+
+ ); +} diff --git a/apps/studio/src/components/ui.tsx b/apps/studio/src/components/ui.tsx new file mode 100644 index 0000000..b7705f9 --- /dev/null +++ b/apps/studio/src/components/ui.tsx @@ -0,0 +1,68 @@ +import type { ReactNode } from "react"; +import { AlertCircle, CheckCircle2, LoaderCircle } from "lucide-react"; + +export function Badge({ children, tone = "neutral" }: { children: ReactNode; tone?: string }) { + return {children}; +} + +export function StatusBadge({ status }: { status: string }) { + const tone = + status === "APPROVED" || status === "STANDARDIZED" || status === "APPLIED" + ? "approved" + : status === "REJECTED" || status === "FAILED" + ? "rejected" + : status === "DEPRECATED" || status === "REVERTED" + ? "deprecated" + : status === "IN_REVIEW" || status === "AWAITING_REVIEW" || status === "OPEN" + ? "review" + : "proposal"; + return {status.replaceAll("_", " ")}; +} + +export function LoadingState({ label = "Loading" }: { label?: string }) { + return ( +
+ + {label} +
+ ); +} + +export function ErrorState({ error }: { error: unknown }) { + return ( +
+ + {error instanceof Error ? error.message : "The operation failed"} +
+ ); +} + +export function EmptyState({ title, detail }: { title: string; detail: string }) { + return ( +
+ + {title} +

{detail}

+
+ ); +} + +export function Section({ + title, + action, + children +}: { + title: string; + action?: ReactNode; + children: ReactNode; +}) { + return ( +
+
+

{title}

+ {action} +
+ {children} +
+ ); +} diff --git a/apps/studio/src/main.tsx b/apps/studio/src/main.tsx new file mode 100644 index 0000000..6957e69 --- /dev/null +++ b/apps/studio/src/main.tsx @@ -0,0 +1,27 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +import { App } from "./App"; +import "./styles.css"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 10_000, + retry: 1, + refetchOnWindowFocus: false + } + } +}); + +const root = document.getElementById("root"); +if (!root) throw new Error("Studio root element is missing"); + +createRoot(root).render( + + + + + +); diff --git a/apps/studio/src/navigation.test.ts b/apps/studio/src/navigation.test.ts new file mode 100644 index 0000000..71ca7b8 --- /dev/null +++ b/apps/studio/src/navigation.test.ts @@ -0,0 +1,20 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { navigate, pageFromHash } from "./navigation"; + +describe("workspace navigation", () => { + beforeEach(() => { + vi.stubGlobal("window", { location: { hash: "" } }); + }); + + it("falls back to concepts for an unknown route", () => { + window.location.hash = "#/unknown"; + expect(pageFromHash()).toBe("concepts"); + }); + + it("round-trips a declared workspace route", () => { + navigate("runtime"); + expect(window.location.hash).toBe("#/runtime"); + expect(pageFromHash()).toBe("runtime"); + }); +}); diff --git a/apps/studio/src/navigation.ts b/apps/studio/src/navigation.ts new file mode 100644 index 0000000..a10e03c --- /dev/null +++ b/apps/studio/src/navigation.ts @@ -0,0 +1,51 @@ +import { + Archive, + BookOpenText, + Boxes, + FileCheck2, + GitFork, + Import, + Library, + ListChecks, + TestTubeDiagonal, + type LucideIcon +} from "lucide-react"; + +export type PageId = + | "concepts" + | "candidates" + | "review" + | "sources" + | "relations" + | "exchange" + | "runtime" + | "releases"; + +export interface NavigationItem { + id: PageId; + label: string; + shortcut: string; + icon: LucideIcon; +} + +export const navigation: NavigationItem[] = [ + { id: "concepts", label: "Concepts", shortcut: "G C", icon: BookOpenText }, + { id: "candidates", label: "Candidates", shortcut: "G A", icon: ListChecks }, + { id: "review", label: "Review", shortcut: "G R", icon: FileCheck2 }, + { id: "sources", label: "Sources", shortcut: "G S", icon: Library }, + { id: "relations", label: "Relations", shortcut: "G L", icon: GitFork }, + { id: "exchange", label: "Exchange", shortcut: "G E", icon: Import }, + { id: "runtime", label: "Runtime", shortcut: "G T", icon: TestTubeDiagonal }, + { id: "releases", label: "Releases", shortcut: "G P", icon: Archive } +]; + +export const productIcon = Boxes; + +export function pageFromHash(): PageId { + const requested = window.location.hash.replace(/^#\/?/, "").split("/")[0]; + return navigation.some((item) => item.id === requested) ? (requested as PageId) : "concepts"; +} + +export function navigate(page: PageId): void { + window.location.hash = `#/${page}`; +} diff --git a/apps/studio/src/pages/CandidatesPage.tsx b/apps/studio/src/pages/CandidatesPage.tsx new file mode 100644 index 0000000..3f86a20 --- /dev/null +++ b/apps/studio/src/pages/CandidatesPage.tsx @@ -0,0 +1,98 @@ +import { useEffect, useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { ArrowRight, GitMerge, ListChecks, Split, XCircle } from "lucide-react"; + +import { api } from "../api/client"; +import { Badge, EmptyState, ErrorState, LoadingState, Section, StatusBadge } from "../components/ui"; + +export function CandidatesPage() { + const queryClient = useQueryClient(); + const [state, setState] = useState(""); + const [selectedId, setSelectedId] = useState(""); + const [targetId, setTargetId] = useState(""); + const [rationale, setRationale] = useState(""); + const candidates = useQuery({ + queryKey: ["candidates", state], + queryFn: () => api.candidates(state) + }); + const detail = useQuery({ + queryKey: ["candidate", selectedId], + queryFn: () => api.candidate(selectedId), + enabled: Boolean(selectedId) + }); + const runs = useQuery({ queryKey: ["forge-runs"], queryFn: api.forgeRuns }); + + useEffect(() => { + if (!selectedId && candidates.data?.[0]) setSelectedId(candidates.data[0].candidate_concept_id); + }, [candidates.data, selectedId]); + + const decide = useMutation({ + mutationFn: (decision: string) => + api.decideCandidate(selectedId, { + decision, + actor: "local-curator", + target_id: targetId, + rationale + }), + onSuccess: async () => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["candidates"] }), + queryClient.invalidateQueries({ queryKey: ["candidate", selectedId] }), + queryClient.invalidateQueries({ queryKey: ["changesets"] }), + queryClient.invalidateQueries({ queryKey: ["workspace"] }) + ]); + } + }); + + return ( +
+
+
Term reconciliation

Candidates

Machine output becomes governed concepts only after evidence-based decisions.

+
+ {["", "PROPOSED", "ATTESTED", "REJECTED"].map((item) => )} +
+
+
+
+
Candidate pool{candidates.data?.length ?? 0}
+ {candidates.isPending ? : candidates.isError ? : candidates.data?.length ? candidates.data.map((item) => ( + + )) : } +
+
+ {!selectedId ? : detail.isPending ? : detail.isError ? : detail.data ? <> +
{detail.data.candidate.candidate_concept_id}

{detail.data.candidate.meaning_hypothesis}

+
+
{detail.data.lexical_entries.map((item) =>
{item.language}{item.text}{item.suggested_status} · {item.suggested_policy}
)}
+
+
+ {detail.data.occurrences.length ? detail.data.occurrences.map((item) =>
{item.source_revision_id}{item.structure || item.location}

{item.text}

{item.language} · {item.excerpt_state}
) :

No redistributable excerpt is stored. Inspect source metadata before deciding.

} +
+
+ {detail.data.candidate.existing_matches.length ? detail.data.candidate.existing_matches.map((item) => ) :

No automatic reconciliation match.

} +
+ : null} +
+