diff --git a/.gitignore b/.gitignore index 6e43d55..e70d45b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,149 @@ -.pydevproject -target -.classpath -.project -.settings -.dbeaver-data-sources.xml -/bin/ +# ============================================================================= +# Java / Maven +# ============================================================================= +target/ +**/target/ +*.class +*.jar +*.war +*.ear +*.nar +hs_err_pid* +replay_pid* +.factorypath +.mvn/wrapper/maven-wrapper.jar + +# Local Spring config (use *.template as the committed baseline) +**/application.properties +!**/application.properties.template +**/properties/application.properties +!**/properties/application.properties.template + +# ============================================================================= +# Node.js / Next.js (frontend) +# ============================================================================= +node_modules/ +**/node_modules/ + +.next/ +**/.next/ +out/ +**/out/ +dist/ +**/dist/ +build/ +**/build/ +.turbo/ +.vercel/ + +# TypeScript / tooling caches +*.tsbuildinfo +.eslintcache +.stylelintcache +.parcel-cache +.cache/ + +# Package manager debug logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# OpenAPI codegen error dumps +openapi-ts-error-*.log + +# ============================================================================= +# Python (BrAPI test scripts) +# ============================================================================= +__pycache__/ +**/__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +.venv/ +venv/ +ENV/ +env/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +*.egg-info/ +.eggs/ +pip-log.txt +pip-delete-this-directory.txt + +# ============================================================================= +# Environment & secrets +# ============================================================================= +.env +.env.local +.env.*.local +.env.development.local +.env.test.local +.env.production.local +!.env.example +!.env.template +!.env.*.example + +*.pem +*.key +*.p12 +*.pfx +credentials.json +secrets.json +**/secrets/ + +# ============================================================================= +# IDE & editors +# ============================================================================= .idea/ -brapi-Java-TestServer.iml +*.iml +*.ipr +*.iws +.vscode/ +!.vscode/extensions.json +!.vscode/settings.json.example +*.swp +*.swo +*~ +.project +.classpath +.settings/ +.pydevproject + +# ============================================================================= +# Database tools +# ============================================================================= +.dbeaver/ +.dbeaver-data-sources.xml + +# ============================================================================= +# OS junk +# ============================================================================= +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +Thumbs.db +ehthumbs.db +Desktop.ini + +# ============================================================================= +# Logs & local runtime output +# ============================================================================= +*.log +logs/ +run*.log +run*.out.log +run*.err.log + +# ============================================================================= +# Local dev tools (bundled JDK/Maven, etc.) +# ============================================================================= .tools/ + +# Legacy / Eclipse output +/bin/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..dd858bf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +1.尽量不要动brapi自己写好的java代码,不要改路径,接口定义,返回值 +2.一般我只会让你加前端,或者加一些删除等简单的后端接口,千万不要破坏原接口的路径,入参定义,出参定义。 +3.下拉框的数据要缓存下来,同一选项源不要重复请求接口。 +4.前端首页(及同类入口页)加载首页数据时,副作用只触发一次查询,避免 Strict Mode 或重复 mount 导致多次请求。 +5.保存、提交类操作要做防抖(debounce),防止连续点击重复提交。 \ No newline at end of file diff --git a/docs/architecture/00-overall-data-architecture.md b/docs/architecture/00-overall-data-architecture.md new file mode 100644 index 0000000..6ab5f14 --- /dev/null +++ b/docs/architecture/00-overall-data-architecture.md @@ -0,0 +1,227 @@ +# BrAPI Test Server 总体数据架构图 + +本文档把 4 个模块串成一张总览图: + +```text +Core -> Germplasm/Seed -> Phenotyping -> Genotyping +``` + +对应的模块文档: + +| 模块 | 文档 | 核心作用 | +| --- | --- | --- | +| Core | `core-data-flow.md` | crop、program、trial、study、location、person 等基础上下文 | +| Germplasm/Seed | `04-germplasm-seed-data-flow.md` | germplasm、breeding_method、seed_lot、cross、pedigree、attribute | +| Phenotyping | `02-phenotyping-data-flow.md` | observation_unit、observation_variable、event、image、observation | +| Genotyping | `03-genotyping-data-flow.md` | sample、plate、reference、variantset、variant、callset、allele_call | + +## 总体结论 + +整个数据模型的主干是: + +```text +Core: crop -> program -> trial -> study +Germplasm: breeding_method -> germplasm -> cross / seed_lot / pedigree / attribute +Phenotyping: study + germplasm/seed_lot/cross -> observation_unit -> observation +Genotyping: observation_unit/study -> sample -> callset -> allele_call +Genotyping: reference_set -> variantset -> variant -> allele_call +``` + +`study` 是 Core 到 Phenotyping/Genotyping 的主桥;`germplasm` 是 Germplasm/Seed 到 Phenotyping/Genotyping 的主桥;`observation_unit` 是 Phenotyping 到 Genotyping 的主桥。 + +## 总架构图 + +```mermaid +flowchart TD + subgraph CORE["Core 基础上下文"] + CROP["crop
作物"] + PERSON["person
人员"] + PROGRAM["program
项目"] + LOCATION["location
地点"] + TRIAL["trial
试验批次"] + SEASON["season
季节"] + STUDY["study
研究/试验实施单元"] + LIST["list / list_item
通用列表"] + + CROP --> PROGRAM + PERSON --> PROGRAM + PROGRAM --> TRIAL + CROP --> TRIAL + PROGRAM --> LOCATION + CROP --> LOCATION + TRIAL --> STUDY + PROGRAM --> STUDY + CROP --> STUDY + LOCATION --> STUDY + SEASON --> STUDY + PERSON --> LIST + end + + subgraph GERM["Germplasm / Seed 种质与种子"] + BM["breeding_method
育种方法"] + GERMPLASM["germplasm
种质"] + GAD["germplasm_attribute_definition
属性定义"] + GAV["germplasm_attribute_value
属性值"] + CP["crossing_project
杂交项目"] + CROSS["cross_entity
Cross / PlannedCross"] + XP["cross_parent
杂交亲本"] + PEDNODE["pedigree_node
系谱节点"] + PEDEDGE["pedigree_edge
系谱边"] + SEEDLOT["seed_lot
种子批次"] + MIX["seed_lot_content_mixture
批次组成"] + TX["seed_lot_transaction
批次流转"] + + BM --> GERMPLASM + GAD --> GAV + GERMPLASM --> GAV + CP --> CROSS + CROSS --> XP + GERMPLASM --> XP + CROSS --> CROSS_PLANNED["cross_entity
planned cross 自关联"] + GERMPLASM --> PEDNODE + CP --> PEDNODE + PEDNODE --> PEDEDGE + PEDEDGE --> PEDNODE2["pedigree_node
父本/子代节点"] + GERMPLASM --> MIX + CROSS --> MIX + MIX --> SEEDLOT + SEEDLOT --> TX + TX --> SEEDLOT + end + + subgraph PHENO["Phenotyping 表型"] + ONTOLOGY["ontology
本体"] + TRAIT["trait
性状"] + METHOD["method
方法"] + SCALE["scale
标尺"] + OV["observation_variable
观测变量"] + OU["observation_unit
观测单元"] + EVENT["event
事件"] + IMAGE["image
图像"] + OBS["observation
观测值"] + + ONTOLOGY --> TRAIT + ONTOLOGY --> METHOD + ONTOLOGY --> SCALE + TRAIT --> OV + METHOD --> OV + SCALE --> OV + OU --> OBS + OV --> OBS + EVENT --> OU + OU --> IMAGE + IMAGE --> OBS + end + + subgraph GENO["Genotyping 基因型"] + PLATE["plate
样本板"] + SAMPLE["sample
样本"] + REFSET["reference_set
参考集"] + REF["reference
参考序列"] + REFB["reference_bases
参考片段"] + VARSET["variantset
变异集合"] + VARIANT["variant
变异位点"] + CALLSET["callset
样本调用集合"] + CALL["allele_call
基因型结果"] + GMAP["genome_map
遗传图谱"] + LG["linkageGroup
连锁群"] + MP["marker_position
图谱位置"] + + PLATE --> SAMPLE + SAMPLE --> CALLSET + CALLSET --> CALL + REFSET --> REF + REF --> REFB + REFSET --> VARSET + VARSET --> VARIANT + REFSET --> VARIANT + VARIANT --> CALL + GMAP --> LG + LG --> MP + VARIANT --> MP + end + + CROP --> GERMPLASM + CROP --> GAD + TRAIT --> GAD + METHOD --> GAD + SCALE --> GAD + ONTOLOGY --> GAD + PROGRAM --> CP + PROGRAM --> SEEDLOT + LOCATION --> SEEDLOT + + STUDY --> OU + TRIAL --> OU + PROGRAM --> OU + CROP --> OU + GERMPLASM --> OU + SEEDLOT --> OU + CROSS --> OU + + STUDY --> EVENT + STUDY --> OBS + TRIAL --> OBS + PROGRAM --> OBS + CROP --> OBS + + STUDY --> PLATE + TRIAL --> PLATE + PROGRAM --> PLATE + STUDY --> SAMPLE + TRIAL --> SAMPLE + PROGRAM --> SAMPLE + OU --> SAMPLE + + GERMPLASM --> REFSET + STUDY --> VARSET + CROP --> GMAP +``` + +## 跨模块关键桥接关系 + +| 桥接点 | 连接模块 | 说明 | +| --- | --- | --- | +| `crop` | Core -> Germplasm/Pheno/Geno | 作物维度贯穿 program、trial、study、germplasm、变量、图谱 | +| `program` | Core -> Germplasm/Seed/Pheno/Geno | 项目维度连接 crossing_project、seed_lot、observation_unit、sample、plate | +| `trial` | Core -> Pheno/Geno | 试验批次维度连接 study、observation_unit、observation、sample、plate | +| `study` | Core -> Pheno/Geno | 最重要的实验上下文,连接 observation_unit、event、observation、sample、plate、variantset | +| `germplasm` | Germplasm -> Pheno/Geno | 种质可连接 observation_unit、cross_parent、seed_lot_content_mixture、reference_set | +| `seed_lot` | Germplasm/Seed -> Pheno | SeedLot 可作为 observation_unit 的材料来源 | +| `cross_entity` | Germplasm/Seed -> Pheno | Cross/PlannedCross 可作为 observation_unit 或 seed_lot_content_mixture 的来源 | +| `observation_unit` | Pheno -> Geno | 表型观测单元可生成或关联 genotyping sample | +| `sample` | Geno 内部入口 | 从 observation_unit/study/trial/program 进入 callset 和 allele_call | +| `variant` | Geno 内部位点 | 与 allele_call、marker_position 连接,承载基因型结果定位 | + +## 推荐整体录入顺序 + +1. 录入 Core 基础上下文:`crop`、`person`、`program`、`location`、`trial`、`season`、`study`。 +2. 录入 Germplasm 上游:`breeding_method`、`germplasm_attribute_definition` 依赖的 `trait/method/scale/ontology`。 +3. 录入 `germplasm`,再补充 `germplasm_attribute_value`、donor、origin、institute、synonym、taxon 等扩展信息。 +4. 如果涉及杂交,录入 `crossing_project`、`cross_entity`、`cross_parent`;计划杂交使用 `cross_entity.planned` 和 `planned_cross_id` 自关联表达。 +5. 录入 Seed 数据:`seed_lot`、`seed_lot_content_mixture`、`seed_lot_transaction`。 +6. 录入 Phenotyping 定义:`ontology`、`trait`、`method`、`scale`、`observation_variable`。 +7. 录入 Phenotyping 实体与事实:`observation_unit`、`event`、`image`、`observation`。 +8. 录入 Genotyping 样本入口:`plate`、`sample`。 +9. 录入 Genotyping 参考和变异:`reference_set`、`reference`、`reference_bases`、`variantset`、`variant`。 +10. 录入 Genotyping 结果:`callset`、`callset_variant_sets`、`allele_call`。 +11. 如需遗传图谱定位,录入 `genome_map`、`linkageGroup`、`marker_position`。 + +## 模块边界速记 + +| 模块 | 根节点 | 主要事实表 | 向外输出 | +| --- | --- | --- | --- | +| Core | `crop/program/trial/study` | `study` | 给所有业务模块提供上下文 | +| Germplasm/Seed | `germplasm` | `germplasm_attribute_value`, `seed_lot_content_mixture`, `seed_lot_transaction`, `cross_parent`, `pedigree_edge` | 给 Pheno 提供材料来源,给 Geno 提供 reference source | +| Phenotyping | `observation_unit` | `observation` | 给 Geno 提供 sample 的观测对象来源 | +| Genotyping | `sample`, `variant` | `allele_call` | 输出样本在位点上的 genotype 结果 | + +## 关键注意点 + +1. `study` 是大多数实验数据的上下文入口;如果数据要进入 Pheno 或 Geno,通常都应该能追溯到 `study`。 +2. `germplasm` 描述种质主数据,`seed_lot` 描述库存批次;二者通过 `seed_lot_content_mixture` 间接关联。 +3. `plannedcross` 没有独立数据库表,落库在 `cross_entity`,通过 `planned` 和 `planned_cross_id` 表达。 +4. `observation_unit` 可以关联 `germplasm`、`seed_lot`、`cross_entity`,是材料进入表型观测的入口。 +5. `sample` 可以从 `observation_unit` 来,也冗余关联 `study/trial/program`,是基因型流程入口。 +6. `allele_call` 是最终 genotype 结果表,连接 `callset` 与 `variant`。 +7. `additional_info` 和 `external_references` 是跨模块通用扩展表,主图中未展开,以免遮挡主干关系。 diff --git a/docs/architecture/02-phenotyping-data-flow.md b/docs/architecture/02-phenotyping-data-flow.md new file mode 100644 index 0000000..b0121c3 --- /dev/null +++ b/docs/architecture/02-phenotyping-data-flow.md @@ -0,0 +1,290 @@ +# Phenotyping 模块数据流与表关系 + +本文档分析 Phenotyping 模块的数据录入顺序、核心表关系,以及它与 Core 模块 `study/trial/program/crop` 的衔接方式。 + +## 结论 + +Phenotyping 模块的数据主线是: + +```text +core study -> ontology -> trait / method / scale -> observation_variable -> observation_unit -> event / image -> observation +``` + +更贴近业务录入的顺序可以理解为: + +```text +1. 先有 Core 数据:crop、program、trial、study +2. 录入 Ontology / Trait / Method / Scale +3. 组装 ObservationVariable +4. 录入 ObservationUnit +5. 录入 Event 和 Image +6. 录入 Observation +``` + +初始化脚本中与 Phenotyping 相关的执行顺序是: + +```text +R__init_data_14_observation_units.sql +R__init_data_17_events.sql +R__init_data_18_images.sql +R__init_data_19_observation_variables.sql +R__init_data_20_observations.sql +R__init_data_26_observation_variables2.sql +R__init_data_26_observation_variables3.sql +``` + +注意:初始化脚本为了构造演示数据,`observation_unit` 早于 `observation_variable` 插入;从业务建模角度看,二者都依赖已存在的 `study`,而真正的观测值 `observation` 需要同时引用 `observation_unit` 和 `observation_variable`。 + +## 核心表说明 + +| 表 | 作用 | 主要上游依赖 | 主要下游 | +| --- | --- | --- | --- | +| `ontology` | 本体信息,定义术语来源 | 无 | `trait`、`method`、`scale`、`observation_variable` | +| `ontology_ref` | 本体引用项 | 可独立录入 | `trait_ontology_reference`、`method_ontology_reference`、`scale_ontology_reference` | +| `trait` | 性状定义,描述“测什么” | 可选 `ontology` | `observation_variable` | +| `method` | 测量方法,描述“怎么测” | 可选 `ontology` | `observation_variable` | +| `scale` | 标尺/单位/数据类型,描述“用什么尺度表达” | 可选 `ontology` | `observation_variable`、`scale_valid_value_category` | +| `observation_variable` | 观测变量,由 trait/method/scale 组成 | `crop`、`trait`、`method`、`scale`、`ontology` | `observation`、`study_variable` | +| `observation_unit` | 观测对象,如 plot/plant/block | `crop`、`program`、`trial`、`study`,可选 germplasm/seed_lot/cross | `observation`、`event_observation_units`、`image` | +| `event` | 田间事件,如施肥、灌溉、采样等 | `study` | `event_param`、`event_observation_units` | +| `event_param` | 事件参数 | `event` | `event_parameter_entity_values_by_date` | +| `image` | 图片/影像数据 | 可选 `observation_unit`、`geojson` | `image_observations` | +| `observation` | 实际观测值 | `observation_unit`、`observation_variable`、`study`、可选 `season` | `image_observations` | + +## 建议录入顺序 + +### 1. 准备 Core 上游数据 + +Phenotyping 数据通常挂在 Core 的层级下面: + +```text +crop -> program -> trial -> study +``` + +其中 `study` 是 Phenotyping 的入口节点。`observation_unit`、`event`、`observation` 都会直接或间接关联到 `study`。 + +### 2. 录入 Ontology + +先录入 `ontology` 和需要的 `ontology_ref`。 + +`ontology` 用来标识术语体系来源,后续 `trait`、`method`、`scale`、`observation_variable` 都可以挂载本体信息。 + +### 3. 录入 Trait / Method / Scale + +这三类数据共同描述一个观测指标: + +```text +Trait = 测什么,例如 plant height +Method = 怎么测,例如 ruler measurement +Scale = 用什么单位/数据类型表达,例如 cm、numeric +``` + +`scale` 如果有枚举或分类值,还会录入: + +```text +scale_valid_value_category +``` + +### 4. 组装 ObservationVariable + +录入 `observation_variable`,它会引用: + +```text +crop +trait +method +scale +ontology +``` + +业务上它相当于“可被采集的一项指标”。例如“株高-尺测法-cm”。 + +`study_variable` 是 `study` 和 `observation_variable` 的多对多关系,表示某个 study 会采集哪些变量。 + +### 5. 录入 ObservationUnit + +录入 `observation_unit`,它表示被观测对象,例如 field、block、plot、plant。 + +它通常会引用: + +```text +crop +program +trial +study +``` + +并且可选关联: + +```text +germplasm +seed_lot +cross +observation_unit_position +observation_unit_treatment +observation_unit_level +``` + +### 6. 录入 Event + +录入 `event`,用于表达发生在 study 或 observation unit 上的事件。 + +常见关系: + +```text +event -> study +event_observation_units -> observation_unit +event_param -> event +``` + +### 7. 录入 Image + +录入 `image`,图片可以直接关联 `observation_unit`,也可以通过 `image_observations` 关联一个或多个 `observation`。 + +图片坐标使用 `geojson/coordinate` 扩展。 + +### 8. 录入 Observation + +最后录入 `observation`,这是 Phenotyping 模块的核心事实数据。 + +一条 observation 通常同时引用: + +```text +observation_unit +observation_variable +study +trial +program +crop +season +``` + +代码里 `ObservationEntity.setObservationUnit(...)` 会从 observation unit 继承 study/trial/program/crop,因此 observation 的上下文可以由 observation unit 自动带出。 + +## Phenotyping 数据流图 + +```mermaid +flowchart TD + C["Core: crop"] --> P["Core: program"] + P --> T["Core: trial"] + T --> S["Core: study"] + + O["ontology 本体"] --> TR["trait 性状"] + O --> M["method 方法"] + O --> SC["scale 标尺"] + OR["ontology_ref 本体引用"] --> TR + OR --> M + OR --> SC + + C --> OV["observation_variable 观测变量"] + TR --> OV + M --> OV + SC --> OV + O --> OV + + S --> SV["study_variable 研究-变量"] + OV --> SV + + C --> OU["observation_unit 观测单元"] + P --> OU + T --> OU + S --> OU + G["Germplasm/SeedLot/Cross 可选"] --> OU + OU --> OUP["position / treatment / level"] + + S --> E["event 事件"] + E --> EP["event_param 事件参数"] + E --> EOU["event_observation_units"] + OU --> EOU + + OU --> IMG["image 图像"] + GEO["geojson / coordinate"] --> IMG + + OU --> OB["observation 观测值"] + OV --> OB + S --> OB + T --> OB + P --> OB + C --> OB + SE["Core: season"] --> OB + + IMG --> IO["image_observations"] + OB --> IO +``` + +## Phenotyping ER 关系图 + +```mermaid +erDiagram + crop ||--o{ observation_variable : "crop_id" + crop ||--o{ observation_unit : "crop_id" + crop ||--o{ observation : "crop_id" + + program ||--o{ observation_unit : "program_id" + program ||--o{ observation : "program_id" + + trial ||--o{ observation_unit : "trial_id" + trial ||--o{ observation : "trial_id" + + study ||--o{ observation_unit : "study_id" + study ||--o{ event : "study_id" + study ||--o{ observation : "study_id" + study ||--o{ study_variable : "study_db_id" + + ontology ||--o{ trait : "ontology_id" + ontology ||--o{ method : "ontology_id" + ontology ||--o{ scale : "ontology_id" + ontology ||--o{ observation_variable : "ontology_id" + + ontology_ref ||--o{ trait_ontology_reference : "ontology_reference_id" + ontology_ref ||--o{ method_ontology_reference : "ontology_reference_id" + ontology_ref ||--o{ scale_ontology_reference : "ontology_reference_id" + + trait ||--o{ observation_variable : "trait_id" + method ||--o{ observation_variable : "method_id" + scale ||--o{ observation_variable : "scale_id" + scale ||--o{ scale_valid_value_category : "scale_id" + + observation_variable ||--o{ observation : "observation_variable_id" + observation_variable ||--o{ study_variable : "variable_db_id" + + observation_unit ||--o{ observation : "observation_unit_id" + observation_unit ||--o{ observation_unit_position : "observation_unit_id" + observation_unit ||--o{ observation_unit_treatment : "observation_unit_id" + observation_unit ||--o{ observation_unit_level : "observation_unit_id" + + event ||--o{ event_param : "event_id" + event ||--o{ event_observation_units : "event_entity_id" + observation_unit ||--o{ event_observation_units : "observation_units_id" + + image ||--o{ image_observations : "image_entity_id" + observation ||--o{ image_observations : "observations_id" + observation_unit ||--o{ image : "observation_unit_id" + + season ||--o{ observation : "season_id" +``` + +## API 与表的对应关系 + +| API | 主表 | 说明 | +| --- | --- | --- | +| `/brapi/v2/ontologies` | `ontology` | 本体查询、新增 | +| `/brapi/v2/traits` | `trait` | 性状定义 | +| `/brapi/v2/methods` | `method` | 测量方法 | +| `/brapi/v2/scales` | `scale` | 标尺、单位、数据类型 | +| `/brapi/v2/variables` | `observation_variable` | 观测变量,由 trait/method/scale 组合 | +| `/brapi/v2/observationunits` | `observation_unit` | 观测单元 | +| `/brapi/v2/events` | `event` | 田间/实验事件 | +| `/brapi/v2/images` | `image` | 图像数据 | +| `/brapi/v2/observations` | `observation` | 实际观测值 | + +## 关键注意点 + +1. `study` 是 Phenotyping 与 Core 的连接点,大多数表型数据都应挂到具体 study。 +2. `observation_variable` 不是单独的数值,它是“性状 + 方法 + 标尺 + 本体”的指标定义。 +3. `observation_unit` 是被观测对象,`observation` 是对这个对象在某个变量上的一次测量结果。 +4. `event` 可以绑定多个 `observation_unit`,适合记录施肥、灌溉、采样等动作。 +5. `image` 可以直接绑定 `observation_unit`,也可以通过 `image_observations` 与观测值关联。 +6. `trait/method/scale/observation_variable` 都有 `*_additional_info` 和 `*_external_references` 扩展表,用于补充业务字段和外部引用。 +7. `observation` 冗余保存了 `crop/program/trial/study` 上下文,代码中会从 `observation_unit` 或 `study` 向上继承这些上下文,方便查询。 + diff --git a/docs/architecture/03-genotyping-data-flow.md b/docs/architecture/03-genotyping-data-flow.md new file mode 100644 index 0000000..b75a60f --- /dev/null +++ b/docs/architecture/03-genotyping-data-flow.md @@ -0,0 +1,313 @@ +# Genotyping 模块数据流与表关系 + +本文档分析 Genotyping 模块的数据录入顺序、核心表关系,以及 Java 实体名与真实数据库表名之间的对应关系。 + +## 结论 + +Genotyping 模块的数据主线是: + +```text +Core/Pheno 上游数据 -> sample / plate +ReferenceSet -> Reference -> ReferenceBases +ReferenceSet + Study -> VariantSet -> Variant +Sample -> CallSet +CallSet + Variant -> Call +GenomeMap -> LinkageGroup -> MarkerPosition -> Variant +``` + +更贴近业务录入的顺序是: + +```text +1. 先有 Core/Phenotyping 上游:crop、program、trial、study、observation_unit +2. 录入 Plate 和 Sample +3. 录入 ReferenceSet、Reference、ReferenceBases +4. 录入 VariantSet +5. 录入 Variant +6. 录入 CallSet +7. 录入 Call,也就是 allele_call 表里的基因型结果 +8. 录入 GenomeMap、LinkageGroup、MarkerPosition +``` + +初始化脚本中与 Genotyping 相关的执行顺序是: + +```text +R__init_data_21_samples.sql +R__init_data_22_references.sql +R__init_data_23_variant_set_1.sql +R__init_data_24_genome_maps.sql +src/main/resources/db/sql/variant_set_4/variant_set_4.sql +src/main/resources/db/sql/variant_set_4/variant_set_4_alleles.sql +``` + +## 实体与真实表名 + +| 业务概念 | Java 实体 | 数据库表 | 说明 | +| --- | --- | --- | --- | +| Call | `CallEntity` | `allele_call` | 单个样本在某个 variant 上的 genotype 结果 | +| CallSet | `CallSetEntity` | `callset` | 某个 sample 的一组 call,通常对应一个样本的基因型调用集合 | +| Sample | `SampleEntity` | `sample` | 送检样本/测序样本 | +| Plate | `PlateEntity` | `plate` | 样本板,包含多个 sample | +| MarkerPosition | `MarkerPositionEntity` | `marker_position` | variant 在 linkage group 上的位置 | +| Variant | `VariantEntity` | `variant` | 变异位点,如 SNP/Indel | +| ReferenceSet | `ReferenceSetEntity` | `reference_set` | 参考基因组集合 | +| GenomeMap | `GenomeMapEntity` | `genome_map` | 遗传图谱 | +| VariantSet | `VariantSetEntity` | `variantset` | 一批 variant 的集合 | +| Reference | `ReferenceEntity` | `reference` | 参考序列,如 chromosome/contig | +| ReferenceBases | `ReferenceBasesPageEntity` | `reference_bases` | reference 的序列分页 | +| LinkageGroup | `LinkageGroupEntity` | `linkageGroup` | 图谱中的连锁群;注意表名是驼峰 `linkageGroup` | + +## 核心表说明 + +| 表 | 作用 | 主要上游依赖 | 主要下游 | +| --- | --- | --- | --- | +| `plate` | 样本板 | `program`、`trial`、`study`,可选 vendor submission | `sample` | +| `sample` | 样本 | `plate`、`observation_unit`、`program`、`trial`、`study` | `callset` | +| `reference_set` | 参考基因组集合 | 可选 `germplasm` | `reference`、`variantset`、`variant` | +| `reference` | 参考序列 | `reference_set` | `reference_bases` | +| `reference_bases` | 参考序列片段/分页 | `reference` | 无 | +| `variantset` | 变异集合 | `reference_set`、`study` | `variant`、`callset_variant_sets`、`variantset_analysis`、`variantset_format` | +| `variant` | 变异位点 | `reference_set`、`variantset` | `allele_call`、`marker_position` | +| `callset` | 样本的 call 集合 | `sample` | `allele_call`、`callset_variant_sets` | +| `allele_call` | genotype 调用结果 | `callset`、`variant` | 无 | +| `genome_map` | 遗传图谱 | `crop`,可关联 `study` | `linkageGroup` | +| `linkageGroup` | 连锁群 | `genome_map` | `marker_position` | +| `marker_position` | marker/variant 在图谱上的位置 | `linkageGroup`、`variant` | 无 | + +## 建议录入顺序 + +### 1. 准备 Core/Phenotyping 上游数据 + +Genotyping 数据通常挂在 Core 和 Phenotyping 之上。 + +必须或常见上游包括: + +```text +crop +program +trial +study +observation_unit +``` + +`sample` 可以关联 `observation_unit`,也会冗余关联 `program/trial/study`,用于查询和筛选。 + +### 2. 录入 Plate + +先录入 `plate`,表示样本板。 + +`plate` 可关联: + +```text +program +trial +study +plate_submission +``` + +如果样本不走板,也可以直接录入 `sample`;但当前模型中 sample 支持挂到 plate 上。 + +### 3. 录入 Sample + +录入 `sample`,它是 genotyping 流程的样本入口。 + +主要关系: + +```text +sample -> plate +sample -> observation_unit +sample -> program / trial / study +sample -> germplasm_taxon +``` + +### 4. 录入 ReferenceSet 和 Reference + +录入 `reference_set`,表示参考基因组集合。 + +然后录入 `reference`,表示具体参考序列,例如 chromosome、contig。 + +如需保存具体序列片段,再录入: + +```text +reference_bases +``` + +### 5. 录入 VariantSet + +录入 `variantset`,它把一批 variant 组织成集合。 + +主要关系: + +```text +variantset -> reference_set +variantset -> study +``` + +附属表包括: + +```text +variantset_analysis +variantset_format +variantset_additional_info +variantset_external_references +``` + +### 6. 录入 Variant + +录入 `variant`,表示具体变异位点。 + +主要关系: + +```text +variant -> reference_set +variant -> variantset +``` + +附属表包括: + +```text +variant_entity_alternate_bases +variant_entity_ciend +variant_entity_cipos +variant_entity_filters_failed +``` + +### 7. 录入 CallSet + +录入 `callset`,表示某个样本的一组 genotype calls。 + +主要关系: + +```text +callset -> sample +callset_variant_sets -> variantset +``` + +`callset_variant_sets` 是 `callset` 和 `variantset` 的多对多关系表。 + +### 8. 录入 Call + +录入 `allele_call`,业务上就是 Call。 + +它是最终基因型调用结果,核心关系是: + +```text +allele_call -> callset +allele_call -> variant +``` + +也就是说,一条 call 表示“某个 sample/callset 在某个 variant 上的 genotype、read depth、likelihood 等结果”。 + +### 9. 录入 GenomeMap 和 MarkerPosition + +如果需要遗传图谱定位,录入: + +```text +genome_map -> linkageGroup -> marker_position -> variant +``` + +`marker_position` 实际上把 variant 放到某个 linkage group 的具体位置上。 + +## Genotyping 数据流图 + +```mermaid +flowchart TD + C["Core: crop"] --> GM["genome_map 遗传图谱"] + C --> P["Core: program"] + P --> T["Core: trial"] + T --> ST["Core: study"] + ST --> PL["plate 样本板"] + ST --> VS["variantset 变异集合"] + ST --> SM["sample 样本"] + + OU["Pheno: observation_unit"] --> SM + PL --> SM + + GER["Germplasm 可选"] --> RS["reference_set 参考集合"] + RS --> R["reference 参考序列"] + R --> RB["reference_bases 参考序列分页"] + + RS --> VS + VS --> V["variant 变异位点"] + RS --> V + + SM --> CS["callset 样本调用集合"] + CS --> CSV["callset_variant_sets"] + VS --> CSV + + CS --> CALL["allele_call / Call 基因型结果"] + V --> CALL + + GM --> LG["linkageGroup 连锁群"] + LG --> MP["marker_position 图谱位置"] + V --> MP + + VS --> VSA["variantset_analysis"] + VS --> VSF["variantset_format"] +``` + +## Genotyping ER 关系图 + +```mermaid +erDiagram + program ||--o{ plate : "program_id" + trial ||--o{ plate : "trial_id" + study ||--o{ plate : "study_id" + + plate ||--o{ sample : "plate_id" + observation_unit ||--o{ sample : "observation_unit_id" + program ||--o{ sample : "program_id" + trial ||--o{ sample : "trial_id" + study ||--o{ sample : "study_id" + + germplasm ||--o{ reference_set : "source_germplasm_id" + reference_set ||--o{ reference : "reference_set_id" + reference ||--o{ reference_bases : "reference_id" + + reference_set ||--o{ variantset : "reference_set_id" + study ||--o{ variantset : "study_id" + variantset ||--o{ variant : "variant_set_id" + reference_set ||--o{ variant : "reference_set_id" + + sample ||--o{ callset : "sample_id" + callset ||--o{ callset_variant_sets : "call_sets_id" + variantset ||--o{ callset_variant_sets : "variant_sets_id" + + callset ||--o{ allele_call : "call_set_id" + variant ||--o{ allele_call : "variant_id" + + crop ||--o{ genome_map : "crop_id" + genome_map ||--o{ linkageGroup : "genome_map_id" + linkageGroup ||--o{ marker_position : "linkage_group_id" + variant ||--o{ marker_position : "variant_id" + + variantset ||--o{ variantset_analysis : "variant_set_id" + variantset ||--o{ variantset_format : "variant_set_id" +``` + +## API 与表的对应关系 + +| API | 主表 | 说明 | +| --- | --- | --- | +| `/brapi/v2/samples` | `sample` | 样本查询、新增、修改 | +| `/brapi/v2/plates` | `plate` | 样本板查询、新增、修改 | +| `/brapi/v2/callsets` | `callset` | 样本调用集合 | +| `/brapi/v2/calls` | `allele_call` | genotype 调用结果 | +| `/brapi/v2/variants` | `variant` | 变异位点 | +| `/brapi/v2/variantsets` | `variantset` | 变异集合 | +| `/brapi/v2/referencesets` | `reference_set` | 参考基因组集合 | +| `/brapi/v2/references` | `reference` | 参考序列 | +| `/brapi/v2/maps` | `genome_map` | 遗传图谱 | +| `/brapi/v2/markerpositions` | `marker_position` | variant/marker 在图谱上的位置 | + +## 关键注意点 + +1. `CallEntity` 对应的数据库表不是 `call`,而是 `allele_call`。 +2. `CallSetEntity` 对应 `callset`,不是 `call_set`。 +3. `VariantSetEntity` 对应 `variantset`,不是 `variant_set`。 +4. `LinkageGroupEntity` 对应表名是 `linkageGroup`,schema 里另有外键引用时大小写需要特别注意。 +5. `sample` 是基因型流程的样本入口,向上关联 `plate/observation_unit/study/trial/program`。 +6. `variant` 是位点定义,`allele_call` 是样本在位点上的结果;不要把二者混成同一层数据。 +7. `reference_set/reference/reference_bases` 是参考基因组侧;`variantset/variant/callset/allele_call` 是变异和结果侧。 +8. `genome_map/linkageGroup/marker_position` 是遗传图谱定位侧,`marker_position` 通过 `variant_id` 与变异位点相连。 +9. 与前两篇一样,`*_additional_info` 和 `*_external_references` 是通用扩展关系,用于补充业务字段和外部引用。 + diff --git a/docs/architecture/04-germplasm-seed-data-flow.md b/docs/architecture/04-germplasm-seed-data-flow.md new file mode 100644 index 0000000..4d164e2 --- /dev/null +++ b/docs/architecture/04-germplasm-seed-data-flow.md @@ -0,0 +1,142 @@ +# Germplasm 与 Seed 数据流及表关系 + +本文档整理 Germplasm 模块中以 `germplasm` 为核心,并覆盖 SeedLot、CrossingProject、GermplasmAttribute、Cross、Pedigree 等相关表的关系。重点表包括: + +```text +germplasm +seed_lot +seed_lot_content_mixture +crossing_project +cross_entity / planned cross / cross_parent +germplasm_attribute_definition +germplasm_attribute_value +pedigree_node / pedigree_edge +breeding_method +``` + +## 结论 + +Germplasm 关系主线可以理解为: + +```text +breeding_method -> germplasm -> germplasm_attribute_value -> germplasm_attribute_definition +program -> crossing_project -> cross_entity -> cross_parent -> germplasm +germplasm / cross_entity -> seed_lot_content_mixture -> seed_lot +germplasm -> pedigree_node -> pedigree_edge +``` + +其中 `plannedcross` 在数据库中不是独立表,而是 `cross_entity` 的自关联:`cross_entity.planned_cross_id -> cross_entity.id`,并通过 `planned` 字段区分计划杂交和实际杂交。 + +## 图 4 Germplasm 关系架构图 + +```mermaid +flowchart TD + BM["breeding_method
育种方法"] -->|"breeding_method_id"| G["germplasm
种质主表"] + CROP["crop
作物"] -->|"crop_id"| G + + GAD["germplasm_attribute_definition
GermplasmAttribute 定义"] -->|"attribute_id"| GAV["germplasm_attribute_value
GermplasmAttributeValue"] + G -->|"germplasm_id"| GAV + CROP -->|"crop_id"| GAD + TR["trait"] -->|"trait_id"| GAD + ME["method"] -->|"method_id"| GAD + SC["scale"] -->|"scale_id"| GAD + ONT["ontology"] -->|"ontology_id"| GAD + + PR["program
项目"] -->|"program_id"| CP["crossing_project
CrossingProject"] + CP -->|"crossing_project_id"| CR["cross_entity
Cross / PlannedCross"] + CP -->|"crossing_project_id"| XP["cross_parent
CrossParent"] + + CR -->|"cross_id"| XP + G -->|"germplasm_id"| XP + OU["observation_unit
可选亲本来源"] -->|"observation_unit_id"| XP + CR -->|"planned_cross_id 自关联"| PCR["cross_entity
planned cross"] + + G -->|"germplasm_id"| SCM["seed_lot_content_mixture
SeedLot 组成"] + CR -->|"cross_id"| SCM + SCM -->|"seed_lot_id"| SL["seed_lot
SeedLot"] + PR -->|"program_id"| SL + LOC["location
库位/地点"] -->|"location_id"| SL + + SL -->|"from_seed_lot_id"| TX["seed_lot_transaction
SeedLot 流转"] + TX -->|"to_seed_lot_id"| SL + + CP -->|"crossing_project_id"| PN["pedigree_node
PedigreeNode"] + G -->|"germplasm_id"| PN + PN -->|"this_node_id"| PE["pedigree_edge
亲子/同胞关系"] + PE -->|"connceted_node_id"| PN2["pedigree_node
父本/子代节点"] +``` + +## 图 4-2 Germplasm ER 关系图 + +```mermaid +erDiagram + crop ||--o{ germplasm : "crop_id" + breeding_method ||--o{ germplasm : "breeding_method_id" + + germplasm ||--o{ germplasm_attribute_value : "germplasm_id" + germplasm ||--o{ germplasm_donor : "germplasm_id" + germplasm ||--o{ germplasm_institute : "germplasm_id" + germplasm ||--o{ germplasm_origin : "germplasm_id" + germplasm ||--o{ germplasm_synonym : "germplasm_id" + germplasm ||--o{ germplasm_taxon : "germplasm_id" + + germplasm ||--o| pedigree_node : "germplasm_id" + pedigree_node ||--o{ pedigree_edge : "this_node_id" + pedigree_node ||--o{ pedigree_edge : "connceted_node_id" + + crossing_project ||--o{ cross_entity : "crossing_project_id" + cross_entity ||--o{ cross_entity : "planned_cross_id" + cross_entity ||--o{ cross_parent : "cross_id" + crossing_project ||--o{ cross_parent : "crossing_project_id" + germplasm ||--o{ cross_parent : "germplasm_id" + + seed_lot ||--o{ seed_lot_content_mixture : "seed_lot_id" + germplasm ||--o{ seed_lot_content_mixture : "germplasm_id" + cross_entity ||--o{ seed_lot_content_mixture : "cross_id" + + location ||--o{ seed_lot : "location_id" + program ||--o{ seed_lot : "program_id" + + seed_lot ||--o{ seed_lot_transaction : "from_seed_lot_id" + seed_lot ||--o{ seed_lot_transaction : "to_seed_lot_id" +``` + +## 核心表说明 + +| 表 | 作用 | 主要上游依赖 | 主要下游 | +| --- | --- | --- | --- | +| `germplasm` | 种质主表,保存 accession、PUI、物种、采集来源、种子来源等信息 | `crop`, `breeding_method` | 属性、机构、来源、系谱、SeedLot 组成、Cross 亲本 | +| `breeding_method` | 育种方法字典 | 无 | `germplasm` | +| `germplasm_attribute_definition` | GermplasmAttribute 定义,继承变量定义体系,可关联 trait/method/scale/ontology/crop | `crop`, `trait`, `method`, `scale`, `ontology` | `germplasm_attribute_value` | +| `germplasm_attribute_value` | GermplasmAttributeValue,保存某个 germplasm 在某个属性上的取值 | `germplasm`, `germplasm_attribute_definition` | 属性查询 | +| `crossing_project` | CrossingProject,杂交项目 | `program` | `cross_entity`, `cross_parent`, `pedigree_node` | +| `cross_entity` | Cross/PlannedCross 统一落库表;`planned_cross_id` 是对本表的自关联 | `crossing_project`, `cross_entity` | `cross_parent`, `seed_lot_content_mixture` | +| `cross_parent` | CrossParent,连接 `cross_entity` 与 `germplasm` 或 observation unit | `cross_entity`, `crossing_project`, `germplasm`, `observation_unit` | 杂交亲本 | +| `seed_lot` | 种子批次/库存批次,保存数量、单位、库位、项目、创建和更新时间 | `location`, `program` | `seed_lot_content_mixture`, `seed_lot_transaction` | +| `seed_lot_content_mixture` | SeedLot 组成明细,连接 `seed_lot` 与 `germplasm` 或 `cross_entity` | `seed_lot`, `germplasm`, `cross_entity` | 表示批次内各成分占比 | +| `seed_lot_transaction` | SeedLot 流转记录,记录从一个批次到另一个批次的数量变化 | `from_seed_lot`, `to_seed_lot` | 库存流向追踪 | +| `pedigree_node` | 系谱节点,一个节点可关联一个 germplasm | `germplasm`, `crossing_project` | `pedigree_edge` | +| `pedigree_edge` | 系谱边,描述 parent/child/sibling 关系 | `pedigree_node` | 系谱查询 | + +## 建议录入顺序 + +1. 先录入上游基础数据:`crop`、`breeding_method`、`program`、`location`,以及属性定义需要的 `trait/method/scale/ontology`。 +2. 录入 `germplasm_attribute_definition`,定义可采集的 GermplasmAttribute。 +3. 录入 `germplasm` 主数据,并通过 `breeding_method_id` 关联育种方法。 +4. 录入 `germplasm_attribute_value`,把 germplasm 与 attribute definition 连接起来并保存具体值。 +5. 如果涉及杂交,录入 `crossing_project`,再录入计划杂交/实际杂交到 `cross_entity`;计划杂交通过 `planned=true` 或 `planned_cross_id` 自关联体现。 +6. 录入 `cross_parent`,把 cross 与 parent germplasm 或 observation unit 关联起来。 +7. 录入 `pedigree_node` 和 `pedigree_edge`,表达 germplasm 的 parent/child/sibling 系谱关系。 +8. 录入 `seed_lot`,保存批次数量、单位、库位和项目归属。 +9. 录入 `seed_lot_content_mixture`,把 seed lot 与一个或多个 `germplasm`/`cross_entity` 连接起来。 +10. 后续出入库、分装、合并或转移时,录入 `seed_lot_transaction`,通过 `from_seed_lot_id` 与 `to_seed_lot_id` 追踪流向。 + +## 关键注意点 + +1. `germplasm.seedSource` 和 `germplasm.seedSourceDescription` 是种质主表上的描述字段,不等同于库存批次。 +2. 真正表示库存批次的是 `seed_lot`,而批次与种质的关系在 `seed_lot_content_mixture` 中。 +3. `seed_lot_content_mixture` 可以关联 `germplasm`,也可以关联 `cross_entity`,适合表达混合种子批次或由杂交产生的批次。 +4. `seed_lot_transaction` 同时有 `fromSeedLot` 与 `toSeedLot`,因此它表达的是 seed lot 到 seed lot 的流转关系,而不是 seed lot 到 germplasm 的直接关系。 +5. `plannedcross` 没有独立数据库表,统一使用 `cross_entity`,通过 `planned` 字段和 `planned_cross_id` 自关联表达。 +6. `germplasm_attribute_definition` 是属性定义,`germplasm_attribute_value` 是种质上的实际属性值,两者通过 `attribute_id` 连接。 +7. 系谱关系由 `pedigree_node` 和 `pedigree_edge` 表达;杂交流程由 `cross_entity` 和 `cross_parent` 表达,两者都可以回到 `germplasm` 主数据。 diff --git a/docs/architecture/core-data-flow.md b/docs/architecture/core-data-flow.md new file mode 100644 index 0000000..01a662f --- /dev/null +++ b/docs/architecture/core-data-flow.md @@ -0,0 +1,210 @@ +# Core 模块数据流与表关系 + +本文档分析 `brapi-java` 项目 core 模块的数据录入顺序、主表关系,以及初始化脚本中的实际数据流。 + +## 结论 + +Core 模块的数据主线是: + +```text +crop -> person -> program -> location -> trial -> season -> study -> study 附属信息 +``` + +`list` 是相对独立的列表能力,可以较早录入;如果需要绑定列表所有人,则依赖 `person`。 + +初始化脚本实际执行顺序是: + +```text +R__init_data_01_crops.sql +R__init_data_02_lists.sql +R__init_data_03_locations.sql +R__init_data_04_people.sql +R__init_data_05_programs.sql +R__init_data_06_trials.sql +R__init_data_07_seasons.sql +R__init_data_08_studies.sql +``` + +其中 `R__init_data_05_programs.sql` 会插入 program 负责人到 `person` 表,并回填部分 `location.program_id/crop_id`。 + +## 核心表说明 + +| 表 | 作用 | 主要上游依赖 | 主要下游 | +| --- | --- | --- | --- | +| `crop` | 作物字典,Core 主根数据之一 | 无 | `program`、`location`、`trial`、`study` | +| `person` | 人员、联系人、负责人 | 无 | `program.lead_person_id`、`trial_contact`、`study_contact`、`list.list_owner_person_id` | +| `program` | 育种项目/业务项目 | `crop`、可选 `person` | `trial`、`study`、`location` | +| `location` | 地点/试验点 | 可选 `crop`、`program`、父级 `location`、`geojson` | `study` | +| `trial` | 试验批次/试验项目 | `crop`、`program` | `study`、`trial_contact`、`trial_publication`、`trial_dataset_authorship` | +| `season` | 季节/年度区间字典 | 无 | `study_season`、部分 observation | +| `study` | 具体研究/试验实施单元 | `crop`、`program`、`trial`、`location` | `study_contact`、`study_season`、`study_data_link`、`study_observation_level` 等 | +| `list` | 通用列表 | 可选 `person` | `list_item` | +| `list_item` | 列表明细项 | `list` | 无 | + +## 建议录入顺序 + +### 1. 录入基础字典 + +先录入 `crop` 和 `person`。 + +`crop` 是作物维度根数据,后续 `program`、`trial`、`study` 都会挂到它下面。`person` 是人员基础资料,后续会作为项目负责人、试验联系人、研究联系人、列表负责人使用。 + +### 2. 录入地点 + +录入 `location`,如果地点有坐标,需要先录入 `geojson` 和 `coordinate`。 + +地点可以先不绑定 `program/crop`,初始化脚本里就是先插入地点,再在 program 初始化阶段回填 `program_id` 和 `crop_id`。 + +### 3. 录入项目 Program + +录入 `program` 时需要已有 `crop`。如果有负责人,需要已有 `person`。 + +程序层面 `ProgramEntity.setCrop(...)` 直接绑定作物;后续 trial/study 设置 program 时,会同步继承 program 的 crop。 + +### 4. 录入 Trial + +录入 `trial` 时需要已有 `program` 和 `crop`。 + +`trial` 还可以同时录入: + +```text +trial_contact +trial_dataset_authorship +trial_publication +trial_additional_info +trial_external_references +``` + +其中 `trial_contact` 是 `trial` 和 `person` 的多对多关系表。 + +### 5. 录入 Season + +录入 `season`。它本身相对独立,但后续 `study` 会通过 `study_season` 关联多个 season。 + +### 6. 录入 Study + +录入 `study` 时通常需要已有: + +```text +crop +program +trial +location +``` + +`study` 是 core 模块向 pheno/geno 数据扩展的关键节点。后续 observation、observation_unit、sample、plate、variantset 等很多模块都会引用 `study`。 + +### 7. 录入 Study 附属信息 + +录入 study 后,再录入依赖 `study_id` 的附属表: + +```text +study_contact +study_data_link +study_environment_parameter +study_experimental_design +study_growth_facility +study_last_update +study_observation_level +study_season +study_variable +study_additional_info +study_external_references +``` + +## Core 数据流图 + +```mermaid +flowchart TD + A["1. crop 作物字典"] --> C["3. program 项目"] + B["2. person 人员"] --> C + B --> L["list 列表负责人,可选"] + L --> LI["list_item 列表项"] + + G["geojson / coordinate 坐标"] --> D["2. location 地点"] + A --> D + C --> D + D --> E["6. study 研究"] + + C --> F["4. trial 试验"] + A --> F + B --> FC["trial_contact 试验联系人"] + F --> FC + F --> FP["trial_publication / trial_dataset_authorship"] + + S["5. season 季节"] --> SS["study_season 研究季节"] + F --> E + C --> E + A --> E + E --> SS + E --> SC["study_contact 研究联系人"] + B --> SC + E --> SA["study_data_link / environment / design / growth_facility / last_update / observation_level"] + + E --> P1["pheno: observation_unit / observation"] + E --> G1["geno: sample / plate / variantset"] +``` + +## Core ER 关系图 + +```mermaid +erDiagram + crop ||--o{ program : "crop_id" + crop ||--o{ location : "crop_id" + crop ||--o{ trial : "crop_id" + crop ||--o{ study : "crop_id" + + person ||--o{ program : "lead_person_id" + person ||--o{ trial_contact : "person_db_id" + person ||--o{ study_contact : "person_db_id" + person ||--o{ list : "list_owner_person_id" + + program ||--o{ location : "program_id" + program ||--o{ trial : "program_id" + program ||--o{ study : "program_id" + + location ||--o{ location : "parent_location_id" + location ||--o{ study : "location_id" + + trial ||--o{ study : "trial_id" + trial ||--o{ trial_contact : "trial_db_id" + trial ||--o{ trial_publication : "trial_id" + trial ||--o{ trial_dataset_authorship : "trial_id" + + study ||--o{ study_contact : "study_db_id" + study ||--o{ study_season : "study_db_id" + season ||--o{ study_season : "season_db_id" + + study ||--o{ study_data_link : "study_id" + study ||--o{ study_environment_parameter : "study_id" + study ||--o{ study_experimental_design : "study_id" + study ||--o{ study_growth_facility : "study_id" + study ||--o{ study_last_update : "study_id" + study ||--o{ study_observation_level : "study_id" + + list ||--o{ list_item : "list_id" +``` + +## API 与表的对应关系 + +| API | 主表 | 说明 | +| --- | --- | --- | +| `GET /brapi/v2/commoncropnames` | `crop` | 查询作物名称列表 | +| `GET/POST/PUT /brapi/v2/people` | `person` | 人员查询、新增、修改;无删除接口 | +| `GET/POST/PUT /brapi/v2/programs` | `program` | 项目依赖 crop,可关联 lead person | +| `GET/POST/PUT /brapi/v2/locations` | `location` | 地点可关联 crop、program、parent location、geojson | +| `GET/POST/PUT /brapi/v2/trials` | `trial` | 试验依赖 program/crop,可关联 contacts/publications | +| `GET/PUT /brapi/v2/seasons` | `season` | 季节字典 | +| `GET/POST/PUT /brapi/v2/studies` | `study` | 研究依赖 trial/program/crop/location | +| `GET/POST/PUT /brapi/v2/lists` | `list`、`list_item` | 列表及列表项 | + +## 关键注意点 + +1. `crop` 是最重要的根字典之一,许多业务表都有 `crop_id`。 +2. `program` 是承上启下的业务节点,它依赖 `crop`,并被 `trial`、`study`、`location` 引用。 +3. `trial` 是 study 的上级试验组织,`study` 是后续表型、基因型数据的核心入口。 +4. `person` 与 `trial/study` 是多对多关系,通过 `trial_contact`、`study_contact` 连接。 +5. `study_season` 是 `study` 和 `season` 的多对多关系。 +6. `additional_info` 和 `external_reference` 是通用扩展表,core 主表通过各自的 `*_additional_info`、`*_external_references` 关联它们。 +7. 初始化脚本中 `list` 早于 `person` 插入,是因为初始 list 数据主要使用文本 owner 字段;如果业务上要设置 `list_owner_person_id`,应先有 `person`。 + diff --git a/docs/dev/01-core/01-crop.md b/docs/dev/01-core/01-crop.md new file mode 100644 index 0000000..9228030 --- /dev/null +++ b/docs/dev/01-core/01-crop.md @@ -0,0 +1,67 @@ +# 01 Core - crop 表录入说明 + +来源:`docs/requirements/01-core-data-entry-requirements.md` + +## 录入目标 + +`crop` 记录项目涉及的作物范围,是 Core 上下文的起点。后续 program、trial、study、germplasm、observation_variable、genome_map 等数据都需要回到作物维度筛选和解释。 + +## 前置依赖和下游引用 + +| 类型 | 内容 | +| --- | --- | +| 表 | `crop` | +| 前置依赖 | 无 | +| 下游引用 | `program`、`location`、`trial`、`study`、`germplasm`、`observation_variable`、`genome_map` | + +## 页面入口 + +| 页面 | 录入要求 | +| --- | --- | +| Crop 列表页 | 支持关键词查询、新增、编辑、查看详情、停用 | +| Crop 新增/编辑页 | 轻量表单,重点录入作物名称 | +| Crop 详情页 | 展示该作物下的项目、试验、研究、种质、图谱入口 | + +列表页表格字段:`crop_name`、下游 program 数、下游 study 数、状态。 + +## 字段录入 + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 作物主键,新增时系统生成,也可导入时指定 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `crop_name` | varchar(255) | 文本框 | 作物名称,用于所有下拉选择展示 | 必填、建议唯一 | + +## 校验规则 + +1. `crop_name` 必填。 +2. `crop_name` 建议唯一,新增和编辑时需要做重复提示。 +3. 已被下游引用的作物不能物理删除,只能停用或提示引用关系。 + +## 接口能力 + +| 接口 | 用途 | +| --- | --- | +| `GET /crops` | crop 分页查询 | +| `POST /crops` | 新增 crop | +| `GET /crops/{id}` | crop 详情 | +| `PUT /crops/{id}` | 编辑 crop | +| `DELETE /crops/{id}` | 删除或停用 crop,需做引用检查 | +| `GET /selectors/crops` | crop 搜索下拉,参数 `keyword` | + +## 导入导出 + +1. 支持通过 Core 导入流程导入 `crop`。 +2. 模板列名使用数据库字段名。 +3. 可按 `id` 幂等更新;无 `id` 时新增。 +4. 列表页支持导出当前筛选结果,导出文件应包含 ID 和展示名称。 + +## 删除/停用 + +当 `crop` 已被 `program`、`location`、`trial`、`study`、`germplasm`、`observation_variable`、`genome_map` 引用时,不允许物理删除。后端需要返回引用对象类型和数量,前端弹窗展示后引导用户停用。 + +## 验收点 + +1. Crop 列表页支持分页、关键词搜索和基础筛选。 +2. 外键选择器展示作物名称,提交保存 `crop.id`。 +3. 被引用的 crop 删除失败时,前端展示引用详情。 diff --git a/docs/dev/01-core/02-person.md b/docs/dev/01-core/02-person.md new file mode 100644 index 0000000..5b0440f --- /dev/null +++ b/docs/dev/01-core/02-person.md @@ -0,0 +1,76 @@ +# 01 Core - person 表录入说明 + +来源:`docs/requirements/01-core-data-entry-requirements.md` + +## 录入目标 + +`person` 记录项目负责人、trial 联系人、study 联系人和 list owner。它支撑责任追踪、联系人展示、权限和通知。 + +## 前置依赖和下游引用 + +| 类型 | 内容 | +| --- | --- | +| 表 | `person` | +| 前置依赖 | 无 | +| 下游引用 | `program.lead_person_id`、`trial_contact`、`study_contact`、`list.list_owner_person_id` | + +## 页面入口 + +| 页面 | 录入要求 | +| --- | --- | +| Person 列表页 | 支持姓名、邮箱、机构筛选;新增、编辑、查看详情、停用 | +| Person 新增/编辑页 | 使用联系人表单维护人员信息 | +| Person 详情页 | 展示负责的 program、参与的 trial/study、拥有的 list | + +列表页表格字段:姓名、邮箱、电话、机构、负责项目数、参与 study 数。 + +## 字段录入 + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 人员主键 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `description` | varchar(255) | 多行文本 | 人员说明、职责补充 | 可选 | +| `email_address` | varchar(255) | 邮箱输入框 | 邮箱地址 | 邮箱格式校验,建议唯一 | +| `first_name` | varchar(255) | 文本框 | 名 | 与 `last_name` 至少填写一个 | +| `institute_name` | varchar(255) | 文本框/选择器 | 所属机构 | 可选 | +| `last_name` | varchar(255) | 文本框 | 姓 | 与 `first_name` 至少填写一个 | +| `mailing_address` | varchar(255) | 多行文本 | 通讯地址 | 可选 | +| `middle_name` | varchar(255) | 文本框 | 中间名 | 可选 | +| `phone_number` | varchar(255) | 电话输入框 | 联系电话 | 可选,格式提示 | +| `userid` | varchar(255) | 文本框 | 外部用户 ID 或登录名 | 可选,建议唯一 | + +## 校验规则 + +1. 姓名必填,`first_name` 与 `last_name` 至少填写一个。 +2. `email_address` 需要校验邮箱格式。 +3. 同一邮箱不建议重复录入。 +4. 作为负责人或联系人被引用时,不允许物理删除。 + +## 接口能力 + +| 接口 | 用途 | +| --- | --- | +| `GET /persons` | person 分页查询 | +| `POST /persons` | 新增 person | +| `GET /persons/{id}` | person 详情 | +| `PUT /persons/{id}` | 编辑 person | +| `DELETE /persons/{id}` | 删除或停用 person,需做引用检查 | +| `GET /selectors/persons` | person 搜索下拉,参数 `keyword`、`instituteName` | + +## 导入导出 + +1. 支持通过 Core 导入流程导入 `person`。 +2. 模板列名使用数据库字段名。 +3. 可按 `id` 幂等更新;无 `id` 时新增。 +4. 列表页支持导出当前筛选结果,导出文件应包含 ID 和展示名称。 + +## 删除/停用 + +当 `person` 已作为 program 负责人、trial 联系人、study 联系人或 list owner 时,不允许物理删除。后端需要返回引用对象类型和数量,前端展示后引导用户停用。 + +## 验收点 + +1. Person 列表页支持分页、关键词搜索和机构筛选。 +2. 邮箱格式错误时不能提交。 +3. 作为负责人或联系人被引用的 person 删除失败时,前端展示引用详情。 diff --git a/docs/dev/01-core/03-program.md b/docs/dev/01-core/03-program.md new file mode 100644 index 0000000..b96188c --- /dev/null +++ b/docs/dev/01-core/03-program.md @@ -0,0 +1,77 @@ +# 01 Core - program 表录入说明 + +来源:`docs/requirements/01-core-data-entry-requirements.md` + +## 录入目标 + +`program` 表示长期育种项目,是多个 trial、study、材料、样本和结果的聚合维度。创建 program 时必须明确所属 crop,可选负责人。 + +## 前置依赖和下游引用 + +| 类型 | 内容 | +| --- | --- | +| 表 | `program` | +| 前置依赖 | `crop`,可选 `person` | +| 下游引用 | `trial`、`study`、`location`、`crossing_project`、`seed_lot`、`plate`、`sample` | + +## 页面入口 + +| 页面 | 录入要求 | +| --- | --- | +| Program 列表页 | 支持 crop、负责人、program_type、关键词筛选;新增、编辑、查看详情、停用 | +| Program 新增/编辑页 | 分为“基本信息”和“负责人/作物”两组 | +| Program 详情页 | Tab 展示 trial、study、location、seed lot、sample | + +列表页表格字段:`name`、`abbreviation`、crop、负责人、trial 数、study 数。 + +## 字段录入 + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 项目主键 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `abbreviation` | varchar(255) | 文本框 | 项目缩写 | 可选 | +| `documentationurl` | varchar(255) | URL 输入框 | 项目文档链接 | URL 格式校验 | +| `funding_information` | varchar(255) | 多行文本 | 经费来源说明 | 可选 | +| `name` | varchar(255) | 文本框 | 项目名称 | 必填 | +| `objective` | varchar(255) | 多行文本 | 项目目标 | 可选 | +| `program_type` | integer | 下拉框 | 项目类型枚举 | 可选,按 BrAPI 枚举 | +| `crop_id` | varchar(255) | 作物选择器 | 所属作物 | 必选,来源 `crop.id` | +| `lead_person_id` | varchar(255) | 人员选择器 | 项目负责人 | 可选,来源 `person.id` | + +## 校验规则 + +1. `name` 必填。 +2. `crop_id` 必选,且必须引用已存在的 `crop.id`。 +3. 选择负责人时,`lead_person_id` 必须引用已存在的 `person.id`。 +4. `documentationurl` 需要做 URL 格式校验。 + +## 接口能力 + +| 接口 | 用途 | +| --- | --- | +| `GET /programs` | program 分页查询 | +| `POST /programs` | 新增 program | +| `GET /programs/{id}` | program 详情 | +| `PUT /programs/{id}` | 编辑 program | +| `DELETE /programs/{id}` | 删除或停用 program,需做引用检查 | +| `GET /programs/{id}/trials` | 查询 program 下 trial | +| `GET /programs/{id}/studies` | 查询 program 下 study | +| `GET /selectors/programs` | program 搜索下拉,参数 `keyword`、`cropId` | + +## 导入导出 + +1. 支持通过 Core 导入流程导入 `program`。 +2. 外键列支持填写 ID;可额外支持名称匹配,但名称重复时必须报错。 +3. 可按 `id` 幂等更新;无 `id` 时新增。 +4. 列表页支持导出当前筛选结果,导出文件应包含 ID 和展示名称。 + +## 删除/停用 + +当 `program` 已有关联 `trial`、`study`、`location`、`crossing_project`、`seed_lot`、`plate`、`sample` 时,不允许物理删除。后端需要返回引用对象类型和数量,前端展示后引导用户停用。 + +## 验收点 + +1. 创建 program 时必须选择 crop。 +2. 负责人选择器只能保存已存在的 person ID。 +3. Program 详情页能进入该项目下的 trial 和 study。 diff --git a/docs/dev/01-core/04-location.md b/docs/dev/01-core/04-location.md new file mode 100644 index 0000000..b471760 --- /dev/null +++ b/docs/dev/01-core/04-location.md @@ -0,0 +1,86 @@ +# 01 Core - location 表录入说明 + +来源:`docs/requirements/01-core-data-entry-requirements.md` + +## 录入目标 + +`location` 记录试验实施地点,可作为公共地点,也可绑定 program 和 crop。study 创建时必须选择实施地点。 + +## 前置依赖和下游引用 + +| 类型 | 内容 | +| --- | --- | +| 表 | `location` | +| 前置依赖 | 可选 `crop`、`program`、父级 `location`、坐标 | +| 下游引用 | `study`、`seed_lot` | + +## 页面入口 + +| 页面 | 录入要求 | +| --- | --- | +| Location 列表页 | 支持 crop、program、location_type、country、关键词筛选;新增、编辑、查看详情、停用、地图查看 | +| Location 新增/编辑页 | 包含基本信息、行政区、坐标、父级地点 | +| Location 详情页 | 展示该地点下的 study 和 seed lot | + +列表页支持地图/表格两种视图。列表页表格字段:`location_name`、location_type、country、program、crop、父级地点。 + +## 字段录入 + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 地点主键 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `abbreviation` | varchar(255) | 文本框 | 地点缩写 | 可选 | +| `coordinate_description` | varchar(255) | 多行文本 | 坐标描述 | 可选 | +| `coordinate_uncertainty` | varchar(255) | 文本框 | 坐标不确定性 | 可选 | +| `country_code` | varchar(255) | 国家代码选择器 | 国家代码 | 可选,建议 ISO 代码 | +| `country_name` | varchar(255) | 文本框 | 国家名称 | 可选,可由国家代码带出 | +| `documentationurl` | varchar(255) | URL 输入框 | 地点文档链接 | URL 格式校验 | +| `environment_type` | varchar(255) | 下拉框/文本框 | 环境类型 | 可选 | +| `exposure` | varchar(255) | 文本框 | 暴露条件 | 可选 | +| `institute_address` | varchar(255) | 多行文本 | 机构地址 | 可选 | +| `institute_name` | varchar(255) | 文本框 | 机构名称 | 可选 | +| `location_name` | varchar(255) | 文本框 | 地点名称 | 必填 | +| `location_type` | varchar(255) | 下拉框 | 地点类型,如 field、greenhouse、storage | 可选 | +| `site_status` | varchar(255) | 下拉框 | 地点状态 | 可选 | +| `slope` | varchar(255) | 文本框 | 坡度 | 可选 | +| `topography` | varchar(255) | 文本框 | 地形 | 可选 | +| `coordinates_id` | varchar(255) | 坐标选择器/地图取点 | 坐标对象 | 可选,来源 `geojson/coordinate` | +| `crop_id` | varchar(255) | 作物选择器 | 关联作物 | 可选,来源 `crop.id` | +| `parent_location_id` | varchar(255) | 地点选择器 | 父级地点 | 可选,不能选择自己 | +| `program_id` | varchar(255) | 项目选择器 | 所属项目 | 可选,来源 `program.id` | + +## 校验规则 + +1. `location_name` 必填。 +2. 坐标字段格式合法。 +3. `parent_location_id` 不能选择自己。 +4. 选择 program 后可自动带出 crop,但允许地点作为公共地点不绑定 program。 +5. `documentationurl` 需要做 URL 格式校验。 + +## 接口能力 + +| 接口 | 用途 | +| --- | --- | +| `GET /locations` | location 分页查询 | +| `POST /locations` | 新增 location | +| `GET /locations/{id}` | location 详情 | +| `PUT /locations/{id}` | 编辑 location | +| `GET /selectors/locations` | location 搜索下拉,参数 `keyword`、`programId`、`cropId`、`locationType` | + +## 导入导出 + +1. 支持通过 Core 导入流程导入 `location`。 +2. 导入时如果 `parent_location_id` 指向自己,应报错并指出行号。 +3. 外键列支持填写 ID;可额外支持名称匹配,但名称重复时必须报错。 +4. 列表页支持导出当前筛选结果,导出文件应包含 ID 和展示名称。 + +## 删除/停用 + +当 `location` 已被 `study` 或 `seed_lot` 引用时,不允许物理删除。后端需要返回引用对象类型和数量,前端展示后引导用户停用。 + +## 验收点 + +1. Location 列表页支持表格视图和地图查看。 +2. 父级地点不能选择自己。 +3. Study 新增时 location 选择器可按 program/crop 过滤。 diff --git a/docs/dev/01-core/05-trial.md b/docs/dev/01-core/05-trial.md new file mode 100644 index 0000000..c927353 --- /dev/null +++ b/docs/dev/01-core/05-trial.md @@ -0,0 +1,78 @@ +# 01 Core - trial 表录入说明 + +来源:`docs/requirements/01-core-data-entry-requirements.md` + +## 录入目标 + +`trial` 表示某一批试验、区域试验或年度试验,位于 program 与 study 之间。创建 trial 时选择 program,并自动带出或校验 crop。 + +## 前置依赖和下游引用 + +| 类型 | 内容 | +| --- | --- | +| 表 | `trial` | +| 前置依赖 | `program`、`crop` | +| 下游引用 | `study`、`observation_unit`、`observation`、`plate`、`sample` | + +## 页面入口 + +| 页面 | 录入要求 | +| --- | --- | +| Trial 列表页 | 支持 crop、program、active、起止日期、关键词筛选;新增、编辑、查看详情、停用 | +| Trial 新增/编辑页 | 包含基本信息、项目作物、联系人、出版物四个区域 | +| Trial 详情页 | 展示 study 列表和 phenotyping/genotyping 入口 | + +列表页表格字段:`trial_name`、program、crop、`start_date`、`end_date`、active。 + +## 字段录入 + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 试验主键 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `active` | boolean | 开关 | 是否启用 | 默认启用 | +| `documentationurl` | varchar(255) | URL 输入框 | 试验文档链接 | URL 格式校验 | +| `end_date` | timestamp | 日期选择器 | 结束日期 | 不早于 `start_date` | +| `start_date` | timestamp | 日期选择器 | 开始日期 | 可选 | +| `trial_description` | varchar(255) | 多行文本 | 试验描述 | 可选 | +| `trial_name` | varchar(255) | 文本框 | 试验名称 | 必填 | +| `trialpui` | varchar(255) | 文本框 | 试验永久标识 | 可选,建议唯一 | +| `crop_id` | varchar(255) | 作物选择器 | 所属作物 | 必选,来源 `crop.id` | +| `program_id` | varchar(255) | 项目选择器 | 所属项目 | 必选,来源 `program.id` | + +## 校验规则 + +1. `trial_name` 必填。 +2. `program_id` 必选且必须存在。 +3. `crop_id` 必选且必须与 `program.crop_id` 一致。 +4. `end_date` 不能早于 `start_date`。 +5. `documentationurl` 需要做 URL 格式校验。 + +## 接口能力 + +| 接口 | 用途 | +| --- | --- | +| `GET /trials` | trial 分页查询 | +| `POST /trials` | 新增 trial | +| `GET /trials/{id}` | trial 详情 | +| `PUT /trials/{id}` | 编辑 trial | +| `DELETE /trials/{id}` | 删除或停用 trial,需检查 study 引用 | +| `GET /trials/{id}/studies` | 查询 trial 下 study | +| `GET /selectors/trials` | trial 搜索下拉,参数 `keyword`、`programId`、`cropId`、`active` | + +## 导入导出 + +1. 支持通过 Core 导入流程导入 `trial`。 +2. 外键列支持填写 ID;可额外支持名称匹配,但名称重复时必须报错。 +3. 导入时必须校验 trial 的 crop 与 program 的 crop 一致。 +4. 列表页支持导出当前筛选结果,导出文件应包含 ID 和展示名称。 + +## 删除/停用 + +当 `trial` 已被 `study`、`observation_unit`、`observation`、`plate`、`sample` 引用时,不允许物理删除。后端需要返回引用对象类型和数量,前端展示后引导用户停用。 + +## 验收点 + +1. 创建 trial 时,选择 program 后自动带出 crop。 +2. Trial 列表页支持按 crop、program、active、日期筛选。 +3. 已被 study 引用的 trial 不能直接删除。 diff --git a/docs/dev/01-core/06-season.md b/docs/dev/01-core/06-season.md new file mode 100644 index 0000000..b9deda4 --- /dev/null +++ b/docs/dev/01-core/06-season.md @@ -0,0 +1,66 @@ +# 01 Core - season 表录入说明 + +来源:`docs/requirements/01-core-data-entry-requirements.md` + +## 录入目标 + +`season` 记录年度和季节,用于 study 的季节绑定,以及部分 observation 的季节维度统计。 + +## 前置依赖和下游引用 + +| 类型 | 内容 | +| --- | --- | +| 表 | `season` | +| 前置依赖 | 无 | +| 下游引用 | `study_season`、部分 `observation` | + +## 页面入口 + +| 页面 | 录入要求 | +| --- | --- | +| Season 列表页 | 支持 year、season 筛选;新增、编辑、查看详情、停用 | +| Season 新增/编辑页 | 简单表单 | +| Season 详情页 | 展示关联 study | + +列表页表格字段:`year`、`season`、关联 study 数。 + +## 字段录入 + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 季节主键 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `season` | varchar(255) | 文本框/下拉框 | 季节名称,如 Spring、Summer | 必填 | +| `year` | integer | 年份选择器 | 年份 | 必填,四位年份 | + +## 校验规则 + +1. `season` 必填。 +2. `year` 必填,使用四位年份。 +3. 同一年份内 season 名称不建议重复。 + +## 接口能力 + +| 接口 | 用途 | +| --- | --- | +| `GET /seasons` | season 查询 | +| `POST /seasons` | 新增 season | +| `PUT /seasons/{id}` | 编辑 season | +| `GET /selectors/seasons` | season 搜索下拉,参数 `year`、`keyword` | + +## 导入导出 + +1. 支持通过 Core 导入流程导入 `season`。 +2. 模板列名使用数据库字段名。 +3. 可按 `id` 幂等更新;无 `id` 时新增。 +4. 列表页支持导出当前筛选结果,导出文件应包含 ID 和展示名称。 + +## 删除/停用 + +当 `season` 已被 `study_season` 或 `observation` 引用时,不允许物理删除。后端需要返回引用对象类型和数量,前端展示后引导用户停用。 + +## 验收点 + +1. Season 列表页支持按 year 和 season 筛选。 +2. 同一年份内重复季节名称需要提示。 +3. Study 新增/编辑页可以多选 season。 diff --git a/docs/dev/01-core/07-study.md b/docs/dev/01-core/07-study.md new file mode 100644 index 0000000..5047c95 --- /dev/null +++ b/docs/dev/01-core/07-study.md @@ -0,0 +1,103 @@ +# 01 Core - study 表录入说明 + +来源:`docs/requirements/01-core-data-entry-requirements.md` + +## 录入目标 + +`study` 是一次真正落地执行的试验,是 Core 模块最关键的上下文单元。后续 observation_unit、event、observation、plate、sample、variantset 都会直接或间接挂到 study 上。 + +## 前置依赖和下游引用 + +| 类型 | 内容 | +| --- | --- | +| 表 | `study` | +| 前置依赖 | `crop`、`program`、`trial`、`location`,可选 `season` | +| 下游引用 | `observation_unit`、`event`、`observation`、`plate`、`sample`、`variantset` | + +## 页面入口 + +| 页面 | 录入要求 | +| --- | --- | +| Study 列表页 | 支持 crop、program、trial、location、season、active、study_type、关键词筛选;新增、编辑、查看详情、停用 | +| Study 新增/编辑页 | 分组表单:基本信息、上下文、地点季节、实验设计、联系人 | +| Study 工作台 | 点击 study 名称或“进入工作台”后进入,展示下游业务入口和聚合数量 | + +Study 列表页表格字段:`study_name`、`study_code`、program、trial、location、`start_date`、`end_date`、active。 + +## 字段录入 + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | study 主键 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `active` | boolean | 开关 | 是否启用 | 默认启用 | +| `cultural_practices` | varchar(255) | 多行文本 | 栽培管理说明 | 可选 | +| `documentationurl` | varchar(255) | URL 输入框 | study 文档链接 | URL 格式校验 | +| `end_date` | timestamp | 日期选择器 | 结束日期 | 不早于 `start_date` | +| `license` | varchar(255) | 文本框 | 数据许可证 | 可选 | +| `observation_units_description` | varchar(255) | 多行文本 | 观测单元说明 | 可选 | +| `start_date` | timestamp | 日期选择器 | 开始日期 | 可选 | +| `study_code` | varchar(255) | 文本框 | study 编码 | 可选,建议同项目内唯一 | +| `study_description` | varchar(255) | 多行文本 | study 描述 | 可选 | +| `study_name` | varchar(255) | 文本框 | study 名称 | 必填 | +| `studypui` | varchar(255) | 文本框 | study 永久标识 | 可选,建议唯一 | +| `study_type` | varchar(255) | 下拉框 | study 类型 | 可选 | +| `crop_id` | varchar(255) | 作物选择器 | 所属作物 | 必选,来源 `crop.id` | +| `location_id` | varchar(255) | 地点选择器 | 实施地点 | 必选,来源 `location.id` | +| `program_id` | varchar(255) | 项目选择器 | 所属项目 | 必选,来源 `program.id` | +| `trial_id` | varchar(255) | 试验选择器 | 所属 trial | 必选,来源 `trial.id` | + +## 新增/编辑分区 + +| 区域 | 字段/功能 | 要求 | +| --- | --- | --- | +| 基本信息 | study_name、study_code、study_type、active、start_date、end_date | study_name 必填;结束日期不早于开始日期 | +| 上下文 | crop、program、trial、location、season | program、trial、crop、location 必选;program -> trial 联动 | +| 说明信息 | study_description、cultural_practices、observation_units_description、license、documentationurl | URL 格式校验 | +| 联系人 | study_contact | 多选 person,可增删 | +| 季节 | study_season | 可多选 season | +| 提交后 | 保存成功进入 Study 工作台 | 新建成功后自动跳转 | + +## 校验规则 + +1. `study_name` 必填。 +2. `program_id`、`trial_id`、`crop_id`、`location_id` 必选且必须存在。 +3. `trial_id` 必须属于所选 `program_id`。 +4. 选择 program 后过滤 trial;选择 trial 后自动带出 crop。 +5. `end_date` 不能早于 `start_date`。 +6. 编辑 study 时,如果已有 observation_unit、sample、observation,变更 program/trial/crop/location 前必须二次确认并检查一致性。 +7. study 是下游核心引用,删除必须强提示。 + +## 接口能力 + +| 接口 | 用途 | +| --- | --- | +| `GET /studies` | study 分页查询 | +| `POST /studies` | 新增 study | +| `GET /studies/{id}` | study 详情 | +| `PUT /studies/{id}` | 编辑 study | +| `DELETE /studies/{id}` | 删除或停用 study,需检查下游引用 | +| `GET /studies/{id}/workbench` | Study 工作台聚合信息 | +| `GET /studies/{id}/contacts` | study 联系人 | +| `GET /studies/{id}/seasons` | study 季节 | +| `GET /selectors/studies` | study 搜索下拉,参数 `keyword`、`programId`、`trialId`、`locationId`、`active` | + +## 导入导出 + +1. 支持通过 Core 导入流程导入 `study`。 +2. 导入时必须校验字段完整性、必填、格式、外键是否存在、联动关系是否一致。 +3. 如果 trial 不属于 program,应报错并指出行号。 +4. study 导出应支持工作台摘要,包括联系人、season、下游数据数量。 +5. 大数据量导出时应走异步任务。 + +## 删除/停用 + +当 `study` 已被 `observation_unit`、`event`、`observation`、`plate`、`sample`、`variantset` 引用时,不允许物理删除。后端需要返回引用对象类型和数量,前端弹窗展示后引导用户停用。 + +## 验收点 + +1. Study 列表页支持按 crop、program、trial、location、season、active 筛选。 +2. 点击 study 名称或“进入工作台”按钮后进入 Study 工作台。 +3. Study 工作台可以看到观测单元、表型、样本、基因型入口。 +4. study 创建成功后自动进入 Study 工作台。 +5. 已被 observation_unit、sample、observation 引用的 study 不能直接删除。 diff --git a/docs/dev/01-core/08-list.md b/docs/dev/01-core/08-list.md new file mode 100644 index 0000000..58cb0bc --- /dev/null +++ b/docs/dev/01-core/08-list.md @@ -0,0 +1,73 @@ +# 01 Core - list 表录入说明 + +来源:`docs/requirements/01-core-data-entry-requirements.md` + +## 录入目标 + +`list` 是通用分组列表的主表,用于维护一组业务对象或文本项。`list` 负责列表基本信息,`list_item` 负责列表明细。 + +## 前置依赖和下游引用 + +| 类型 | 内容 | +| --- | --- | +| 表 | `list` | +| 前置依赖 | 可选 `person` | +| 下游引用 | `list_item`、业务查询和分组 | + +## 页面入口 + +| 页面 | 录入要求 | +| --- | --- | +| List 列表页 | 展示 list,支持查询、新增、编辑 | +| List 详情页 | 顶部展示 list 基本信息,下方内嵌 list_item 表格 | + +List 详情页支持新增 item、批量导入 item、删除 item、排序。同一 list 内 item 不重复。 + +## 字段录入 + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 列表主键 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `date_created` | timestamp | 只读日期时间 | 创建时间 | 系统自动写入 | +| `date_modified` | timestamp | 只读日期时间 | 修改时间 | 系统自动更新 | +| `description` | varchar(255) | 多行文本 | 列表描述 | 可选 | +| `list_name` | varchar(255) | 文本框 | 列表名称 | 必填 | +| `list_owner_name` | varchar(255) | 文本框 | 列表 owner 名称 | 可选,可由 owner person 带出 | +| `list_source` | varchar(255) | 文本框 | 列表来源 | 可选 | +| `list_type` | integer | 下拉框 | 列表类型 | 必填,按 BrAPI 枚举 | +| `list_owner_person_id` | varchar(255) | 人员选择器 | 列表 owner | 可选,来源 `person.id` | + +## 校验规则 + +1. `list_name` 必填。 +2. `list_type` 必填,按 BrAPI 枚举选择。 +3. 如果绑定 owner,`list_owner_person_id` 必须引用已存在的 `person.id`。 +4. `date_created`、`date_modified` 由系统维护,前端只读。 + +## 接口能力 + +| 接口 | 用途 | +| --- | --- | +| `GET /lists` | list 查询 | +| `POST /lists` | 新增 list | +| `PUT /lists/{id}` | 编辑 list | +| `POST /lists/{id}/items` | 给 list 添加 item | +| `DELETE /lists/{id}/items/{itemId}` | 删除 list item | + +## 导入导出 + +1. 支持通过 Core 导入流程导入 `list`。 +2. 模板列名使用数据库字段名。 +3. 外键列支持填写 `list_owner_person_id`;可额外支持名称匹配,但名称重复时必须报错。 +4. 列表页支持导出当前筛选结果,导出文件应包含 ID 和展示名称。 + +## 删除/停用 + +当 `list` 已有 `list_item` 时,删除前必须提示。可以先清空明细再删除 list。 + +## 验收点 + +1. List 详情页顶部展示 list 基本信息,下方展示 list_item 表格。 +2. 同一 list 内 item 不重复。 +3. 绑定 owner 时只能保存已存在的 person ID。 diff --git a/docs/dev/01-core/09-list_item.md b/docs/dev/01-core/09-list_item.md new file mode 100644 index 0000000..2ce3d4a --- /dev/null +++ b/docs/dev/01-core/09-list_item.md @@ -0,0 +1,62 @@ +# 01 Core - list_item 表录入说明 + +来源:`docs/requirements/01-core-data-entry-requirements.md` + +## 录入目标 + +`list_item` 是 list 的明细项,是“给 list 添加成员”这个动作留下的记录。它可以保存目标对象 ID 或文本值,用于业务查询和分组。 + +## 前置依赖和下游引用 + +| 类型 | 内容 | +| --- | --- | +| 表 | `list_item` | +| 前置依赖 | `list` | +| 下游引用 | 业务查询和分组 | + +## 页面入口 + +| 页面 | 录入要求 | +| --- | --- | +| List 详情页 | 在 list 基本信息下方内嵌 list_item 表格 | +| 批量导入 | 支持给指定 list 批量导入 item | + +新增 item 时可选择目标类型,例如 germplasm、study、sample。 + +## 字段录入 + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 列表项主键 | 必填、唯一 | +| `item` | varchar(255) | 文本框/对象选择器 | 列表项值,可存目标对象 ID 或文本 | 必填,同一 list 内不重复 | +| `list_id` | varchar(255) | List 选择器 | 所属列表 | 必选,来源 `list.id` | + +## 校验规则 + +1. `list_id` 必选,且必须引用已存在的 `list.id`。 +2. `item` 必填。 +3. 同一 `list_id` 内 `item` 不应重复。 + +## 接口能力 + +| 接口 | 用途 | +| --- | --- | +| `POST /lists/{id}/items` | 给 list 添加 item | +| `DELETE /lists/{id}/items/{itemId}` | 删除 list item | + +## 导入导出 + +1. 支持通过 Core 导入流程导入 `list_item`。 +2. 导入时需要校验 `list_id` 是否存在。 +3. 导入时需要校验同一 list 内 item 是否重复。 +4. 错误报告返回行号、字段名、错误原因、建议修正方式。 + +## 删除/停用 + +`list_item` 是明细记录,可从 List 详情页删除。删除前需要确认,删除后不影响 list 基本信息。 + +## 验收点 + +1. List 详情页可新增 item、批量导入 item、删除 item、排序。 +2. 同一 list 内重复 item 不能提交。 +3. 批量导入错误能定位到行号和字段名。 diff --git a/docs/dev/01-core/10-trial_contact.md b/docs/dev/01-core/10-trial_contact.md new file mode 100644 index 0000000..600c13a --- /dev/null +++ b/docs/dev/01-core/10-trial_contact.md @@ -0,0 +1,57 @@ +# 01 Core - trial_contact 表录入说明 + +来源:`docs/requirements/01-core-data-entry-requirements.md` + +## 录入目标 + +`trial_contact` 是给 trial 添加联系人的关系表,不是独立主数据。它记录 trial 与 person 的多对多关系,用于试验责任追踪、联系人展示、权限和通知。 + +## 前置依赖和下游引用 + +| 类型 | 内容 | +| --- | --- | +| 表 | `trial_contact` | +| 前置依赖 | `trial`、`person` | +| 下游引用 | trial 联系人展示、权限和通知 | + +## 页面入口 + +| 页面 | 录入要求 | +| --- | --- | +| Trial 新增/编辑页 | 联系人区域使用可增删表格选择 `person` | +| Trial 详情页 | 展示 trial 联系人 | +| 批量导入 | 支持通过 Core 导入流程导入 trial 联系人关系 | + +## 字段录入 + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `trial_db_id` | varchar(255) | Trial 选择器 | 所属 trial | 必选,来源 `trial.id` | +| `person_db_id` | varchar(255) | 人员选择器 | 联系人 | 必选,来源 `person.id` | + +## 校验规则 + +1. `trial_db_id` 必选,且必须引用已存在的 `trial.id`。 +2. `person_db_id` 必选,且必须引用已存在的 `person.id`。 +3. 同一 trial 下不建议重复添加同一个 person。 + +## 接口能力 + +本文档原始需求未强制限定 trial_contact 的独立 URL。实现时可以随 trial 新增/编辑一起保存,也可以提供 trial 联系人子资源接口;前端需要具备新增、删除、查询 trial 联系人的能力。 + +## 导入导出 + +1. 支持通过 Core 导入流程导入 `trial_contact`。 +2. 外键列支持填写 ID;可额外支持名称匹配,但名称重复时必须报错。 +3. 导入时需要校验 trial 和 person 是否存在。 +4. 错误报告返回行号、字段名、错误原因、建议修正方式。 + +## 删除/停用 + +`trial_contact` 是关系记录,可在 Trial 新增/编辑页或详情页移除。删除关系不应删除 `trial` 或 `person` 主数据。 + +## 验收点 + +1. Trial 新增/编辑页可增删联系人。 +2. 联系人选择器展示 person 名称,提交保存 `person.id`。 +3. 删除 trial_contact 后,person 主数据仍保留。 diff --git a/docs/dev/01-core/11-trial_publication.md b/docs/dev/01-core/11-trial_publication.md new file mode 100644 index 0000000..38bed18 --- /dev/null +++ b/docs/dev/01-core/11-trial_publication.md @@ -0,0 +1,56 @@ +# 01 Core - trial_publication 表录入说明 + +来源:`docs/requirements/01-core-data-entry-requirements.md` + +## 录入目标 + +`trial_publication` 是给 trial 记录出版物、报告或文献引用的痕迹表。它挂在 trial 下,用于说明试验相关的外部文献和永久标识。 + +## 前置依赖和下游引用 + +| 类型 | 内容 | +| --- | --- | +| 表 | `trial_publication` | +| 前置依赖 | `trial` | +| 下游引用 | trial 出版物/报告展示 | + +## 页面入口 + +| 页面 | 录入要求 | +| --- | --- | +| Trial 新增/编辑页 | 出版物区域可增删 publication 记录 | +| Trial 详情页 | 展示 trial 的出版物、报告或引用 | + +## 字段录入 + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 出版物记录主键 | 必填、唯一 | +| `publicationpui` | varchar(255) | 文本框 | 出版物 PUI | 可选 | +| `publication_reference` | varchar(255) | 文本框/URL | 出版物引用 | 可选 | +| `trial_id` | varchar(255) | Trial 选择器 | 所属 trial | 必选,来源 `trial.id` | + +## 校验规则 + +1. `id` 必填且唯一,新增时可由系统生成。 +2. `trial_id` 必选,且必须引用已存在的 `trial.id`。 +3. `publication_reference` 如果录入 URL,需要做 URL 格式校验。 +4. `publicationpui` 建议唯一。 + +## 接口能力 + +本文档原始需求未强制限定 trial_publication 的独立 URL。实现时可以随 trial 新增/编辑一起保存,也可以提供 trial 出版物子资源接口;前端需要具备新增、删除、查询 trial 出版物的能力。 + +## 导入导出 + +原始导入对象清单未单独列出 `trial_publication`,但字段需求中包含该表。若纳入 Core 导入,应遵循通用导入流程:模板列名使用数据库字段名,外键校验 `trial_id`,错误报告返回行号、字段名和错误原因。 + +## 删除/停用 + +`trial_publication` 是 trial 的明细记录,可在 Trial 新增/编辑页或详情页移除。删除 publication 记录不应删除 `trial` 主数据。 + +## 验收点 + +1. Trial 新增/编辑页可维护出版物记录。 +2. `trial_id` 不存在时不能提交。 +3. 删除 publication 记录后,trial 主数据仍保留。 diff --git a/docs/dev/01-core/12-study_contact.md b/docs/dev/01-core/12-study_contact.md new file mode 100644 index 0000000..3cec137 --- /dev/null +++ b/docs/dev/01-core/12-study_contact.md @@ -0,0 +1,61 @@ +# 01 Core - study_contact 表录入说明 + +来源:`docs/requirements/01-core-data-entry-requirements.md` + +## 录入目标 + +`study_contact` 是给 study 添加联系人的关系表,不是独立主数据。它记录 study 与 person 的多对多关系,用于 Study 工作台联系人展示、责任追踪、权限和通知。 + +## 前置依赖和下游引用 + +| 类型 | 内容 | +| --- | --- | +| 表 | `study_contact` | +| 前置依赖 | `study`、`person` | +| 下游引用 | Study 工作台联系人展示、权限和通知 | + +## 页面入口 + +| 页面 | 录入要求 | +| --- | --- | +| Study 新增/编辑页 | 联系人区域多选 person,可增删 | +| Study 工作台 | 展示 study 联系人 | +| 批量导入 | 支持通过 Core 导入流程导入 study 联系人关系 | + +## 字段录入 + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `study_db_id` | varchar(255) | Study 选择器 | 所属 study | 必选,来源 `study.id` | +| `person_db_id` | varchar(255) | 人员选择器 | 联系人 | 必选,来源 `person.id` | + +## 校验规则 + +1. `study_db_id` 必选,且必须引用已存在的 `study.id`。 +2. `person_db_id` 必选,且必须引用已存在的 `person.id`。 +3. 同一 study 下不建议重复添加同一个 person。 + +## 接口能力 + +| 接口 | 用途 | +| --- | --- | +| `GET /studies/{id}/contacts` | study 联系人,返回 person 列表 | + +本文档原始需求未强制限定 study_contact 的写入 URL。实现时可以随 study 新增/编辑一起保存,也可以提供 study 联系人子资源接口;前端需要具备新增、删除、查询 study 联系人的能力。 + +## 导入导出 + +1. 支持通过 Core 导入流程导入 `study_contact`。 +2. 外键列支持填写 ID;可额外支持名称匹配,但名称重复时必须报错。 +3. 导入时需要校验 study 和 person 是否存在。 +4. 错误报告返回行号、字段名、错误原因、建议修正方式。 + +## 删除/停用 + +`study_contact` 是关系记录,可在 Study 新增/编辑页或工作台移除。删除关系不应删除 `study` 或 `person` 主数据。 + +## 验收点 + +1. Study 新增/编辑页可增删联系人。 +2. Study 工作台能展示联系人列表。 +3. 删除 study_contact 后,person 主数据仍保留。 diff --git a/docs/dev/01-core/13-study_season.md b/docs/dev/01-core/13-study_season.md new file mode 100644 index 0000000..8bdfb1e --- /dev/null +++ b/docs/dev/01-core/13-study_season.md @@ -0,0 +1,61 @@ +# 01 Core - study_season 表录入说明 + +来源:`docs/requirements/01-core-data-entry-requirements.md` + +## 录入目标 + +`study_season` 是给 study 绑定 season 的关系表。一个 study 可以关联多个 season,用于多季节筛选、统计和 Study 工作台上下文展示。 + +## 前置依赖和下游引用 + +| 类型 | 内容 | +| --- | --- | +| 表 | `study_season` | +| 前置依赖 | `study`、`season` | +| 下游引用 | Study 工作台季节展示、多季节筛选和统计 | + +## 页面入口 + +| 页面 | 录入要求 | +| --- | --- | +| Study 新增/编辑页 | 季节区域多选 season | +| Study 工作台 | 展示 study 关联 season | +| 批量导入 | 支持通过 Core 导入流程导入 study 与 season 关系 | + +## 字段录入 + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `study_db_id` | varchar(255) | Study 选择器 | 所属 study | 必选,来源 `study.id` | +| `season_db_id` | varchar(255) | Season 选择器 | 关联季节 | 必选,来源 `season.id` | + +## 校验规则 + +1. `study_db_id` 必选,且必须引用已存在的 `study.id`。 +2. `season_db_id` 必选,且必须引用已存在的 `season.id`。 +3. 同一 study 下不建议重复绑定同一个 season。 + +## 接口能力 + +| 接口 | 用途 | +| --- | --- | +| `GET /studies/{id}/seasons` | study 季节,返回 season 列表 | + +本文档原始需求未强制限定 study_season 的写入 URL。实现时可以随 study 新增/编辑一起保存,也可以提供 study season 子资源接口;前端需要具备新增、删除、查询 study season 的能力。 + +## 导入导出 + +1. 支持通过 Core 导入流程导入 `study_season`。 +2. 外键列支持填写 ID;可额外支持名称匹配,但名称重复时必须报错。 +3. 导入时需要校验 study 和 season 是否存在。 +4. 错误报告返回行号、字段名、错误原因、建议修正方式。 + +## 删除/停用 + +`study_season` 是关系记录,可在 Study 新增/编辑页或工作台移除。删除关系不应删除 `study` 或 `season` 主数据。 + +## 验收点 + +1. Study 新增/编辑页可多选 season。 +2. Study 工作台能展示关联 season。 +3. 删除 study_season 后,season 主数据仍保留。 diff --git a/docs/dev/02-germplasm-seed/01-breeding_method.md b/docs/dev/02-germplasm-seed/01-breeding_method.md new file mode 100644 index 0000000..bc7503c --- /dev/null +++ b/docs/dev/02-germplasm-seed/01-breeding_method.md @@ -0,0 +1,26 @@ +# 02 Germplasm / Seed - breeding_method 表录入说明 + +来源:`docs/requirements/02-germplasm-seed-entry-requirements.md` + +## 录入目标 + +`breeding_method` 是育种方法字典,用来说明某个 germplasm 是通过什么方式形成的,例如杂交选育、回交、自交系选育、诱变、转基因、克隆选择等。它不是一次具体杂交动作,而是材料来源方法的分类。 + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| -------------- | -------------------------------------------- | -------------------------------- | --------- | -------------------------------- | +| `id` | 育种方法主键,系统内部唯一标识 | 新增时系统生成;导入时可允许指定 | 隐藏/只读 | 必填、唯一;编辑时不允许修改 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许前端手填 | +| `abbreviation` | 方法缩写,如 MB、BC、DH | 用户录入 | 文本框 | 可选;建议同一用户下唯一 | +| `description` | 方法解释,如“回交用于恢复目标基因” | 用户录入 | 多行文本 | 可选,限制长度 | +| `name` | 方法名称,如 Male Backcross、Doubled Haploid | 用户录入 | 文本框 | 必填;建议唯一;作为下拉展示名称 | + +## 页面与交互 + +- 列表页展示:方法名称、缩写、描述、使用材料数量。 +- 新增页为简单字典表单。 +- 删除前检查是否被 `germplasm.breeding_method_id` 引用;已引用时不允许物理删除,只允许停用。 + +--- + diff --git a/docs/dev/02-germplasm-seed/02-germplasm.md b/docs/dev/02-germplasm-seed/02-germplasm.md new file mode 100644 index 0000000..e52a5f8 --- /dev/null +++ b/docs/dev/02-germplasm-seed/02-germplasm.md @@ -0,0 +1,54 @@ +# 02 Germplasm / Seed - germplasm 表录入说明 + +来源:`docs/requirements/02-germplasm-seed-entry-requirements.md` + +## 录入目标 + +`germplasm` 是材料身份证,描述一个品种、品系、亲本、后代材料、遗传资源或研究材料“是谁”。它不表示库存数量,库存数量由 `seed_lot` 表达。 + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------------------------- | ------------------------------------------------------------ | -------------------------------------- | ----------------- | --------------------------------------------------------- | +| `id` | 种质主键,系统内部唯一标识 | 系统生成;导入可指定 | 隐藏/只读 | 必填、唯一;编辑不可随意修改 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `accession_number` | 材料在种质库/机构内的 accession 编号,如 PI 113869 | 用户录入或导入 | 文本框 | 可选;建议同一 crop / institution 下唯一 | +| `acquisition_date` | 材料进入本系统或本机构的获取日期 | 用户录入 | 日期选择器 | 可选;不得晚于当前日期太多,导入时允许缺月/缺日需统一规则 | +| `acquisition_source_code` | 获取来源编码,如采集、引进、交换、繁殖等 | 用户选择 | 下拉框 | 可选;值来自 BrAPI/MCPD 枚举或系统字典 | +| `biological_status_of_accession_code` | 材料生物状态,如野生、地方品种、育种材料、改良品种、突变体等 | 用户选择 | 下拉框 | 可选;使用受控枚举,不建议自由输入 | +| `collection` | 材料所属集合、群体、panel 或 collection | 用户录入/选择 | 文本框/选择器 | 可选;可用于分组筛选 | +| `country_of_origin_code` | 原产国或育成/选育国家代码 | 用户选择 | 国家代码选择器 | 可选;建议使用 ISO 3166-1 三字母代码 | +| `default_display_name` | 系统默认展示名,给下拉框、表格、详情标题使用 | 用户录入,可由 germplasm_name 自动带出 | 文本框 | 与 `germplasm_name` 至少填一个;建议必填 | +| `documentationurl` | 材料说明文档、外部数据库页面或 DOI 链接 | 用户录入 | URL 输入框 | 可选;校验 URL 格式 | +| `genus` | 属名,如 Oryza、Triticum | 用户录入/字典选择 | 文本框/物种选择器 | 可选;建议首字母大写 | +| `germplasm_name` | 材料名称,可以是品种名、品系名、后代编号 | 用户录入 | 文本框 | 与 `default_display_name` 至少填一个;不强制全局唯一 | +| `germplasmpui` | 永久唯一标识,通常是 DOI、URI 或全局唯一编码 | 用户录入/外部导入 | 文本框/URL 输入框 | 可选;若填写必须唯一;建议用于跨系统交换 | +| `germplasm_preprocessing` | 材料用于试验前的统一处理说明,如消毒、催芽、低温处理 | 用户录入 | 文本框/多行文本 | 可选 | +| `mls_status` | 多边系统 MLS 状态,涉及植物遗传资源交换协议 | 用户选择 | 下拉框 | 可选;普通业务可隐藏到高级信息 | +| `seed_source` | 材料来源标识,如来源机构+accession,或亲本组合描述 | 用户录入 | 文本框 | 可选;注意它不是库存批次,不等于 seed_lot | +| `seed_source_description` | 材料来源详细说明 | 用户录入 | 多行文本 | 可选 | +| `species` | 种名,如 sativa、aestivum | 用户录入/物种字典 | 文本框 | 可选;建议小写 | +| `species_authority` | 种名命名权威,如 L. | 用户录入 | 文本框 | 可选 | +| `subtaxa` | 亚种、变种、品种群、line 等更细分类 | 用户录入 | 文本框 | 可选 | +| `subtaxa_authority` | 亚种/变种命名权威 | 用户录入 | 文本框 | 可选 | +| `breeding_method_id` | 该材料形成所使用的育种方法 | 从 breeding_method 选择 | 搜索选择器 | 可选;必须引用存在的 breeding_method | +| `crop_id` | 所属作物 | 从 crop 选择 | 作物选择器 | 必填;后续 trial/study/attribute 应尽量同 crop | + +## 录入建议 + +- 新建材料时,第一屏只放核心字段:`crop_id`、`germplasm_name`、`default_display_name`、`germplasmpui`、`accession_number`、`breeding_method_id`。 +- 分类与来源信息放在“高级信息”或“来源信息”分组。 +- `germplasmpui`、`accession_number`、`germplasm_name` 三者不要混为一谈: + - `germplasm_name` 是人看的名字; + - `accession_number` 是机构内编号; + - `germplasmpui` 是跨系统长期唯一标识。 + +## 验收点 + +1. 新增 germplasm 时,必须选择 crop。 +2. `germplasm_name` 和 `default_display_name` 至少填写一个。 +3. 下拉选择材料时展示 `default_display_name`,辅助展示 accession number / PUI。 +4. 如果 germplasm 已被 seed lot、cross parent、observation unit 引用,不允许物理删除。 + +--- + diff --git a/docs/dev/02-germplasm-seed/03-germplasm_attribute_definition.md b/docs/dev/02-germplasm-seed/03-germplasm_attribute_definition.md new file mode 100644 index 0000000..d36c314 --- /dev/null +++ b/docs/dev/02-germplasm-seed/03-germplasm_attribute_definition.md @@ -0,0 +1,48 @@ +# 02 Germplasm / Seed - germplasm_attribute_definition 表录入说明 + +来源:`docs/requirements/02-germplasm-seed-entry-requirements.md` + +## 录入目标 + +属性定义描述“材料可以有哪些稳定属性”。这些属性通常不是环境依赖的田间观测值,而是材料自身特征,例如籽粒颜色、抗病基因、硬度、熟期类型、特定 QTL、分子标记结果等。 + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ---------------------- | ------------------------------------------------------------ | ---------------------------- | ----------------- | ------------------------------------- | +| `id` | 属性定义主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `default_value` | 属性默认值 | 用户录入 | 动态输入框 | 可选;按 datatype / scale 校验 | +| `documentationurl` | 属性说明文档链接 | 用户录入 | URL 输入框 | 可选;校验 URL | +| `growth_stage` | 属性适用生长阶段,如 flowering | 用户录入/选择 | 下拉框/文本框 | 可选 | +| `institution` | 提交或维护该属性定义的机构 | 用户录入 | 文本框 | 可选 | +| `language` | 定义语言,如 zh、en | 用户选择 | 下拉框 | 可选;建议 ISO 639-1 | +| `scientist` | 提交该属性定义的科学家或负责人 | 用户录入/人员选择 | 文本框/人员选择器 | 可选 | +| `status` | 属性状态,如 recommended、obsolete、legacy | 用户选择 | 下拉框 | 可选;推荐使用枚举 | +| `submission_timestamp` | 属性定义提交时间 | 系统默认当前时间,可手动调整 | 日期时间选择器 | 可选;新增默认当前时间 | +| `crop_id` | 适用作物 | 从 crop 选择 | 作物选择器 | 可选;若填写,下游材料应同 crop | +| `method_id` | 属性测定方法 | 从 method 选择 | 方法选择器 | 可选;若填写,属性值录入按该方法解释 | +| `ontology_id` | 所属本体 | 从 ontology 选择 | 本体选择器 | 可选 | +| `scale_id` | 值标尺/单位/有效值范围 | 从 scale 选择 | 标尺选择器 | 可选;若填写,属性值必须按 scale 校验 | +| `trait_id` | 关联性状 | 从 trait 选择 | 性状选择器 | 可选 | +| `attribute_category` | 属性分类,如 Morphological、Genetic、Quality | 用户选择/录入 | 下拉框/文本框 | 可选;建议字典化 | +| `code` | 属性代码,便于导入导出 | 用户录入 | 文本框 | 可选;建议唯一 | +| `datatype` | 属性值数据类型,如 text、numeric、date、boolean、categorical | 用户选择 | 下拉框 | 必填 | +| `description` | 属性解释 | 用户录入 | 多行文本 | 可选 | +| `name` | 属性名称 | 用户录入 | 文本框 | 必填;作为属性选择器展示名称 | +| `pui` | 属性永久标识 | 用户录入 | 文本框/URL 输入框 | 可选;建议唯一 | +| `uri` | 属性 URI | 用户录入 | URL 输入框 | 可选;校验 URL | + +## 录入建议 + +- 属性定义页面本质是“属性字典配置”。 +- 前端应根据 `datatype` 动态决定属性值录入控件: + - numeric:数字输入框; + - categorical:下拉框; + - date:日期选择器; + - boolean:开关; + - text:文本框。 +- 若绑定了 `scale_id`,则优先按 scale 的单位、上下限、有效分类值校验。 + +--- + diff --git a/docs/dev/02-germplasm-seed/04-germplasm_attribute_value.md b/docs/dev/02-germplasm-seed/04-germplasm_attribute_value.md new file mode 100644 index 0000000..4dc5c24 --- /dev/null +++ b/docs/dev/02-germplasm-seed/04-germplasm_attribute_value.md @@ -0,0 +1,27 @@ +# 02 Germplasm / Seed - germplasm_attribute_value 表录入说明 + +来源:`docs/requirements/02-germplasm-seed-entry-requirements.md` + +## 录入目标 + +属性值是“某个 germplasm 在某个属性上的实际取值”。它不是属性定义,也不是 observation。它适合记录材料相对稳定、不强依赖环境的特征。 + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ----------------- | ------------------------------ | -------------------------------------- | ---------- | ---------------------------------------- | +| `id` | 属性值主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `determined_date` | 属性值被测定或确认的日期 | 用户录入 | 日期选择器 | 可选;多次测定时必须填写以区分记录 | +| `value` | 某个材料在某个属性上的实际取值 | 用户录入 | 动态控件 | 必填;按 attribute datatype / scale 校验 | +| `attribute_id` | 属性定义 | 从 germplasm_attribute_definition 选择 | 属性选择器 | 必选;必须存在 | +| `germplasm_id` | 所属材料 | 从 germplasm 选择 | 材料选择器 | 必选;必须存在 | + +## 录入建议 + +- 推荐嵌入 Germplasm 详情页的“属性值”Tab。 +- 支持批量导入,模板列建议为:`germplasm_id/germplasm_name`、`attribute_code/attribute_name`、`value`、`determined_date`。 +- 同一个 germplasm + attribute 可以允许多次测定,但页面必须显示测定日期、来源和最新值标记。 + +--- + diff --git a/docs/dev/02-germplasm-seed/05-crossing_project.md b/docs/dev/02-germplasm-seed/05-crossing_project.md new file mode 100644 index 0000000..f0dfae9 --- /dev/null +++ b/docs/dev/02-germplasm-seed/05-crossing_project.md @@ -0,0 +1,25 @@ +# 02 Germplasm / Seed - crossing_project 表录入说明 + +来源:`docs/requirements/02-germplasm-seed-entry-requirements.md` + +## 录入目标 + +`crossing_project` 是某个育种项目下的一组杂交任务集合。它不是一次杂交,而是一个杂交工作台,例如“2026 抗倒伏杂交项目”。 + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| -------------- | ------------------ | ------------------ | ---------- | ----------------------------- | +| `id` | 杂交项目主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `description` | 杂交项目说明 | 用户录入 | 多行文本 | 可选 | +| `name` | 杂交项目名称 | 用户录入 | 文本框 | 必填;同一 program 下建议唯一 | +| `program_id` | 所属育种项目 | 从 program 选择 | 项目选择器 | 必选;必须存在 | + +## 页面与交互 + +- 详情页应展示计划杂交、实际杂交、潜在亲本、后代材料、产生的 seed lot。 +- 创建 cross 时应自动带入 crossing_project_id。 + +--- + diff --git a/docs/dev/02-germplasm-seed/06-cross_entity.md b/docs/dev/02-germplasm-seed/06-cross_entity.md new file mode 100644 index 0000000..d200e1d --- /dev/null +++ b/docs/dev/02-germplasm-seed/06-cross_entity.md @@ -0,0 +1,30 @@ +# 02 Germplasm / Seed - cross_entity 表录入说明 + +来源:`docs/requirements/02-germplasm-seed-entry-requirements.md` + +## 录入目标 + +`cross_entity` 统一承载计划杂交和实际杂交。通过 `planned` 字段区分计划与实际,通过 `planned_cross_id` 指向来源计划。 + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| --------------------- | ------------------------------------------- | ------------------------ | -------------- | --------------------------------------- | +| `id` | cross 主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `cross_type` | 杂交类型,如 biparental、self、backcross 等 | 用户选择 | 下拉框 | 可选;值来自枚举字典 | +| `name` | cross 名称,如 A × B、A/B、Cross-2026-001 | 用户录入或自动生成 | 文本框 | 必填;同一 crossing project 下建议唯一 | +| `planned` | 是否为计划杂交 | 页面根据入口自动设置 | 开关/分段控件 | 必填;计划杂交为 true,实际杂交为 false | +| `status` | 状态,如 TODO、DONE、SKIPPED、FAILED | 用户选择/系统更新 | 下拉框 | 可选;计划杂交常用 TODO/DONE/SKIPPED | +| `crossing_project_id` | 所属杂交项目 | 从 crossing_project 选择 | 杂交项目选择器 | 必选 | +| `planned_cross_id` | 实际杂交来源的计划杂交 | 从 cross_entity 选择 | Cross 选择器 | 可选;不能选择自己;实际杂交建议填写 | + +## 录入建议 + +- 页面上分成“计划杂交”和“实际杂交”两个入口,但后端都保存到 `cross_entity`。 +- 创建计划杂交时:`planned=true`,`planned_cross_id=null`。 +- 完成实际杂交时:`planned=false`,`planned_cross_id=原计划杂交 id`。 +- 亲本不要直接塞在 cross 主表字段中,应通过 `cross_parent` 维护,便于支持多亲本和 observation_unit 亲本来源。 + +--- + diff --git a/docs/dev/02-germplasm-seed/07-cross_parent.md b/docs/dev/02-germplasm-seed/07-cross_parent.md new file mode 100644 index 0000000..7efa283 --- /dev/null +++ b/docs/dev/02-germplasm-seed/07-cross_parent.md @@ -0,0 +1,26 @@ +# 02 Germplasm / Seed - cross_parent 表录入说明 + +来源:`docs/requirements/02-germplasm-seed-entry-requirements.md` + +## 录入目标 + +`cross_parent` 表示某个 cross 的亲本。亲本可以来自 `germplasm`,也可以来自某个 `observation_unit`,例如田间某一株实际被选作父本/母本。 + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| --------------------- | --------------------------------------------------- | ---------------------------------- | ----------------- | ------------------------------------- | +| `id` | 亲本记录主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `parent_type` | 亲本角色,如 MALE、FEMALE、SELF、POPULATION、CLONAL | 用户选择 | 下拉框 | 必填;使用枚举 | +| `cross_id` | 所属 cross | 从 cross_entity 选择或由详情页带入 | Cross 选择器/隐藏 | 必选 | +| `crossing_project_id` | 所属 crossing project | 由 cross 自动带出 | 只读/隐藏 | 可选;如填写必须与 cross 一致 | +| `germplasm_id` | 亲本材料 | 从 germplasm 选择 | 材料选择器 | 与 `observation_unit_id` 至少一个必填 | +| `observation_unit_id` | 亲本观测单元 | 从 observation_unit 选择 | 观测单元选择器 | 与 `germplasm_id` 至少一个必填 | + +## 录入建议 + +- 在 Cross 详情页内嵌“亲本列表”。 +- 常见快捷录入:Parent1 / Parent2。 +- 对于田间选株杂交,优先记录 observation_unit_id,同时可带出 germplasm_id,保证可追溯到具体植株。 + +--- \ No newline at end of file diff --git a/docs/dev/02-germplasm-seed/08-pedigree_node.md b/docs/dev/02-germplasm-seed/08-pedigree_node.md new file mode 100644 index 0000000..842d0ae --- /dev/null +++ b/docs/dev/02-germplasm-seed/08-pedigree_node.md @@ -0,0 +1,28 @@ +# 02 Germplasm / Seed - pedigree_node 表录入说明 + +来源:`docs/requirements/02-germplasm-seed-entry-requirements.md` + +## 录入目标 + +`pedigree_node` 是系谱图中的节点,通常对应一个 germplasm。它用于描述材料在系谱树中的位置,不等同于一次杂交记录。 + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| --------------------- | --------------------- | ------------------------ | -------------- | ----------------------------------------- | +| `id` | 系谱节点主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `crossing_year` | 亲本最初杂交年份 | 用户录入 | 年份选择器 | 可选;四位年份 | +| `family_code` | 家系编号 | 用户录入 | 文本框 | 可选;同一 crossing_project 下建议唯一 | +| `pedigree_string` | 系谱字符串,如 A/B//C | 用户录入/系统生成 | 文本框 | 可选;建议支持 Purdy notation | +| `crossing_project_id` | 产生该节点的杂交项目 | 从 crossing_project 选择 | 杂交项目选择器 | 可选 | +| `germplasm_id` | 该系谱节点对应的材料 | 从 germplasm 选择 | 材料选择器 | 建议必填;同一 germplasm 不建议重复建节点 | + +## 录入建议 + +- Germplasm 详情页提供“系谱”Tab。 +- 支持两种维护方式:树图拖拽维护、表格维护节点和边。 +- 如果 cross 完成后产生后代 germplasm,应自动或半自动创建 pedigree_node。 + +--- + diff --git a/docs/dev/02-germplasm-seed/09-pedigree_edge.md b/docs/dev/02-germplasm-seed/09-pedigree_edge.md new file mode 100644 index 0000000..b8ad763 --- /dev/null +++ b/docs/dev/02-germplasm-seed/09-pedigree_edge.md @@ -0,0 +1,27 @@ +# 02 Germplasm / Seed - pedigree_edge 表录入说明 + +来源:`docs/requirements/02-germplasm-seed-entry-requirements.md` + +## 录入目标 + +`pedigree_edge` 是系谱图中的边,描述节点之间的父子、同胞等关系。 + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------- | ------------------------------------------ | --------------------- | ---------- | ------------------------------------ | +| `id` | 系谱边主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `edge_type` | 边类型,如 parent、child、sibling | 用户选择 | 下拉框 | 必填 | +| `parent_type` | 如果是亲本关系,标识 MALE、FEMALE、SELF 等 | 用户选择 | 下拉框 | 可选;当 edge_type=parent 时建议必填 | +| `connceted_node_id` | 被连接节点 | 从 pedigree_node 选择 | 节点选择器 | 必选 | +| `this_node_id` | 当前节点 | 从 pedigree_node 选择 | 节点选择器 | 必选;不能等于 connected node | + +## 录入建议 + +- 前端展示时不要暴露“this_node_id / connected_node_id”这种技术词,应该显示为“当前材料”和“关联材料”。 +- 添加父本/母本时,系统自动创建 edge_type=parent。 +- 需要避免明显循环,例如 A 是 B 的父本,同时 B 又是 A 的父本。 + +--- + diff --git a/docs/dev/02-germplasm-seed/10-seed_lot.md b/docs/dev/02-germplasm-seed/10-seed_lot.md new file mode 100644 index 0000000..13b7f76 --- /dev/null +++ b/docs/dev/02-germplasm-seed/10-seed_lot.md @@ -0,0 +1,33 @@ +# 02 Germplasm / Seed - seed_lot 表录入说明 + +来源:`docs/requirements/02-germplasm-seed-entry-requirements.md` + +## 录入目标 + +`seed_lot` 是实物库存批次,描述某一批种子当前有多少、放在哪里、属于哪个项目。它不是 germplasm 身份;同一个 germplasm 可以有多个 seed_lot。 + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------- | ---------------------------------------------------- | --------------------- | ------------------- | ----------------------------- | +| `id` | SeedLot 主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `amount` | 当前库存数量,可以是粒数、重量、株数等 | 用户录入/交易自动更新 | 数字输入框 | 必填;非负;交易后自动更新 | +| `created_date` | 批次创建时间 | 系统默认,可导入 | 日期时间选择器/只读 | 默认当前时间 | +| `description` | 批次说明 | 用户录入 | 多行文本 | 可选 | +| `last_updated` | 最后更新时间,包含交易变化 | 系统自动更新 | 只读 | 不允许手动改 | +| `name` | 批次名称,如 华占-2026-荆门-扩繁批 | 用户录入或自动生成 | 文本框 | 必填;同一 program 下建议唯一 | +| `source_collection` | 原始来源 collection,如野外采集、nursery、种质库集合 | 用户录入 | 文本框 | 可选 | +| `storage_location` | 具体库位描述,如 冰箱A-2层-盒03 | 用户录入 | 文本框 | 可选 | +| `units` | 数量单位,如 seeds、g、kg、plants | 用户选择 | 下拉框/文本框 | 必填;交易单位需一致或可换算 | +| `location_id` | 库存所在地点 | 从 location 选择 | 地点选择器 | 可选 | +| `program_id` | 所属项目 | 从 program 选择 | 项目选择器 | 可选;用于项目库存筛选 | + +## 录入建议 + +- 创建 seed_lot 后必须进入“批次组成”Tab,至少录入一条 `seed_lot_content_mixture`。 +- 普通用户不要直接编辑 amount;amount 应通过入库、出库、转移、分装等交易动作更新。 +- 支持库存状态:充足、低库存、耗尽,可由 amount 和阈值计算。 + +--- + diff --git a/docs/dev/02-germplasm-seed/11-seed_lot_content_mixture.md b/docs/dev/02-germplasm-seed/11-seed_lot_content_mixture.md new file mode 100644 index 0000000..4d7d3de --- /dev/null +++ b/docs/dev/02-germplasm-seed/11-seed_lot_content_mixture.md @@ -0,0 +1,26 @@ +# 02 Germplasm / Seed - seed_lot_content_mixture 表录入说明 + +来源:`docs/requirements/02-germplasm-seed-entry-requirements.md` + +## 录入目标 + +`seed_lot_content_mixture` 描述一个 seed_lot 由哪些材料或 cross 组成。单一材料批次也需要有一条组成记录,比例为 100%。混合批次则多条记录占比合计为 100%。 + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| -------------------- | ----------------------------- | -------------------- | ------------------- | ---------------------------------------- | +| `id` | 批次组成主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `mixture_percentage` | 该材料或 cross 在批次中的占比 | 用户录入 | 百分比输入框 | 0 到 100;同一 seed lot 总和建议等于 100 | +| `cross_id` | 来源 cross | 从 cross_entity 选择 | Cross 选择器 | 与 `germplasm_id` 至少一个必填 | +| `germplasm_id` | 来源 germplasm | 从 germplasm 选择 | 材料选择器 | 与 `cross_id` 至少一个必填 | +| `seed_lot_id` | 所属 seed lot | 由详情页带入或选择 | SeedLot 选择器/隐藏 | 必选 | + +## 录入建议 + +- 新建 seed_lot 时,如果用户选择了单个 germplasm,系统自动生成一条 mixture:`germplasm_id=所选材料`,`mixture_percentage=100`。 +- 如果来源是某次杂交产生的种子,优先填写 `cross_id`。 +- 如果既能追溯 cross 又能追溯 germplasm,可按系统设计决定是否允许同时填写;若允许,同时展示“来源杂交”和“当前材料身份”。 + +--- + diff --git a/docs/dev/02-germplasm-seed/12-seed_lot_transaction.md b/docs/dev/02-germplasm-seed/12-seed_lot_transaction.md new file mode 100644 index 0000000..fd6d74e --- /dev/null +++ b/docs/dev/02-germplasm-seed/12-seed_lot_transaction.md @@ -0,0 +1,42 @@ +# 02 Germplasm / Seed - seed_lot_transaction 表录入说明 + +来源:`docs/requirements/02-germplasm-seed-entry-requirements.md` + +## 录入目标 + +`seed_lot_transaction` 记录库存变化。它不应该由用户像普通表单一样手动维护,而应该由“入库、出库、转移、分装、合并、消耗”等业务动作自动生成。 + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------ | -------------------------------------------- | ----------------------- | ------------------- | ------------------------------------------------- | +| `id` | 流转记录主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `amount` | 流转数量 | 用户在业务动作中录入 | 数字输入框 | 必填;大于 0 | +| `description` | 流转说明,如用于某 study、分装原因、报废原因 | 用户录入 | 多行文本 | 可选;出库/报废建议必填 | +| `timestamp` | 流转发生时间 | 默认当前时间,可调整 | 日期时间选择器 | 必填 | +| `units` | 流转单位 | 默认继承 seed_lot.units | 下拉框/只读 | 必填;需与 seed_lot 单位一致或有换算关系 | +| `from_seed_lot_id` | 来源批次 | 按动作自动设置 | SeedLot 选择器/隐藏 | 与 `to_seed_lot_id` 至少一个存在 | +| `to_seed_lot_id` | 目标批次 | 按动作自动设置 | SeedLot 选择器/隐藏 | 与 `from_seed_lot_id` 至少一个存在;不能等于 from | + +## 业务动作映射 + +| 动作 | from_seed_lot_id | to_seed_lot_id | amount 对库存影响 | +| --------- | ---------------- | -------------- | -------------------------------------------- | +| 入库 | 空 | 目标批次 | 目标批次增加 | +| 出库 | 来源批次 | 空 | 来源批次减少 | +| 转移 | 来源批次 | 目标批次 | 来源减少,目标增加 | +| 分装 | 原批次 | 新批次 | 原批次减少,新批次增加 | +| 合并 | 多个来源批次 | 目标批次 | 来源减少,目标增加;可能生成多条 transaction | +| 消耗/报废 | 来源批次 | 空 | 来源减少,并记录原因 | + +## 验收点 + +1. amount 必须大于 0。 +2. 出库/消耗时,amount 不得超过来源批次当前库存。 +3. from 和 to 不能相同。 +4. transaction 创建后应自动更新 seed_lot.amount 和 last_updated。 +5. 已生成的 transaction 原则上不允许随意修改;如需纠错,应通过反向交易或更正记录处理。 + +--- + diff --git a/docs/dev/03-genotyping/01-plate.md b/docs/dev/03-genotyping/01-plate.md new file mode 100644 index 0000000..888776d --- /dev/null +++ b/docs/dev/03-genotyping/01-plate.md @@ -0,0 +1,48 @@ +# 03 Genotyping - plate 表录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +## 录入目标 + +`plate` 表示样本板,是批量送检和测序前的样本组织单元。一个 plate 可以包含多个 `sample`,也可以关联 vendor submission。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `plate` | +| Java 实体 | `PlateEntity` | +| 前置依赖 | `program`、`trial`、`study`,可选 `plate_submission` | +| 下游引用 | `sample` | +| API | `/brapi/v2/plates` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | plate 主键 | 系统生成;导入时可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `plate_name` | 样本板名称 | 用户录入 | 建议必填,同一 study 下建议唯一 | +| `plate_barcode` | 样本板条码 | 扫码/手填 | 建议唯一 | +| `client_plate_db_id` | 客户侧 plate ID | 用户录入/导入 | 可选 | +| `client_plate_barcode` | 客户侧 plate 条码 | 用户录入/导入 | 可选 | +| `plate_format` | plate 规格,如 96/384 孔板 | 枚举选择 | 可选;与样本孔位校验联动 | +| `sample_type` | plate 中样本类型 | 枚举选择 | 可选 | +| `sample_submission_format` | 提交给 vendor 的样本板格式 | 枚举选择 | 可选 | +| `status_time_stamp` | 状态时间 | 系统写入或导入 | 可选 | +| `program_id` | 所属项目 | 项目选择器 | 可选;若填需存在 | +| `trial_id` | 所属 trial | Trial 选择器 | 可选;若填需存在 | +| `study_id` | 所属 study | Study 选择器 | 可选;若填需存在 | +| `submission_id` | vendor plate submission | Vendor submission 选择器 | 可选 | + +## 页面与交互 + +- Plate 列表页支持按 program、trial、study、barcode、plateName 查询。 +- 详情页展示 plate 基本信息和下属 sample 列表。 +- 新增 sample 时如果从 plate 详情进入,默认带出 `plate_id`。 + +## 关键校验 + +1. 如果录入 `program_id/trial_id/study_id`,需要校验 Core 上下文一致性。 +2. 如果使用 `plate_format`,样本 `plate_row/plate_column/well` 不能超出规格。 +3. 已有关联 `sample` 的 plate 删除前必须提示,通常只允许停用或先迁移样本。 diff --git a/docs/dev/03-genotyping/02-sample.md b/docs/dev/03-genotyping/02-sample.md new file mode 100644 index 0000000..95c794d --- /dev/null +++ b/docs/dev/03-genotyping/02-sample.md @@ -0,0 +1,56 @@ +# 03 Genotyping - sample 表录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +## 录入目标 + +`sample` 是 Genotyping 流程的样本入口,表示送检样本或测序样本。它可以挂到 `plate`,也可以直接关联 `observation_unit`,并冗余保存 program/trial/study 方便查询。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `sample` | +| Java 实体 | `SampleEntity` | +| 前置依赖 | `plate`、`observation_unit`、`program`、`trial`、`study`,可选 `germplasm_taxon` | +| 下游引用 | `callset` | +| API | `/brapi/v2/samples` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | sample 主键 | 系统生成;导入时可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `sample_name` | 样本名称 | 用户录入/批量导入 | 建议必填 | +| `sample_barcode` | 样本条码 | 扫码/手填 | 建议唯一 | +| `samplepui` | 样本永久标识 | 用户录入/导入 | 可选,建议唯一 | +| `sample_description` | 样本说明 | 用户录入 | 可选 | +| `sample_type` | 样本类型 | 下拉/文本 | 可选 | +| `tissue_type` | 组织类型 | 下拉/文本 | 可选 | +| `sample_timestamp` | 取样时间 | 日期时间选择器 | 可选 | +| `taken_by` | 取样人 | 文本/人员选择 | 可选 | +| `sample_group_db_id` | 样本分组 ID | 文本/导入 | 可选 | +| `concentration` | 样本浓度 | 文本/数值 | 可选,建议带单位 | +| `volume` | 样本体积 | 文本/数值 | 可选,建议带单位 | +| `plate_id` | 所在样本板 | Plate 选择器 | 可选,若填需存在 | +| `plate_row` | 板行 | 文本 | 与 plate format 联动校验 | +| `plate_column` | 板列 | 数字输入 | 与 plate format 联动校验 | +| `well` | 孔位,如 A01 | 文本/自动生成 | 同一 plate 内建议唯一 | +| `observation_unit_id` | 对应观测单元 | ObservationUnit 选择器 | 可选,若填需存在 | +| `program_id` | 所属项目 | 项目选择器 | 可选,若填需存在 | +| `trial_id` | 所属 trial | Trial 选择器 | 可选,若填需存在 | +| `study_id` | 所属 study | Study 选择器 | 可选,若填需存在 | +| `taxon_id_id` | germplasm taxon | Taxon 选择器 | 可选 | + +## 页面与交互 + +- Sample 列表页支持按 sampleName、barcode、plate、study、trial、program、observationUnit 查询。 +- 从 Study 工作台创建 sample 时默认继承 `study_id/trial_id/program_id`。 +- 从 ObservationUnit 详情创建 sample 时默认带出 `observation_unit_id`。 + +## 关键校验 + +1. `sample` 若关联 `observation_unit`,需要检查 observation unit 所属 study 与 sample 的 study 一致。 +2. 同一 plate 内 `well` 不应重复。 +3. 删除 sample 前必须检查是否已有 `callset`。 diff --git a/docs/dev/03-genotyping/03-reference_set.md b/docs/dev/03-genotyping/03-reference_set.md new file mode 100644 index 0000000..3ee7ea3 --- /dev/null +++ b/docs/dev/03-genotyping/03-reference_set.md @@ -0,0 +1,44 @@ +# 03 Genotyping - reference_set 表录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +## 录入目标 + +`reference_set` 表示参考基因组集合,是 reference、variantset、variant 的参考侧上游。它可以关联来源 germplasm。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `reference_set` | +| Java 实体 | `ReferenceSetEntity` | +| 前置依赖 | 可选 `germplasm` | +| 下游引用 | `reference`、`variantset`、`variant` | +| API | `/brapi/v2/referencesets` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | reference set 主键 | 系统生成;导入时可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `reference_set_name` | 参考集合名称 | 用户录入 | 建议必填 | +| `assemblypui` | assembly 永久标识 | 用户录入/导入 | 可选,建议唯一 | +| `description` | 参考集合说明 | 多行文本 | 可选 | +| `is_derived` | 是否派生参考 | 开关 | 可选 | +| `md5checksum` | 校验值 | 文本 | 可选,格式提示 | +| `sourceuri` | 来源 URI | URL 输入 | 可选,URL 格式校验 | +| `species_ontology_term` | 物种本体 term | 文本/本体选择器 | 可选 | +| `species_ontology_termuri` | 物种本体 URI | URL 输入 | 可选,URL 格式校验 | +| `source_germplasm_id` | 来源 germplasm | Germplasm 选择器 | 可选,若填需存在 | + +## 页面与交互 + +- 列表页展示 referenceSetName、assemblyPUI、species、sourceGermplasm、reference 数、variantset 数。 +- 详情页展示 reference、variantset、variant 入口。 + +## 关键校验 + +1. 删除 reference_set 前检查 `reference`、`variantset`、`variant` 引用。 +2. 若填写 `source_germplasm_id`,必须引用已存在 germplasm。 +3. md5 checksum 建议做格式提示,不强行阻断历史数据。 diff --git a/docs/dev/03-genotyping/04-reference.md b/docs/dev/03-genotyping/04-reference.md new file mode 100644 index 0000000..276a44e --- /dev/null +++ b/docs/dev/03-genotyping/04-reference.md @@ -0,0 +1,40 @@ +# 03 Genotyping - reference 表录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +## 录入目标 + +`reference` 表示具体参考序列,例如 chromosome、contig 或 scaffold。它属于一个 `reference_set`,下游可以分页保存序列片段。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `reference` | +| Java 实体 | `ReferenceEntity` | +| 前置依赖 | `reference_set` | +| 下游引用 | `reference_bases` | +| API | `/brapi/v2/references` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | reference 主键 | 系统生成;导入时可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `reference_name` | 参考序列名称 | 用户录入 | 建议必填 | +| `reference_set_id` | 所属 reference_set | ReferenceSet 选择器 | 必选,必须存在 | +| `length` | 序列长度 | 数字输入/导入 | 可选,非负 | +| `md5checksum` | 序列校验值 | 文本 | 可选 | +| `source_divergence` | 与来源差异度 | 数字输入 | 可选 | + +## 页面与交互 + +- Reference 列表页支持按 referenceSet、referenceName 查询。 +- Reference 详情页展示 reference_bases 分页和长度、checksum 信息。 + +## 关键校验 + +1. `reference_set_id` 必须存在。 +2. 删除 reference 前检查 `reference_bases`。 +3. 若维护 `reference_bases`,建议校验分页总长度与 `length` 的一致性。 diff --git a/docs/dev/03-genotyping/05-reference_bases.md b/docs/dev/03-genotyping/05-reference_bases.md new file mode 100644 index 0000000..64f198f --- /dev/null +++ b/docs/dev/03-genotyping/05-reference_bases.md @@ -0,0 +1,37 @@ +# 03 Genotyping - reference_bases 表录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +## 录入目标 + +`reference_bases` 保存 reference 的序列片段或分页内容。它是参考序列的明细表,通常通过导入或后端任务写入。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `reference_bases` | +| Java 实体 | `ReferenceBasesPageEntity` | +| 前置依赖 | `reference` | +| 下游引用 | 无 | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | reference bases 主键 | 系统生成;导入时可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `reference_id` | 所属 reference | Reference 选择器/导入 | 必选,必须存在 | +| `page_number` | 分页序号 | 数字输入/导入 | 同一 reference 内建议唯一 | +| `bases` | 碱基序列片段 | 文本/文件导入 | 最大 2048 字符;建议仅允许 A/C/G/T/N 等字符 | + +## 页面与交互 + +- 一般不单独提供复杂 CRUD,可在 Reference 详情页查看或导入。 +- 长序列建议走文件导入或异步任务,不建议手工逐页录入。 + +## 关键校验 + +1. `reference_id` 必须存在。 +2. `bases` 长度不能超过数据库字段限制。 +3. 同一 reference 下 `page_number` 不应重复。 diff --git a/docs/dev/03-genotyping/06-variantset.md b/docs/dev/03-genotyping/06-variantset.md new file mode 100644 index 0000000..467af98 --- /dev/null +++ b/docs/dev/03-genotyping/06-variantset.md @@ -0,0 +1,39 @@ +# 03 Genotyping - variantset 表录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +## 录入目标 + +`variantset` 表示一批 variant 的集合,通常对应一次测序、芯片、DArTSeq 或某个 study 下的位点集合。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `variantset` | +| Java 实体 | `VariantSetEntity` | +| 前置依赖 | `reference_set`、`study` | +| 下游引用 | `variant`、`callset_variant_sets`、`variantset_analysis`、`variantset_format` | +| API | `/brapi/v2/variantsets` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | variantset 主键 | 系统生成;导入时可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `variant_set_name` | 变异集合名称 | 用户录入/导入 | 建议必填 | +| `reference_set_id` | 参考基因组集合 | ReferenceSet 选择器 | 建议必选,若填需存在 | +| `study_id` | 所属 study | Study 选择器 | 可选,若填需存在 | + +## 页面与交互 + +- VariantSet 列表页支持按 referenceSet、study、variantSetName 查询。 +- 详情页展示 variants、callsets、analysis、available formats。 +- 从 Study 工作台创建时默认带出 `study_id`。 + +## 关键校验 + +1. `reference_set_id` 与下属 `variant.reference_set_id` 应保持一致。 +2. 删除 variantset 前检查 `variant`、`callset_variant_sets`、`variantset_analysis`、`variantset_format`。 +3. 导入大型 variantset 时建议先建 variantset,再异步导入 variants 和 calls。 diff --git a/docs/dev/03-genotyping/07-variant.md b/docs/dev/03-genotyping/07-variant.md new file mode 100644 index 0000000..9ef6a36 --- /dev/null +++ b/docs/dev/03-genotyping/07-variant.md @@ -0,0 +1,57 @@ +# 03 Genotyping - variant 表录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +## 录入目标 + +`variant` 表示具体变异位点,例如 SNP、Indel 或结构变异。它是位点定义,不是某个样本的结果;样本上的 genotype 结果写入 `allele_call`。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `variant` | +| Java 实体 | `VariantEntity` | +| 前置依赖 | `reference_set`、`variantset` | +| 下游引用 | `allele_call`、`marker_position` | +| API | `/brapi/v2/variants` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | variant 主键 | 系统生成;导入时可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `variant_name` | 位点名称/marker 名称 | 用户录入/导入 | 建议必填 | +| `variant_type` | 位点类型,如 SNP/INDEL/SV | 下拉/导入 | 可选,建议枚举 | +| `reference_set_id` | 参考集合 | ReferenceSet 选择器 | 可选,若填需存在 | +| `variant_set_id` | 所属 variantset | VariantSet 选择器 | 建议必选,若填需存在 | +| `reference_bases` | 参考碱基 | 文本/导入 | 可选 | +| `variant_start` | 起始位置 | 数字输入/导入 | 可选,非负 | +| `variant_end` | 结束位置 | 数字输入/导入 | 可选,不能小于 start | +| `svlen` | 结构变异长度 | 数字输入/导入 | 可选 | +| `filters_applied` | 是否做过过滤 | 开关/导入 | 可选 | +| `filters_passed` | 是否通过过滤 | 开关/导入 | 可选 | +| `created` | 创建时间 | 系统写入/导入 | 可选 | +| `updated` | 更新时间 | 系统写入/导入 | 可选 | + +## 附属集合字段 + +| 附属表 | 内容 | +| --- | --- | +| `variant_entity_alternate_bases` | alternateBases 列表 | +| `variant_entity_ciend` | CIEND 区间 | +| `variant_entity_cipos` | CIPOS 区间 | +| `variant_entity_filters_failed` | 未通过的 filter 列表 | + +## 页面与交互 + +- Variant 列表页支持按 variantSet、referenceSet、variantName、variantType 查询。 +- 大批量位点建议通过文件导入,不建议普通表单逐条录入。 +- 详情页展示 allele_call 数量和 marker_position 入口。 + +## 关键校验 + +1. `variant` 是位点定义,不能把样本 genotype 写在本表。 +2. `variant_set_id` 和 `reference_set_id` 应与所属 variantset 保持一致。 +3. 删除 variant 前检查 `allele_call` 和 `marker_position` 引用。 diff --git a/docs/dev/03-genotyping/08-callset.md b/docs/dev/03-genotyping/08-callset.md new file mode 100644 index 0000000..c4097fb --- /dev/null +++ b/docs/dev/03-genotyping/08-callset.md @@ -0,0 +1,40 @@ +# 03 Genotyping - callset 表录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +## 录入目标 + +`callset` 表示某个 sample 的一组 genotype calls,通常对应一个样本在一个或多个 variantset 上的调用集合。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `callset` | +| Java 实体 | `CallSetEntity` | +| 前置依赖 | `sample` | +| 下游引用 | `allele_call`、`callset_variant_sets` | +| API | `/brapi/v2/callsets` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | callset 主键 | 系统生成;导入时可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `call_set_name` | 调用集合名称 | 用户录入/导入 | 建议必填,同一 sample 下建议唯一 | +| `sample_id` | 所属 sample | Sample 选择器/导入 | 必选,必须存在 | +| `created` | 创建时间 | 系统写入/导入 | 可选 | +| `updated` | 更新时间 | 系统写入/导入 | 可选 | + +## 页面与交互 + +- CallSet 列表页支持按 sample、variantSet、callSetName 查询。 +- 从 Sample 详情创建时默认带出 `sample_id`。 +- 需要通过 `callset_variant_sets` 绑定参与的 variantset。 + +## 关键校验 + +1. `sample_id` 必须存在。 +2. 删除 callset 前检查 `allele_call` 和 `callset_variant_sets`。 +3. 如果 callset 绑定多个 variantset,查询和导出时要明确当前 variantset 范围。 diff --git a/docs/dev/03-genotyping/09-allele_call.md b/docs/dev/03-genotyping/09-allele_call.md new file mode 100644 index 0000000..dd3f986 --- /dev/null +++ b/docs/dev/03-genotyping/09-allele_call.md @@ -0,0 +1,42 @@ +# 03 Genotyping - allele_call 表录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +## 录入目标 + +`allele_call` 是最终 genotype 调用结果。业务上它就是 Call:一条记录表示某个 sample/callset 在某个 variant 上的 genotype、read depth、likelihood 等结果。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `allele_call` | +| Java 实体 | `CallEntity` | +| 前置依赖 | `callset`、`variant` | +| 下游引用 | 无 | +| API | `/brapi/v2/calls` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | call 主键 | 系统生成;导入时可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `call_set_id` | 所属 callset | CallSet 选择器/导入 | 必选,必须存在 | +| `variant_id` | 对应 variant | Variant 选择器/导入 | 必选,必须存在 | +| `genotype` | genotype 值,如 0/1、A/T | 文本/导入 | 建议必填 | +| `read_depth` | 测序深度 | 数字输入/导入 | 可选,非负 | +| `genotype_likelihood` | genotype likelihood | 数字输入/导入 | 可选 | +| `phase_set` | phase set | 文本/导入 | 可选 | + +## 页面与交互 + +- 通常不做逐条手工录入,主要通过 VCF/HapMap/矩阵文件导入。 +- 列表页支持按 callset、sample、variantset、variant 查询。 +- 在 Variant 详情页可查看该位点下的 calls,在 Sample/CallSet 详情页可查看该样本的 calls。 + +## 关键校验 + +1. `call_set_id` 和 `variant_id` 必须存在。 +2. 同一 callset 对同一 variant 不应重复。 +3. `allele_call` 不应直接承担 variant 定义字段;位点信息应来自 `variant`。 diff --git a/docs/dev/03-genotyping/10-genome_map.md b/docs/dev/03-genotyping/10-genome_map.md new file mode 100644 index 0000000..dbb9396 --- /dev/null +++ b/docs/dev/03-genotyping/10-genome_map.md @@ -0,0 +1,45 @@ +# 03 Genotyping - genome_map 表录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +## 录入目标 + +`genome_map` 表示遗传图谱,用于组织 linkage group,并通过 marker_position 将 variant 放到图谱坐标上。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `genome_map` | +| Java 实体 | `GenomeMapEntity` | +| 前置依赖 | `crop`,可关联 `study` | +| 下游引用 | `linkageGroup` / `linkage_group` | +| API | `/brapi/v2/maps` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | map 主键 | 系统生成;导入时可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `map_name` | 图谱名称 | 用户录入 | 建议必填 | +| `mappui` | map 永久标识 | 用户录入/导入 | 可选,建议唯一 | +| `crop_id` | 所属作物 | Crop 选择器 | 可选,若填需存在 | +| `scientific_name` | 学名 | 文本 | 可选 | +| `type` | 图谱类型 | 下拉/文本 | 可选 | +| `unit` | 图谱单位,如 cM | 文本/下拉 | 可选 | +| `published_date` | 发表日期 | 日期选择器 | 可选 | +| `documentationurl` | 文档链接 | URL 输入 | 可选,URL 格式校验 | +| `comments` | 备注 | 多行文本 | 可选 | + +## 页面与交互 + +- GenomeMap 列表页支持按 crop、mapName、type 查询。 +- 详情页展示 linkage groups 和 marker positions。 +- 可通过图谱详情批量导入 linkage group 与 marker position。 + +## 关键校验 + +1. 删除 genome_map 前检查 linkage group 引用。 +2. 若关联 study,应检查 study 的 crop 与 map 的 crop 一致。 +3. `unit` 应与 marker_position 的 position 语义保持一致。 diff --git a/docs/dev/03-genotyping/11-linkage_group.md b/docs/dev/03-genotyping/11-linkage_group.md new file mode 100644 index 0000000..3e81f32 --- /dev/null +++ b/docs/dev/03-genotyping/11-linkage_group.md @@ -0,0 +1,42 @@ +# 03 Genotyping - linkageGroup / linkage_group 表录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +## 命名注意 + +架构文档标注 `LinkageGroupEntity` 对应表名是驼峰 `linkageGroup`。当前初始 schema DDL 中实际表名为 `linkage_group`。做 Atlas/Flyway 迁移时,必须先确认目标数据库中的真实表名,避免生成大小写或命名不一致的重复表。 + +## 录入目标 + +连锁群是 genome_map 下的分组,例如 chromosome 或 linkage group。`marker_position` 会把 variant 放到具体 linkage group 的位置上。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| Java 实体 | `LinkageGroupEntity` | +| 表 | 架构文档:`linkageGroup`;当前 DDL:`linkage_group` | +| 前置依赖 | `genome_map` | +| 下游引用 | `marker_position` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | linkage group 主键 | 系统生成;导入时可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `genome_map_id` | 所属 genome_map | GenomeMap 选择器 | 必选,必须存在 | +| `linkage_group_name` | 连锁群名称 | 用户录入/导入 | 建议必填,同一 map 下唯一 | +| `max_marker_position` | 最大 marker 位置 | 数字输入/导入 | 可选,非负 | + +## 页面与交互 + +- 通常作为 GenomeMap 详情页的子表维护。 +- 支持批量导入 linkage group。 +- 详情页展示该 linkage group 下的 marker_position 列表。 + +## 关键校验 + +1. `genome_map_id` 必须存在。 +2. 同一 genome_map 下 `linkage_group_name` 不应重复。 +3. 删除 linkage group 前检查 `marker_position` 引用。 diff --git a/docs/dev/03-genotyping/12-marker_position.md b/docs/dev/03-genotyping/12-marker_position.md new file mode 100644 index 0000000..a0f2307 --- /dev/null +++ b/docs/dev/03-genotyping/12-marker_position.md @@ -0,0 +1,39 @@ +# 03 Genotyping - marker_position 表录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +## 录入目标 + +`marker_position` 记录 marker/variant 在 linkage group 上的位置,是遗传图谱定位侧与 variant 位点侧的连接表。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `marker_position` | +| Java 实体 | `MarkerPositionEntity` | +| 前置依赖 | `linkageGroup` / `linkage_group`、`variant` | +| 下游引用 | 无 | +| API | `/brapi/v2/markerpositions` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | marker position 主键 | 系统生成;导入时可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `linkage_group_id` | 所属 linkage group | LinkageGroup 选择器/导入 | 必选,必须存在 | +| `variant_id` | 对应 variant | Variant 选择器/导入 | 必选,必须存在 | +| `position` | 图谱位置 | 数字输入/导入 | 必填,非负 | + +## 页面与交互 + +- 可在 GenomeMap/LinkageGroup 详情页批量维护。 +- 列表页支持按 map、linkageGroup、variant 查询。 +- Variant 详情页可展示 marker_position 信息。 + +## 关键校验 + +1. `linkage_group_id` 和 `variant_id` 必须存在。 +2. 同一 linkage group 下同一 variant 不应重复。 +3. `position` 不应超过 linkage group 的 `max_marker_position`。 diff --git a/docs/dev/03-genotyping/13-callset_variant_sets.md b/docs/dev/03-genotyping/13-callset_variant_sets.md new file mode 100644 index 0000000..5d60b2e --- /dev/null +++ b/docs/dev/03-genotyping/13-callset_variant_sets.md @@ -0,0 +1,34 @@ +# 03 Genotyping - callset_variant_sets 表录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +## 录入目标 + +`callset_variant_sets` 是 `callset` 与 `variantset` 的多对多关系表,表示某个样本调用集合覆盖了哪些 variantset。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `callset_variant_sets` | +| 前置依赖 | `callset`、`variantset` | +| 下游引用 | 查询、导出和 allele matrix 范围过滤 | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `call_sets_id` | callset 主键 | CallSet 选择器/导入 | 必选,必须存在 | +| `variant_sets_id` | variantset 主键 | VariantSet 选择器/导入 | 必选,必须存在 | + +## 页面与交互 + +- 通常在 CallSet 详情页或 VariantSet 详情页维护,不建议独立做主菜单。 +- 创建 callset 后可选择一个或多个 variantset。 +- 大批量导入 calls 时可以由导入任务自动创建关系。 + +## 关键校验 + +1. `call_sets_id` 和 `variant_sets_id` 必须存在。 +2. 同一 callset 与 variantset 关系不应重复。 +3. 删除关系不应删除 callset 或 variantset 主数据。 diff --git a/docs/dev/03-genotyping/14-variantset_analysis.md b/docs/dev/03-genotyping/14-variantset_analysis.md new file mode 100644 index 0000000..90f4f9e --- /dev/null +++ b/docs/dev/03-genotyping/14-variantset_analysis.md @@ -0,0 +1,45 @@ +# 03 Genotyping - variantset_analysis 表录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +## 录入目标 + +`variantset_analysis` 记录 variantset 的分析或 QC 信息,例如分析名称、软件、类型、描述和时间。它是 variantset 的附属明细表。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `variantset_analysis` | +| Java 实体 | `VariantSetAnalysisEntity` | +| 前置依赖 | `variantset` | +| 下游引用 | 分析信息展示 | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | analysis 主键 | 系统生成;导入时可指定 | 必填、唯一 | +| `variant_set_id` | 所属 variantset | VariantSet 选择器/导入 | 必选,必须存在 | +| `analysis_name` | 分析名称 | 用户录入 | 建议必填 | +| `description` | 分析说明 | 多行文本 | 可选 | +| `type` | 分析类型,如 QC | 下拉/文本 | 可选 | +| `created` | 创建时间 | 日期时间 | 可选 | +| `updated` | 更新时间 | 日期时间 | 可选 | + +## 附属集合字段 + +| 附属表 | 内容 | +| --- | --- | +| `variant_set_analysis_entity_software` | software 列表,如软件名称、版本或 URL | + +## 页面与交互 + +- 在 VariantSet 详情页以 Analysis Tab 维护。 +- 支持添加多条分析记录,每条可维护多个 software。 + +## 关键校验 + +1. `variant_set_id` 必须存在。 +2. 删除 variantset 时需要先处理或级联处理 analysis。 +3. `software` 如果是 URL,前端可做 URL 格式提示。 diff --git a/docs/dev/03-genotyping/15-variantset_format.md b/docs/dev/03-genotyping/15-variantset_format.md new file mode 100644 index 0000000..8bade35 --- /dev/null +++ b/docs/dev/03-genotyping/15-variantset_format.md @@ -0,0 +1,42 @@ +# 03 Genotyping - variantset_format 表录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +## 录入目标 + +`variantset_format` 记录 variantset 可用的数据格式和文件地址,例如 allele matrix、VCF、HapMap 或 CSV。它用于下载、导出和矩阵读取。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `variantset_format` | +| Java 实体 | `VariantSetAvailableFormatEntity` | +| 前置依赖 | `variantset` | +| 下游引用 | 文件下载、allele matrix 展示、导出 | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | format 主键 | 系统生成;导入时可指定 | 必填、唯一 | +| `variant_set_id` | 所属 variantset | VariantSet 选择器/导入 | 必选,必须存在 | +| `data_format` | 数据格式,如 VCF/HapMap/矩阵 | 枚举选择 | 建议必填 | +| `file_format` | MIME/文件格式 | 枚举选择 | 建议必填 | +| `fileurl` | 文件 URL | URL 输入/导入 | 可选,URL 格式校验 | +| `expand_homozygotes` | 是否展开纯合位点 | 开关 | 可选 | +| `sep_phased` | phased genotype 分隔符 | 文本 | 可选 | +| `sep_unphased` | unphased genotype 分隔符 | 文本 | 可选 | +| `unknown_string` | 缺失值字符串 | 文本 | 可选 | + +## 页面与交互 + +- 在 VariantSet 详情页以 Formats Tab 维护。 +- 支持一组 variantset 配置多个可下载格式。 +- 点击 `fileurl` 可下载或预览,对 allele matrix URL 可进入矩阵查看。 + +## 关键校验 + +1. `variant_set_id` 必须存在。 +2. `fileurl` 如填写需通过 URL 格式校验。 +3. 对矩阵格式,`sep_phased/sep_unphased/unknown_string` 会影响解析,应在导入预览时展示。 diff --git a/docs/dev/03-genotyping/README.md b/docs/dev/03-genotyping/README.md new file mode 100644 index 0000000..0d80d88 --- /dev/null +++ b/docs/dev/03-genotyping/README.md @@ -0,0 +1,45 @@ +# 03 Genotyping 开发录入说明 + +来源:`docs/architecture/03-genotyping-data-flow.md` + +本目录按 Genotyping 模块的数据录入顺序拆分开发说明。主线是: + +```text +Core/Phenotyping 上游数据 -> plate / sample +reference_set -> reference -> reference_bases +reference_set + study -> variantset -> variant +sample -> callset +callset + variantset -> callset_variant_sets +callset + variant -> allele_call +genome_map -> linkageGroup/linkage_group -> marker_position -> variant +``` + +## 文档清单 + +| 顺序 | 文档 | 表 | 作用 | +| --- | --- | --- | --- | +| 01 | `01-plate.md` | `plate` | 样本板 | +| 02 | `02-sample.md` | `sample` | 送检样本/测序样本 | +| 03 | `03-reference_set.md` | `reference_set` | 参考基因组集合 | +| 04 | `04-reference.md` | `reference` | 参考序列 | +| 05 | `05-reference_bases.md` | `reference_bases` | 参考序列片段/分页 | +| 06 | `06-variantset.md` | `variantset` | 变异集合 | +| 07 | `07-variant.md` | `variant` | 变异位点 | +| 08 | `08-callset.md` | `callset` | 样本调用集合 | +| 09 | `09-allele_call.md` | `allele_call` | genotype 调用结果 | +| 10 | `10-genome_map.md` | `genome_map` | 遗传图谱 | +| 11 | `11-linkage_group.md` | `linkageGroup` / `linkage_group` | 连锁群 | +| 12 | `12-marker_position.md` | `marker_position` | marker/variant 图谱位置 | +| 13 | `13-callset_variant_sets.md` | `callset_variant_sets` | callset 与 variantset 关系 | +| 14 | `14-variantset_analysis.md` | `variantset_analysis` | variantset 分析信息 | +| 15 | `15-variantset_format.md` | `variantset_format` | variantset 可用文件/矩阵格式 | + +## 全局注意点 + +1. `CallEntity` 对应真实业务表是 `allele_call`,不要新建 `call` 表。 +2. `CallSetEntity` 对应 `callset`,不是 `call_set`。 +3. `VariantSetEntity` 对应 `variantset`,不是 `variant_set`。 +4. 架构文档标注 `LinkageGroupEntity` 的表名为 `linkageGroup`;当前初始 DDL 中表名为 `linkage_group`。做迁移前必须以实际数据库 catalog 为准。 +5. `sample` 是 Genotyping 样本入口,向上关联 `plate/observation_unit/study/trial/program`。 +6. `variant` 是位点定义,`allele_call` 是某个样本/callset 在位点上的结果。 +7. `reference_set/reference/reference_bases` 是参考基因组侧;`variantset/variant/callset/allele_call` 是变异和结果侧。 diff --git a/docs/dev/04-germplasm/01-breeding_method.md b/docs/dev/04-germplasm/01-breeding_method.md new file mode 100644 index 0000000..c45a1f1 --- /dev/null +++ b/docs/dev/04-germplasm/01-breeding_method.md @@ -0,0 +1,37 @@ +# 04 Germplasm / Seed - breeding_method 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`breeding_method` 是育种方法字典,用来说明 germplasm 是通过什么方式形成的,例如杂交选育、回交、自交系选育、诱变、DH、克隆选择等。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `breeding_method` | +| Java 实体 | `BreedingMethodEntity` | +| 前置依赖 | 无 | +| 下游引用 | `germplasm.breeding_method_id` | +| API | `/brapi/v2/breedingmethods` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | 育种方法主键 | 系统生成;导入可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `name` | 方法名称 | 用户录入 | 必填,建议唯一 | +| `abbreviation` | 方法缩写 | 用户录入 | 可选,建议唯一 | +| `description` | 方法说明 | 多行文本 | 可选 | + +## 页面与交互 + +- 列表页展示 name、abbreviation、description、引用 germplasm 数。 +- 新增/编辑页为简单字典表单。 + +## 关键校验 + +1. `name` 必填。 +2. 已被 `germplasm` 引用时不允许物理删除,只允许停用或提示引用关系。 diff --git a/docs/dev/04-germplasm/02-germplasm.md b/docs/dev/04-germplasm/02-germplasm.md new file mode 100644 index 0000000..b967147 --- /dev/null +++ b/docs/dev/04-germplasm/02-germplasm.md @@ -0,0 +1,68 @@ +# 04 Germplasm / Seed - germplasm 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`germplasm` 是种质主表,保存 accession、PUI、分类、采集、来源、育种方法和展示名称等身份信息。它不是库存批次;库存批次由 `seed_lot` 表达。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `germplasm` | +| Java 实体 | `GermplasmEntity` | +| 前置依赖 | `crop`,可选 `breeding_method` | +| 下游引用 | `germplasm_attribute_value`、`germplasm_donor`、`germplasm_institute`、`germplasm_origin`、`germplasm_synonym`、`germplasm_taxon`、`pedigree_node`、`cross_parent`、`seed_lot_content_mixture` | +| API | `/brapi/v2/germplasm` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | germplasm 主键 | 系统生成;导入可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `germplasm_name` | 种质名称 | 用户录入/导入 | 建议必填 | +| `default_display_name` | 默认展示名称 | 用户录入/自动生成 | 可选,缺省用 germplasm_name | +| `germplasmpui` | 种质永久标识 | 用户录入/导入 | 可选,建议唯一 | +| `accession_number` | accession 编号 | 用户录入/导入 | 可选,建议唯一 | +| `crop_id` | 所属作物 | Crop 选择器 | 建议必选,若填需存在 | +| `breeding_method_id` | 育种方法 | BreedingMethod 选择器 | 可选,若填需存在 | +| `genus` | 属 | 用户录入 | 可选 | +| `species` | 种 | 用户录入 | 可选 | +| `species_authority` | 种命名人 | 用户录入 | 可选 | +| `subtaxa` | 亚种/变种 | 用户录入 | 可选 | +| `subtaxa_authority` | 亚种命名人 | 用户录入 | 可选 | +| `country_of_origin_code` | 来源国家代码 | 国家选择器 | 可选 | +| `collection` | collection | 用户录入 | 可选 | +| `acquisition_date` | 获取日期 | 日期选择器 | 可选 | +| `acquisition_source_code` | 获取来源编码 | 枚举选择 | 可选 | +| `biological_status_of_accession_code` | 生物状态编码 | 枚举选择 | 可选 | +| `mls_status` | MLS 状态 | 枚举选择 | 可选 | +| `germplasm_preprocessing` | 种质预处理说明 | 文本 | 可选 | +| `seed_source` | 种子来源描述字段 | 文本 | 可选,不等于 seed_lot | +| `seed_source_description` | 种子来源补充说明 | 多行文本 | 可选 | +| `documentationurl` | 文档链接 | URL 输入 | 可选,URL 格式校验 | + +## 附属信息 + +| 附属表 | 内容 | +| --- | --- | +| `germplasm_donor` | donor accession、donor institute | +| `germplasm_institute` | host、donor、breeding、collecting、redundant 等机构 | +| `germplasm_origin` | 来源坐标和坐标不确定性 | +| `germplasm_synonym` | 别名及别名类型 | +| `germplasm_taxon` | taxon 来源和值 | +| `germplasm_entity_type_of_germplasm_storage_code` | storage type code 列表 | + +## 页面与交互 + +- Germplasm 列表页支持按 crop、germplasmName、accessionNumber、PUI、synonym 查询。 +- 详情页以 Tab 展示 attributes、donors、institutes、origin、synonyms、taxon、pedigree、seed lots、cross parent 记录。 +- 从 Study/ObservationUnit 或 SeedLot 进入时应能回到 germplasm 详情。 + +## 关键校验 + +1. `germplasm_name` 建议必填,`accession_number` 和 `germplasmpui` 建议唯一。 +2. 删除 germplasm 前必须检查属性值、seed lot 组成、cross parent、pedigree、sample/taxon 等引用。 +3. 不要用 `seed_source` 表达库存;库存必须走 `seed_lot`。 diff --git a/docs/dev/04-germplasm/03-germplasm_donor.md b/docs/dev/04-germplasm/03-germplasm_donor.md new file mode 100644 index 0000000..fa97165 --- /dev/null +++ b/docs/dev/04-germplasm/03-germplasm_donor.md @@ -0,0 +1,39 @@ +# 04 Germplasm / Seed - germplasm_donor 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`germplasm_donor` 记录某个 germplasm 的 donor accession 和 donor institute 信息,是 germplasm 的附属明细。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `germplasm_donor` | +| Java 实体 | `DonorEntity` | +| 前置依赖 | `germplasm` | +| 下游引用 | MCPD 展示、来源追踪 | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | donor 记录主键 | 系统生成;导入可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `germplasm_id` | 所属 germplasm | Germplasm 选择器/详情页带出 | 必选,必须存在 | +| `donor_accession_number` | donor accession 编号 | 用户录入 | 可选 | +| `donor_institute_code` | donor 机构代码 | 用户录入/选择 | 可选 | +| `donor_institute_name` | donor 机构名称 | 用户录入/选择 | 可选 | +| `germplasmpui` | donor germplasm PUI | 用户录入 | 可选 | + +## 页面与交互 + +- 在 Germplasm 详情页 Donor Tab 内维护。 +- 支持一条 germplasm 维护多个 donor 记录。 + +## 关键校验 + +1. `germplasm_id` 必须存在。 +2. 同一 germplasm 下 donor accession + institute code 不建议重复。 +3. 删除 donor 记录不应删除 germplasm 主数据。 diff --git a/docs/dev/04-germplasm/04-germplasm_institute.md b/docs/dev/04-germplasm/04-germplasm_institute.md new file mode 100644 index 0000000..484d152 --- /dev/null +++ b/docs/dev/04-germplasm/04-germplasm_institute.md @@ -0,0 +1,38 @@ +# 04 Germplasm / Seed - germplasm_institute 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`germplasm_institute` 记录 germplasm 相关机构,包括 host、donor、breeding、collecting、redundant 等类型。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `germplasm_institute` | +| Java 实体 | `GermplasmInstituteEntity` | +| 前置依赖 | `germplasm` | +| 下游引用 | Germplasm MCPD、机构筛选 | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | 机构记录主键 | 系统生成 | 必填、唯一 | +| `germplasm_id` | 所属 germplasm | Germplasm 选择器/详情页带出 | 必选,必须存在 | +| `institute_type` | 机构类型 | 枚举选择 | HOST、DONOR、BREEDING、COLLECTING、REDUNDANT | +| `institute_code` | 机构代码 | 用户录入 | 可选 | +| `institute_name` | 机构名称 | 用户录入 | 建议必填 | +| `institute_address` | 机构地址 | 多行文本 | 可选 | + +## 页面与交互 + +- 在 Germplasm 详情页 Institute Tab 内维护。 +- 可把 HOST institute 作为 germplasm 主信息摘要展示。 + +## 关键校验 + +1. `germplasm_id` 必须存在。 +2. 同一 germplasm 下同类型、同 code 的机构不建议重复。 +3. 删除 institute 记录不应删除 germplasm 主数据。 diff --git a/docs/dev/04-germplasm/05-germplasm_origin.md b/docs/dev/04-germplasm/05-germplasm_origin.md new file mode 100644 index 0000000..6b4d029 --- /dev/null +++ b/docs/dev/04-germplasm/05-germplasm_origin.md @@ -0,0 +1,36 @@ +# 04 Germplasm / Seed - germplasm_origin 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`germplasm_origin` 记录 germplasm 的来源地坐标和坐标不确定性,适合表达采集地点或原产地的空间信息。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `germplasm_origin` | +| Java 实体 | `GermplasmOriginEntity` | +| 前置依赖 | `germplasm`,可选 `geojson` | +| 下游引用 | 地理来源展示、MCPD | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | origin 记录主键 | 系统生成 | 必填、唯一 | +| `germplasm_id` | 所属 germplasm | Germplasm 选择器/详情页带出 | 必选,必须存在 | +| `coordinate_uncertainty` | 坐标不确定性 | 文本/数字 | 可选 | +| `coordinates_id` | GeoJSON 坐标对象 | 地图取点/GeoJSON 导入 | 可选,若填需存在 | + +## 页面与交互 + +- 在 Germplasm 详情页 Origin Tab 内维护。 +- 支持地图取点和 GeoJSON 查看。 + +## 关键校验 + +1. `germplasm_id` 必须存在。 +2. 坐标格式需要合法。 +3. 删除 origin 记录不应删除 germplasm 主数据。 diff --git a/docs/dev/04-germplasm/06-germplasm_synonym.md b/docs/dev/04-germplasm/06-germplasm_synonym.md new file mode 100644 index 0000000..b4128f9 --- /dev/null +++ b/docs/dev/04-germplasm/06-germplasm_synonym.md @@ -0,0 +1,36 @@ +# 04 Germplasm / Seed - germplasm_synonym 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`germplasm_synonym` 记录 germplasm 的别名、旧名、商品名或本地名,用于检索和展示。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `germplasm_synonym` | +| Java 实体 | `GermplasmSynonymEntity` | +| 前置依赖 | `germplasm` | +| 下游引用 | Germplasm 搜索、详情展示 | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | synonym 记录主键 | 系统生成 | 必填、唯一 | +| `germplasm_id` | 所属 germplasm | Germplasm 选择器/详情页带出 | 必选,必须存在 | +| `synonym` | 别名 | 用户录入 | 必填 | +| `type` | 别名类型 | 下拉/文本 | 可选,如 local、commercial、old name | + +## 页面与交互 + +- 在 Germplasm 详情页 Synonym Tab 内维护。 +- Germplasm 列表搜索应支持 synonym 命中。 + +## 关键校验 + +1. `germplasm_id` 必须存在。 +2. 同一 germplasm 下同一个 synonym 不应重复。 +3. 删除 synonym 不应删除 germplasm 主数据。 diff --git a/docs/dev/04-germplasm/07-germplasm_taxon.md b/docs/dev/04-germplasm/07-germplasm_taxon.md new file mode 100644 index 0000000..4d17b44 --- /dev/null +++ b/docs/dev/04-germplasm/07-germplasm_taxon.md @@ -0,0 +1,36 @@ +# 04 Germplasm / Seed - germplasm_taxon 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`germplasm_taxon` 记录外部 taxon 标识和来源,用于把 germplasm 连接到分类数据库或外部生物分类体系。该表由 `TaxonEntity` 映射。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `germplasm_taxon` | +| Java 实体 | `TaxonEntity` | +| 前置依赖 | `germplasm` | +| 下游引用 | `sample.taxon_id_id`、分类展示 | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | taxon 记录主键 | 系统生成 | 必填、唯一 | +| `germplasm_id` | 所属 germplasm | Germplasm 选择器/详情页带出 | 必选,必须存在 | +| `source_name` | taxon 来源名称 | 文本/下拉 | 可选,如 NCBI Taxonomy | +| `taxon_id` | taxon 标识 | 用户录入 | 必填 | + +## 页面与交互 + +- 在 Germplasm 详情页 Taxon Tab 内维护。 +- Sample 表可引用 taxon 记录。 + +## 关键校验 + +1. `germplasm_id` 必须存在。 +2. 同一 source 下 taxon_id 不建议重复。 +3. 删除 taxon 前检查 sample 引用。 diff --git a/docs/dev/04-germplasm/08-germplasm_attribute_definition.md b/docs/dev/04-germplasm/08-germplasm_attribute_definition.md new file mode 100644 index 0000000..19aac29 --- /dev/null +++ b/docs/dev/04-germplasm/08-germplasm_attribute_definition.md @@ -0,0 +1,55 @@ +# 04 Germplasm / Seed - germplasm_attribute_definition 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`germplasm_attribute_definition` 定义 germplasm 可维护的属性。它继承变量定义体系,可关联 crop、trait、method、scale、ontology。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `germplasm_attribute_definition` | +| Java 实体 | `GermplasmAttributeDefinitionEntity` | +| 前置依赖 | `crop`、`trait`、`method`、`scale`、`ontology` | +| 下游引用 | `germplasm_attribute_value` | +| API | `/brapi/v2/attributes` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | 属性定义主键 | 系统生成;导入可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `name` | 属性名称 | 用户录入 | 必填 | +| `code` | 属性编码 | 用户录入 | 可选,建议唯一 | +| `pui` | 永久标识 | 用户录入 | 可选,建议唯一 | +| `attribute_category` | 属性分类 | 下拉/文本 | 可选 | +| `datatype` | 数据类型 | 下拉 | 建议必填 | +| `description` | 属性说明 | 多行文本 | 可选 | +| `uri` | 外部 URI | URL 输入 | 可选,URL 格式校验 | +| `crop_id` | 作物 | Crop 选择器 | 可选 | +| `trait_id` | 性状 | Trait 选择器 | 可选 | +| `method_id` | 方法 | Method 选择器 | 可选 | +| `scale_id` | 标尺 | Scale 选择器 | 可选 | +| `ontology_id` | 本体 | Ontology 选择器 | 可选 | +| `documentationurl` | 文档链接 | URL 输入 | 可选 | +| `status` | 状态 | 下拉/文本 | 可选 | +| `default_value` | 默认值 | 文本 | 可选 | +| `growth_stage` | 生育期 | 文本 | 可选 | +| `institution` | 机构 | 文本 | 可选 | +| `language` | 语言 | 文本 | 可选 | +| `scientist` | 科学家/维护人 | 文本 | 可选 | +| `submission_timestamp` | 提交时间 | 日期时间 | 可选 | + +## 页面与交互 + +- Attribute Definition 列表页支持按 crop、category、datatype、keyword 查询。 +- 创建 attribute value 前必须先有 definition。 + +## 关键校验 + +1. `name` 必填。 +2. 已被 `germplasm_attribute_value` 引用时不允许物理删除。 +3. `datatype` 要与 value 输入控件联动。 diff --git a/docs/dev/04-germplasm/09-germplasm_attribute_value.md b/docs/dev/04-germplasm/09-germplasm_attribute_value.md new file mode 100644 index 0000000..c277d02 --- /dev/null +++ b/docs/dev/04-germplasm/09-germplasm_attribute_value.md @@ -0,0 +1,39 @@ +# 04 Germplasm / Seed - germplasm_attribute_value 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`germplasm_attribute_value` 保存某个 germplasm 在某个属性定义上的实际取值。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `germplasm_attribute_value` | +| Java 实体 | `GermplasmAttributeValueEntity` | +| 前置依赖 | `germplasm`、`germplasm_attribute_definition` | +| 下游引用 | 属性查询、详情展示 | +| API | `/brapi/v2/attributevalues` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | 属性值主键 | 系统生成;导入可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `germplasm_id` | 所属 germplasm | Germplasm 选择器/详情页带出 | 必选,必须存在 | +| `attribute_id` | 属性定义 | Attribute 选择器 | 必选,必须存在 | +| `value` | 属性值 | 根据 definition.datatype 选择控件 | 必填或按业务要求 | +| `determined_date` | 测定日期 | 日期选择器 | 可选 | + +## 页面与交互 + +- 在 Germplasm 详情页 Attributes Tab 维护。 +- 批量导入时按 germplasm + attribute 组成矩阵录入更高效。 + +## 关键校验 + +1. `germplasm_id` 与 `attribute_id` 必须存在。 +2. 同一 germplasm 下同一 attribute 不建议重复,若允许多次测定,需要用 determined_date 区分。 +3. `value` 必须符合 attribute definition 的 datatype。 diff --git a/docs/dev/04-germplasm/10-crossing_project.md b/docs/dev/04-germplasm/10-crossing_project.md new file mode 100644 index 0000000..b88adf2 --- /dev/null +++ b/docs/dev/04-germplasm/10-crossing_project.md @@ -0,0 +1,38 @@ +# 04 Germplasm / Seed - crossing_project 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`crossing_project` 表示杂交项目,是 cross、planned cross、cross parent 和 pedigree node 的上游组织维度,通常挂在 Core 的 `program` 下。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `crossing_project` | +| Java 实体 | `CrossingProjectEntity` | +| 前置依赖 | `program` | +| 下游引用 | `cross_entity`、`cross_parent`、`pedigree_node` | +| API | `/brapi/v2/crossingprojects` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | crossing project 主键 | 系统生成;导入可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `name` | 杂交项目名称 | 用户录入 | 必填,建议同一 program 下唯一 | +| `description` | 项目说明 | 多行文本 | 可选 | +| `program_id` | 所属 program | Program 选择器 | 必选,必须存在 | + +## 页面与交互 + +- 列表页支持按 program、keyword 查询。 +- 详情页展示 crosses、planned crosses、potential parents、pedigree nodes。 + +## 关键校验 + +1. `program_id` 必须存在。 +2. 删除 crossing_project 前检查 cross、cross_parent、pedigree_node 引用。 +3. 如果 program 有 crop,创建 cross 时亲本 germplasm 建议与 program crop 一致。 diff --git a/docs/dev/04-germplasm/11-cross_entity.md b/docs/dev/04-germplasm/11-cross_entity.md new file mode 100644 index 0000000..7fdbb50 --- /dev/null +++ b/docs/dev/04-germplasm/11-cross_entity.md @@ -0,0 +1,50 @@ +# 04 Germplasm / Seed - cross_entity 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`cross_entity` 是 Cross 和 PlannedCross 的统一落库表。`plannedcross` 不是独立数据库表,计划杂交通过 `planned=true` 表达;实际杂交可以通过 `planned_cross_id` 关联计划杂交。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `cross_entity` | +| Java 实体 | `CrossEntity` | +| 前置依赖 | `crossing_project`,可选自关联 `cross_entity` | +| 下游引用 | `cross_parent`、`cross_pollination_event`、`seed_lot_content_mixture` | +| API | `/brapi/v2/crosses`、`/brapi/v2/plannedcrosses` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | cross 主键 | 系统生成;导入可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `name` | cross 名称 | 用户录入/自动生成 | 建议必填 | +| `cross_type` | 杂交类型 | 枚举选择 | 可选 | +| `crossing_project_id` | 所属 crossing project | CrossingProject 选择器 | 建议必选,若填需存在 | +| `status` | planned cross 状态 | 枚举选择 | 计划杂交常用 | +| `planned` | 是否为计划杂交 | 开关 | 必填,默认 false | +| `planned_cross_id` | 关联计划杂交 | Cross 选择器 | 可选,必须指向 `planned=true` 的 cross | + +## 附属集合字段 + +| 附属表 | 内容 | +| --- | --- | +| `cross_entity_cross_attributes` | crossAttributes 列表 | +| `cross_parent` | cross 的亲本 | +| `cross_pollination_event` | 授粉事件 | + +## 页面与交互 + +- Cross 列表页展示实际杂交;PlannedCross 列表页展示 `planned=true` 数据。 +- 新建实际 cross 时可选择 planned cross,并继承 planned cross 的亲本作为初始值。 +- 详情页展示 parents、pollination events 和 seed lot 组成入口。 + +## 关键校验 + +1. `plannedcross` 不新建表,所有 planned cross 走 `cross_entity`。 +2. `planned_cross_id` 不能指向自己。 +3. 删除 cross 前检查 `cross_parent`、`cross_pollination_event`、`seed_lot_content_mixture` 引用。 diff --git a/docs/dev/04-germplasm/12-cross_parent.md b/docs/dev/04-germplasm/12-cross_parent.md new file mode 100644 index 0000000..acdb6a5 --- /dev/null +++ b/docs/dev/04-germplasm/12-cross_parent.md @@ -0,0 +1,39 @@ +# 04 Germplasm / Seed - cross_parent 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`cross_parent` 连接 cross 与亲本来源。亲本可以是 germplasm,也可以是 observation_unit,用来表达实际田间单株或 plot 来源。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `cross_parent` | +| Java 实体 | `CrossParentEntity` | +| 前置依赖 | `cross_entity`、`crossing_project`、`germplasm` 或 `observation_unit` | +| 下游引用 | 杂交亲本展示和校验 | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | cross parent 主键 | 系统生成 | 必填、唯一 | +| `cross_id` | 所属 cross | Cross 选择器/详情页带出 | 必选,必须存在 | +| `crossing_project_id` | 所属 crossing project | 自动带出/选择器 | 建议与 cross 一致 | +| `germplasm_id` | 亲本 germplasm | Germplasm 选择器 | 与 observation_unit 至少填一个 | +| `observation_unit_id` | 亲本 observation unit | ObservationUnit 选择器 | 与 germplasm 至少填一个 | +| `parent_type` | 亲本类型 | 枚举选择 | FEMALE、MALE、SELF、POPULATION、CLONAL 等 | + +## 页面与交互 + +- 在 Cross 或 PlannedCross 详情页 Parents Tab 内维护。 +- 创建 cross 时至少添加一个亲本;常见杂交需要 FEMALE 和 MALE。 + +## 关键校验 + +1. `cross_id` 必须存在。 +2. `germplasm_id` 和 `observation_unit_id` 至少填写一个,不建议同时为空。 +3. 同一 cross 下相同 parentType + germplasm/observationUnit 不应重复。 +4. 如果填 `crossing_project_id`,应与 `cross.crossing_project_id` 一致。 diff --git a/docs/dev/04-germplasm/13-cross_pollination_event.md b/docs/dev/04-germplasm/13-cross_pollination_event.md new file mode 100644 index 0000000..14f7409 --- /dev/null +++ b/docs/dev/04-germplasm/13-cross_pollination_event.md @@ -0,0 +1,37 @@ +# 04 Germplasm / Seed - cross_pollination_event 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`cross_pollination_event` 记录实际杂交过程中的授粉事件,包括授粉编号、时间和是否成功。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `cross_pollination_event` | +| Java 实体 | `CrossPollinationEventEntity` | +| 前置依赖 | `cross_entity` | +| 下游引用 | Cross 过程追踪 | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | 授粉事件主键 | 系统生成 | 必填、唯一 | +| `cross_id` | 所属 cross | Cross 选择器/详情页带出 | 必选,必须存在 | +| `pollination_number` | 授粉编号 | 用户录入/自动生成 | 可选,建议同一 cross 下唯一 | +| `pollination_successful` | 是否授粉成功 | 开关 | 可选 | +| `pollination_time_stamp` | 授粉时间 | 日期时间选择器 | 可选 | + +## 页面与交互 + +- 在 Cross 详情页 Pollination Events Tab 内维护。 +- 可以按授粉时间排序展示。 + +## 关键校验 + +1. `cross_id` 必须存在。 +2. 同一 cross 下 `pollination_number` 不建议重复。 +3. 删除授粉事件不应删除 cross 主数据。 diff --git a/docs/dev/04-germplasm/14-pedigree_node.md b/docs/dev/04-germplasm/14-pedigree_node.md new file mode 100644 index 0000000..07cf02d --- /dev/null +++ b/docs/dev/04-germplasm/14-pedigree_node.md @@ -0,0 +1,40 @@ +# 04 Germplasm / Seed - pedigree_node 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`pedigree_node` 表示系谱中的一个节点,一个节点通常关联一个 germplasm,并可归属 crossing_project。节点之间的 parent/child/sibling 关系由 `pedigree_edge` 记录。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `pedigree_node` | +| Java 实体 | `PedigreeNodeEntity` | +| 前置依赖 | `germplasm`、可选 `crossing_project` | +| 下游引用 | `pedigree_edge` | +| API | `/brapi/v2/pedigree` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | pedigree node 主键 | 系统生成;导入可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `germplasm_id` | 对应 germplasm | Germplasm 选择器 | 建议必选,若填需存在 | +| `crossing_project_id` | 所属 crossing project | CrossingProject 选择器 | 可选 | +| `crossing_year` | 杂交年份 | 年份输入 | 可选 | +| `family_code` | 家系编号 | 文本 | 可选 | +| `pedigree_string` | 系谱字符串 | 文本 | 可选 | + +## 页面与交互 + +- 可从 Germplasm 详情页进入 Pedigree Tab。 +- 支持图谱视图和表格视图。 + +## 关键校验 + +1. 同一 germplasm 通常只应有一个 pedigree node。 +2. 删除 pedigree_node 前检查 `pedigree_edge` 中 this_node 和 connceted_node 引用。 +3. 导入 pedigree 时需要先创建所有节点,再创建边。 diff --git a/docs/dev/04-germplasm/15-pedigree_edge.md b/docs/dev/04-germplasm/15-pedigree_edge.md new file mode 100644 index 0000000..f09ffda --- /dev/null +++ b/docs/dev/04-germplasm/15-pedigree_edge.md @@ -0,0 +1,43 @@ +# 04 Germplasm / Seed - pedigree_edge 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`pedigree_edge` 描述两个 pedigree_node 之间的 parent、child 或 sibling 关系,并标注亲本类型。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `pedigree_edge` | +| Java 实体 | `PedigreeEdgeEntity` | +| 前置依赖 | `pedigree_node` | +| 下游引用 | 系谱查询、亲本/后代展示 | +| API | `/brapi/v2/pedigree` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | pedigree edge 主键 | 系统生成;导入可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `this_node_id` | 当前节点 | PedigreeNode 选择器 | 必选,必须存在 | +| `connceted_node_id` | 关联节点 | PedigreeNode 选择器 | 必选,必须存在 | +| `edge_type` | 关系类型 | 枚举选择 | parent、child、sibling | +| `parent_type` | 亲本类型 | 枚举选择 | 可选,如 FEMALE、MALE | + +## 命名注意 + +实体字段是 `conncetedNode`,拼写与 `connected` 不同;开发 DTO、SQL、迁移时要留意当前代码和数据库列名。 + +## 页面与交互 + +- 在 Germplasm/Pedigree 图谱视图中维护。 +- 创建 parent 关系时可自动补齐反向 child 关系,具体以业务实现为准。 + +## 关键校验 + +1. `this_node_id` 和 `connceted_node_id` 必须存在。 +2. 两个节点不能相同。 +3. 同一节点之间同一种 edge_type 不应重复。 diff --git a/docs/dev/04-germplasm/16-seed_lot.md b/docs/dev/04-germplasm/16-seed_lot.md new file mode 100644 index 0000000..ea43d4d --- /dev/null +++ b/docs/dev/04-germplasm/16-seed_lot.md @@ -0,0 +1,46 @@ +# 04 Germplasm / Seed - seed_lot 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`seed_lot` 是真实库存批次,描述某一批种子当前数量、单位、库位和项目归属。同一个 germplasm 可以有多个 seed lot。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `seed_lot` | +| Java 实体 | `SeedLotEntity` | +| 前置依赖 | 可选 `location`、`program` | +| 下游引用 | `seed_lot_content_mixture`、`seed_lot_transaction` | +| API | `/brapi/v2/seedlots` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | seed lot 主键 | 系统生成;导入可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `name` | 批次名称 | 用户录入/自动生成 | 必填,建议同一 program 下唯一 | +| `amount` | 当前库存数量 | 交易自动更新/用户录入初始值 | 非负 | +| `units` | 数量单位 | 下拉/文本 | 建议必填 | +| `created_date` | 创建时间 | 系统写入/导入 | 可选 | +| `last_updated` | 最后更新时间 | 系统更新 | 不建议手填 | +| `description` | 批次说明 | 多行文本 | 可选 | +| `source_collection` | 来源 collection | 文本 | 可选 | +| `storage_location` | 具体库位 | 文本 | 可选 | +| `location_id` | 所在地点/库位 | Location 选择器 | 可选,若填需存在 | +| `program_id` | 所属项目 | Program 选择器 | 可选,若填需存在 | + +## 页面与交互 + +- SeedLot 列表页支持按 program、location、name、库存状态查询。 +- 详情页展示 content mixture 和 transactions。 +- 创建 seed lot 后建议至少维护一条 `seed_lot_content_mixture`。 + +## 关键校验 + +1. `amount` 不允许为负。 +2. 普通用户不应直接编辑 amount,库存变化应通过 `seed_lot_transaction`。 +3. 删除 seed lot 前检查组成明细和交易引用。 diff --git a/docs/dev/04-germplasm/17-seed_lot_content_mixture.md b/docs/dev/04-germplasm/17-seed_lot_content_mixture.md new file mode 100644 index 0000000..f20e83b --- /dev/null +++ b/docs/dev/04-germplasm/17-seed_lot_content_mixture.md @@ -0,0 +1,38 @@ +# 04 Germplasm / Seed - seed_lot_content_mixture 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`seed_lot_content_mixture` 表示 seed lot 的组成明细。它可以把 seed lot 连接到 germplasm,也可以连接到 cross_entity,适合表达混合批次或杂交产生的批次。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `seed_lot_content_mixture` | +| Java 实体 | `SeedLotContentMixtureEntity` | +| 前置依赖 | `seed_lot`、可选 `germplasm`、可选 `cross_entity` | +| 下游引用 | Seed lot 组成展示 | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | 组成明细主键 | 系统生成 | 必填、唯一 | +| `seed_lot_id` | 所属 seed lot | SeedLot 选择器/详情页带出 | 必选,必须存在 | +| `germplasm_id` | 组成 germplasm | Germplasm 选择器 | 与 cross_id 至少填一个 | +| `cross_id` | 组成来源 cross | Cross 选择器 | 与 germplasm_id 至少填一个 | +| `mixture_percentage` | 组成比例 | 数字输入 | 0-100 | + +## 页面与交互 + +- 在 SeedLot 详情页 Content Mixture Tab 内维护。 +- 支持多个组成明细,合计比例可展示为进度或校验提示。 + +## 关键校验 + +1. `seed_lot_id` 必须存在。 +2. `germplasm_id` 与 `cross_id` 至少填写一个。 +3. 同一 seed lot 下 mixture_percentage 合计建议为 100。 +4. 删除组成明细不应删除 seed lot、germplasm 或 cross 主数据。 diff --git a/docs/dev/04-germplasm/18-seed_lot_transaction.md b/docs/dev/04-germplasm/18-seed_lot_transaction.md new file mode 100644 index 0000000..f37d1c4 --- /dev/null +++ b/docs/dev/04-germplasm/18-seed_lot_transaction.md @@ -0,0 +1,43 @@ +# 04 Germplasm / Seed - seed_lot_transaction 表录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +## 录入目标 + +`seed_lot_transaction` 记录 seed lot 之间的库存流转,例如入库、出库、转移、分装、合并或消耗。它表达 from seed lot 到 to seed lot 的数量变化。 + +## 上下游关系 + +| 类型 | 内容 | +| --- | --- | +| 表 | `seed_lot_transaction` | +| Java 实体 | `SeedLotTransactionEntity` | +| 前置依赖 | `from_seed_lot`、`to_seed_lot` | +| 下游引用 | 库存追踪、库存数量更新 | +| API | `/brapi/v2/seedlots/transactions`、`/brapi/v2/seedlots/{seedLotDbId}/transactions` | + +## 字段录入 + +| 字段 | 业务意义 | 录入方式 | 校验规则 | +| --- | --- | --- | --- | +| `id` | transaction 主键 | 系统生成;导入可指定 | 必填、唯一 | +| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 | +| `from_seed_lot_id` | 来源 seed lot | SeedLot 选择器 | 入库场景可为空,若填需存在 | +| `to_seed_lot_id` | 目标 seed lot | SeedLot 选择器 | 出库/消耗场景可为空,若填需存在 | +| `amount` | 流转数量 | 数字输入 | 必填,正数 | +| `units` | 数量单位 | 下拉/文本 | 必填,应与批次单位一致或可换算 | +| `timestamp` | 流转时间 | 日期时间选择器 | 默认当前时间 | +| `description` | 流转说明 | 多行文本 | 可选 | + +## 页面与交互 + +- 在 SeedLot 详情页 Transactions Tab 展示相关流转。 +- 新增交易后应更新 from/to seed lot 的 amount 和 last_updated。 +- 支持按时间、from/to seed lot、program、location 查询。 + +## 关键校验 + +1. `from_seed_lot_id` 和 `to_seed_lot_id` 至少填写一个。 +2. `from_seed_lot_id` 不能等于 `to_seed_lot_id`。 +3. 出库或转移时,来源批次数量不能被扣成负数。 +4. transaction 是业务动作痕迹,原则上不允许随意物理删除。 diff --git a/docs/dev/04-germplasm/README.md b/docs/dev/04-germplasm/README.md new file mode 100644 index 0000000..c044805 --- /dev/null +++ b/docs/dev/04-germplasm/README.md @@ -0,0 +1,46 @@ +# 04 Germplasm / Seed 开发录入说明 + +来源:`docs/architecture/04-germplasm-seed-data-flow.md` + +本目录按 Germplasm / Seed 模块的数据录入顺序拆分开发说明。主线是: + +```text +breeding_method -> germplasm +germplasm_attribute_definition -> germplasm_attribute_value +program -> crossing_project -> cross_entity -> cross_parent +germplasm / cross_entity -> seed_lot_content_mixture -> seed_lot +seed_lot -> seed_lot_transaction +germplasm -> pedigree_node -> pedigree_edge +``` + +## 文档清单 + +| 顺序 | 文档 | 表 | 作用 | +| --- | --- | --- | --- | +| 01 | `01-breeding_method.md` | `breeding_method` | 育种方法字典 | +| 02 | `02-germplasm.md` | `germplasm` | 种质主表 | +| 03 | `03-germplasm_donor.md` | `germplasm_donor` | donor 信息 | +| 04 | `04-germplasm_institute.md` | `germplasm_institute` | 机构信息 | +| 05 | `05-germplasm_origin.md` | `germplasm_origin` | 来源地/坐标 | +| 06 | `06-germplasm_synonym.md` | `germplasm_synonym` | 别名 | +| 07 | `07-germplasm_taxon.md` | `germplasm_taxon` | taxon 标识 | +| 08 | `08-germplasm_attribute_definition.md` | `germplasm_attribute_definition` | 属性定义 | +| 09 | `09-germplasm_attribute_value.md` | `germplasm_attribute_value` | 属性值 | +| 10 | `10-crossing_project.md` | `crossing_project` | 杂交项目 | +| 11 | `11-cross_entity.md` | `cross_entity` | Cross / PlannedCross | +| 12 | `12-cross_parent.md` | `cross_parent` | 杂交亲本 | +| 13 | `13-cross_pollination_event.md` | `cross_pollination_event` | 授粉事件 | +| 14 | `14-pedigree_node.md` | `pedigree_node` | 系谱节点 | +| 15 | `15-pedigree_edge.md` | `pedigree_edge` | 系谱边 | +| 16 | `16-seed_lot.md` | `seed_lot` | 种子库存批次 | +| 17 | `17-seed_lot_content_mixture.md` | `seed_lot_content_mixture` | 批次组成 | +| 18 | `18-seed_lot_transaction.md` | `seed_lot_transaction` | 批次流转 | + +## 全局注意点 + +1. `plannedcross` 没有独立数据库表,统一落在 `cross_entity`,通过 `planned` 和 `planned_cross_id` 自关联表达。 +2. `germplasm.seedSource` / `seedSourceDescription` 是种质来源描述,不等于真实库存批次。 +3. 真正表达库存的是 `seed_lot`,批次与种质或杂交来源的关系在 `seed_lot_content_mixture`。 +4. `seed_lot_transaction` 表达 seed lot 到 seed lot 的流转,不直接表达 seed lot 到 germplasm。 +5. 属性定义和值要分开:`germplasm_attribute_definition` 定义“可填什么”,`germplasm_attribute_value` 记录“某个 germplasm 填了什么”。 +6. 系谱关系用 `pedigree_node` / `pedigree_edge`;杂交流程用 `cross_entity` / `cross_parent`,两条线都可以回到 `germplasm`。 diff --git a/docs/dev/backend/auth.md b/docs/dev/backend/auth.md new file mode 100644 index 0000000..d37ad7c --- /dev/null +++ b/docs/dev/backend/auth.md @@ -0,0 +1,533 @@ +# 后端登录注册接口说明 + +本文档描述 `common-br-api` 项目中用户认证(注册、登录、Token 校验)的后端实现,便于前后端联调与部署排查。 + +--- + +## 1. 总览 + +| 项目 | 说明 | +|------|------| +| 路由前缀 | `/auth` | +| 框架 | FastAPI | +| 用户表 | `sys_users` | +| 会话表 | `user_sessions` | +| 密码算法 | PBKDF2-HMAC-SHA256(210000 次迭代) | +| Token 形式 | 自实现 JWT 风格 Bearer Token(HS256 签名) | +| 默认有效期 | 24 小时(1440 分钟) | + +**当前已实现的 HTTP 接口:** + +| 方法 | 路径 | 说明 | +|------|------|------| +| `POST` | `/auth/register` | 注册并自动登录 | +| `POST` | `/auth/login` | 账号密码登录 | + +**尚未实现的接口(前端 UI 可能已预留):** + +- `/auth/logout`(登出 / 服务端吊销会话) +- `/auth/refresh`(刷新 Token) +- 手机号 / 短信验证码登录 + +--- + +## 2. 代码结构 + +``` +app/ +├── api/ +│ └── auth.py # 注册、登录、get_current_user 依赖 +├── core/ +│ └── security.py # 密码哈希、Token 签发、Token 哈希 +├── models/ +│ └── auth.py # User、UserSession ORM 模型 +├── config/ +│ └── settings.py # JWT 密钥、过期时间等配置 +└── main.py # 挂载 auth 路由 + +migrations/versions/ +└── 7c91a64f2b2e_create_auth_tables.py # 建表迁移 +``` + +路由挂载(`app/main.py`): + +```python +app.include_router(auth_router) # 无前缀叠加,router 自身 prefix="/auth" +``` + +因此完整 URL 为: + +```text +http://:8000/auth/register +http://:8000/auth/login +``` + +经 Next.js 代理后(开发环境)也可访问: + +```text +http://localhost:3000/auth/register +http://localhost:3000/auth/login +``` + +--- + +## 3. 认证流程 + +### 3.1 注册 / 登录发 Token + +```mermaid +sequenceDiagram + participant Client as 客户端 + participant API as /auth/* + participant DB as PostgreSQL + + Client->>API: POST /auth/register 或 /auth/login + API->>DB: 查重 / 校验密码 + API->>API: create_access_token(sub, account, jti) + API->>DB: INSERT user_sessions(jti, token_hash, expires_at) + API-->>Client: TokenResponse(access_token, expires_at, user) +``` + +### 3.2 受保护接口校验 Token + +```mermaid +sequenceDiagram + participant Client as 客户端 + participant API as 业务接口 + participant DB as PostgreSQL + + Client->>API: Authorization: Bearer + API->>API: 校验 JWT 签名与 exp + API->>DB: JOIN sys_users + user_sessions
匹配 user_id, jti, token_hash + alt 会话有效 + API-->>Client: 200 + 业务数据 + else 无效或过期 + API-->>Client: 401 / 403 + end +``` + +**要点:** + +1. Token 不仅校验 JWT 签名和过期时间,还必须在 `user_sessions` 表中有对应且未吊销的记录。 +2. 数据库存的是 Token 的 **SHA256 哈希**,不是明文 Token。 +3. 每个 Token 有唯一 `jti`(JWT ID),用于会话追踪。 + +--- + +## 4. 数据库设计 + +### 4.1 `sys_users` + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `id` | UUID | PK | 用户 ID | +| `account` | VARCHAR(100) | UNIQUE, NOT NULL | 登录账号 | +| `password_hash` | TEXT | NOT NULL | PBKDF2 哈希串 | +| `name` | VARCHAR(100) | NOT NULL | 显示名称 | +| `email` | VARCHAR(255) | UNIQUE, NOT NULL | 邮箱(注册时转小写) | +| `phone` | VARCHAR(50) | NULL | 手机号(可选) | +| `created_at` | TIMESTAMPTZ | NOT NULL | 创建时间 | +| `updated_at` | TIMESTAMPTZ | NOT NULL | 更新时间 | + +索引: + +- `ux_users_account`(account 唯一) +- `ux_users_email`(email 唯一) + +### 4.2 `user_sessions` + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `id` | UUID | PK | 会话记录 ID | +| `user_id` | UUID | FK → sys_users.id, CASCADE | 所属用户 | +| `jti` | VARCHAR(64) | UNIQUE, NOT NULL | Token 会话 ID | +| `token_hash` | TEXT | NOT NULL | SHA256(access_token) | +| `expires_at` | TIMESTAMPTZ | NOT NULL | 过期时间 | +| `revoked_at` | TIMESTAMPTZ | NULL | 吊销时间(预留,当前无 logout 接口写入) | +| `created_at` | TIMESTAMPTZ | NOT NULL | 创建时间 | +| `updated_at` | TIMESTAMPTZ | NOT NULL | 更新时间 | + +--- + +## 5. 环境配置 + +配置类:`app/config/settings.py` + +| 环境变量 | 默认值 | 说明 | +|----------|--------|------| +| `JWT_SECRET_KEY` | `change-me-in-production` | HMAC 签名密钥,**生产必须修改** | +| `JWT_ALGORITHM` | `HS256` | 写入 JWT Header(实际签名为 HMAC-SHA256) | +| `ACCESS_TOKEN_EXPIRE_MINUTES` | `1440`(24h) | Token 有效期(分钟) | +| `DATABASE_URL` | 见 `.env.example` | PostgreSQL 连接串 | + +示例(`.env`): + +```env +JWT_SECRET_KEY=your-long-random-secret +DATABASE_URL=postgresql+asyncpg://user:pass@postgres:5432/brapi-python +``` + +--- + +## 6. 密码安全 + +实现文件:`app/core/security.py` + +### 6.1 哈希格式 + +``` +pbkdf2_sha256$210000$$ +``` + +- 算法:`PBKDF2-HMAC-SHA256` +- 迭代次数:`210000` +- Salt:16 字节随机 hex(`secrets.token_hex(16)`) + +### 6.2 注册时 + +```python +password_hash=hash_password(payload.password) +``` + +### 6.3 登录时 + +```python +verify_password(payload.password, user.password_hash) +``` + +校验失败统一返回 `401 Invalid account or password`,不区分「账号不存在」与「密码错误」。 + +--- + +## 7. Access Token 结构 + +Token 由 `create_access_token()` 生成,格式为三段式 Base64URL: + +```text +.. +``` + +### 7.1 Header + +```json +{"alg": "HS256", "typ": "JWT"} +``` + +### 7.2 Payload + +```json +{ + "sub": "", + "account": "", + "jti": "<32_hex_chars>", + "iat": 1710000000, + "exp": 1710086400 +} +``` + +| Claim | 含义 | +|-------|------| +| `sub` | 用户 UUID 字符串 | +| `account` | 登录账号 | +| `jti` | 会话唯一 ID | +| `iat` | 签发时间(Unix 秒) | +| `exp` | 过期时间(Unix 秒) | + +### 7.3 签名 + +```python +HMAC-SHA256(key=JWT_SECRET_KEY, msg=f"{header_b64}.{payload_b64}") +``` + +> 注意:项目使用**自实现**的 JWT 编解码(`app/api/auth.py` + `app/core/security.py`),未依赖 PyJWT 库。 + +--- + +## 8. HTTP 接口详情 + +### 8.1 注册 `POST /auth/register` + +**请求体 `RegisterRequest`:** + +| 字段 | 类型 | 必填 | 校验 | +|------|------|------|------| +| `account` | string | 是 | 长度 3–100 | +| `password` | string | 是 | 长度 6–128 | +| `name` | string | 是 | 长度 1–100 | +| `email` | string | 是 | 长度 3–255,入库前转小写 | +| `phone` | string | 否 | 最大 50 | + +**业务逻辑:** + +1. `account`、`email` 去空格 / 小写处理 +2. 查询 `sys_users`,若 `account` 或 `email` 已存在 → `409 Conflict` +3. 创建用户并 `flush` +4. 调用 `_issue_token()` 写入 `user_sessions` 并返回 Token + +**成功响应:** `201 Created`,Body 为 `TokenResponse` + +**curl 示例:** + +```bash +curl -X POST http://127.0.0.1:8000/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "account": "demo_user", + "password": "demo123456", + "name": "演示用户", + "email": "demo@example.com", + "phone": "13800138000" + }' +``` + +--- + +### 8.2 登录 `POST /auth/login` + +**请求体 `LoginRequest`:** + +| 字段 | 类型 | 必填 | 校验 | +|------|------|------|------| +| `account` | string | 是 | 长度 1–100 | +| `password` | string | 是 | 长度 1–128 | + +**业务逻辑:** + +1. 按 `account` 查用户 +2. `verify_password` 失败 → `401 Unauthorized` +3. 成功则 `_issue_token()` 并返回 + +**成功响应:** `200 OK`,Body 为 `TokenResponse` + +**curl 示例:** + +```bash +curl -X POST http://127.0.0.1:8000/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "account": "demo_user", + "password": "demo123456" + }' +``` + +--- + +### 8.3 统一成功响应 `TokenResponse` + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....", + "token_type": "bearer", + "expires_at": "2026-05-27T10:00:00+00:00", + "user": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "account": "demo_user", + "name": "演示用户", + "email": "demo@example.com", + "phone": "13800138000" + } +} +``` + +| 字段 | 说明 | +|------|------| +| `access_token` | Bearer Token 字符串 | +| `token_type` | 固定 `"bearer"` | +| `expires_at` | ISO8601 UTC 过期时间 | +| `user` | 当前用户公开信息(不含密码) | + +--- + +### 8.4 错误响应格式 + +认证相关错误通过 `ApiError` 抛出,由 `app/main.py` 统一处理: + +```json +{ + "message": "错误摘要", + "detail": "错误摘要或详细信息" +} +``` + +| HTTP 状态码 | 场景 | message 示例 | +|-------------|------|----------------| +| `401` | 登录失败 | `Invalid account or password` | +| `401` | 缺少 Token | `Missing bearer token` | +| `401` | Token 无效 | `Invalid token signature` / `Session invalid or expired` | +| `403` | Token 过期 | `Token expired` | +| `409` | 注册冲突 | `Account or email already exists` | +| `422` | 参数校验失败 | `Validation failed`(detail 为字段错误列表) | + +--- + +## 9. 受保护接口:`get_current_user` + +定义位置:`app/api/auth.py` + +```python +async def get_current_user( + credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme), + session: AsyncSession = Depends(get_db_session), +) -> User: + ... +``` + +### 9.1 客户端如何携带 Token + +```http +Authorization: Bearer +``` + +### 9.2 校验步骤 + +1. 检查 `Authorization` 头是否存在且 scheme 为 `bearer` +2. `_decode_access_token()`:验证格式、HMAC 签名、`exp` 未过期 +3. 从 payload 读取 `sub`(user_id)、`jti` +4. 数据库查询: + +```sql +SELECT sys_users.* +FROM sys_users +JOIN user_sessions ON user_sessions.user_id = sys_users.id +WHERE sys_users.id = :user_id + AND user_sessions.jti = :jti + AND user_sessions.token_hash = sha256(:token) + AND user_sessions.revoked_at IS NULL + AND user_sessions.expires_at > now() +``` + +5. 查不到用户 → `401 Session invalid or expired` + +### 9.3 哪些接口使用了 `get_current_user` + +#### `/auth` 模块 + +无(register/login 本身不需要 Token) + +#### `/api/dictionaries` 模块(`app/api/dictionaries.py`) + +| 接口 | 是否需要登录 | +|------|-------------| +| `GET /api/dictionaries/crops` | 否 | +| `POST /api/dictionaries/crops` | **是** | +| `GET /api/dictionaries/persons` | 否 | +| `POST /api/dictionaries/persons` | 否 | +| `POST /api/dictionaries/countries` | **是** | +| `PUT /api/dictionaries/countries/{code}` | **是** | + +#### `/brapi/v2` 模块(`app/api/router.py`) + +- 多数 **GET** 接口:接收 `Authorization` 头但**不强制校验**(BrAPI 兼容占位) +- 多数 **POST / PUT / DELETE** 写操作:通过 `Depends(get_current_user)` **强制登录** + +写操作成功后会将 `current_user.id` 写入业务表的 `auth_user_id` 字段,用于记录数据创建者。 + +> 上传接口 `POST /brapi/v2/upload/image` 当前**未**接入 `get_current_user`,前端可选择性携带 Token。 + +--- + +## 10. `_issue_token` 内部流程 + +```python +async def _issue_token(session: AsyncSession, user: User) -> TokenResponse: + jti = uuid4().hex + token, expires_at = create_access_token( + subject=str(user.id), + account=user.account, + jti=jti, + ) + session.add(UserSession( + user_id=user.id, + jti=jti, + token_hash=hash_token(token), + expires_at=expires_at, + )) + await session.commit() + return TokenResponse(...) +``` + +每次登录/注册都会**新增一条** `user_sessions` 记录;旧会话不会自动吊销(除非手动清理数据库或未来实现 logout)。 + +--- + +## 11. 前端对接(简要) + +| 文件 | 作用 | +|------|------| +| `frontend/src/services/authService.ts` | 封装 `loginWithPassword`、`registerAccount` | +| `frontend/src/lib/api/sdk.gen.ts` | OpenAPI 生成的 `/auth/login`、`/auth/register` 客户端 | +| `frontend/src/stores/modules/auth.ts` | Zustand 持久化 Token 与用户信息 | +| `frontend/src/utils/token.ts` | 从 `localStorage` 读取 `auth_token` | +| `frontend/src/lib/client.ts` | 请求拦截器自动附加 `Authorization: Bearer ...` | + +登录成功后前端会: + +1. 调用 `setSession(session)` 写入 Zustand + `localStorage.auth_token` +2. 跳转到 `/central-config/user/employee` 或 `redirect` 参数指定页 + +登出:仅前端 `logout()` 清除本地存储,**不会**通知后端吊销会话。 + +--- + +## 12. 本地调试 + +### 12.1 启动后端 + +```bash +uv run uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload +``` + +### 12.2 Swagger 文档 + +```text +http://127.0.0.1:8000/docs +``` + +在 Swagger 中可找到 `auth` 分组下的 `register`、`login`。 + +### 12.3 用 Token 访问受保护接口 + +```bash +TOKEN="" + +curl http://127.0.0.1:8000/api/dictionaries/crops \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"crop_name": "maize"}' +``` + +--- + +## 13. 部署注意事项 + +1. **必须修改** `JWT_SECRET_KEY`,不要使用默认值 `change-me-in-production`。 +2. 首次部署需执行数据库迁移:`uv run alembic upgrade head`(包含 `sys_users`、`user_sessions` 建表)。 +3. Docker / K8s 环境中,容器内 `API_BASE_URL=http://127.0.0.1:8000` 供 Next.js 代理;客户端浏览器仍通过同源 `/auth/*` 访问。 +4. 当前无服务端 logout:Token 在过期前始终有效(只要 `user_sessions` 记录存在)。如需强制下线,需扩展 logout 接口并设置 `revoked_at`。 + +--- + +## 14. 后续可扩展项 + +| 功能 | 建议实现 | +|------|----------| +| 登出 | `POST /auth/logout`,将当前 `jti` 的 `revoked_at` 设为 now | +| 刷新 Token | `POST /auth/refresh`,验证旧 Token 后签发新 Token | +| 当前用户 | `GET /auth/me`,返回 `UserResponse` | +| 修改密码 | `POST /auth/change-password` | +| 手机号登录 | 独立短信验证码表 + 新 login 分支 | +| 上传鉴权 | `upload.py` 增加 `Depends(get_current_user)` | + +--- + +## 15. 相关源码索引 + +| 内容 | 路径 | +|------|------| +| 路由与 DTO | `app/api/auth.py` | +| 密码 / Token 工具 | `app/core/security.py` | +| ORM 模型 | `app/models/auth.py` | +| 配置 | `app/config/settings.py` | +| 建表迁移 | `migrations/versions/7c91a64f2b2e_create_auth_tables.py` | +| 应用入口 | `app/main.py` | +| 前端封装 | `frontend/src/services/authService.ts` | diff --git a/docs/requirements/00-data-entry-requirements-overview.md b/docs/requirements/00-data-entry-requirements-overview.md new file mode 100644 index 0000000..3efbe39 --- /dev/null +++ b/docs/requirements/00-data-entry-requirements-overview.md @@ -0,0 +1,89 @@ +# 数据录入需求总览 + +本文档基于 `docs/architecture` 下 5 份架构文档拆解,目标是把 BrAPI Test Server 的数据录入需求按上游到下游梳理清楚: + +```text +Core 基础数据 -> Germplasm/Seed 种质种子 -> Phenotyping 表型 -> Genotyping 基因型 +``` + +## 文档索引 + +| 顺序 | 文档 | 说明 | +| --- | --- | --- | +| 1 | `01-core-data-entry-requirements.md` | crop、person、program、location、trial、season、study、list | +| 2 | `02-germplasm-seed-entry-requirements.md` | breeding_method、germplasm、attribute、cross、seed_lot、pedigree | +| 3 | `03-phenotyping-entry-requirements.md` | ontology、trait、method、scale、observation_variable、observation_unit、event、image、observation | +| 4 | `04-genotyping-entry-requirements.md` | plate、sample、reference、variantset、variant、callset、allele_call、genome_map | + +## 总体录入链路 + +```mermaid +flowchart TD + CROP["crop 作物"] --> PROGRAM["program 项目"] + PERSON["person 人员"] --> PROGRAM + PROGRAM --> TRIAL["trial 试验批次"] + PROGRAM --> LOCATION["location 地点"] + TRIAL --> STUDY["study 研究/试验单元"] + LOCATION --> STUDY + + CROP --> BM["breeding_method 育种方法"] + BM --> G["germplasm 种质"] + G --> GAV["germplasm_attribute_value 种质属性值"] + PROGRAM --> CP["crossing_project 杂交项目"] + CP --> CROSS["cross_entity Cross/PlannedCross"] + G --> CROSSP["cross_parent 亲本"] + CROSS --> CROSSP + G --> MIX["seed_lot_content_mixture 批次组成"] + CROSS --> MIX + MIX --> SL["seed_lot 种子批次"] + + STUDY --> OU["observation_unit 观测单元"] + G --> OU + SL --> OU + CROSS --> OU + OU --> OBS["observation 观测值"] + + OU --> SAMPLE["sample 样本"] + STUDY --> SAMPLE + SAMPLE --> CALLSET["callset 调用集合"] + VARIANT["variant 变异位点"] --> CALL["allele_call 基因型结果"] + CALLSET --> CALL +``` + +## 全局界面原则 + +| 页面类型 | 用途 | 建议界面 | +| --- | --- | --- | +| 列表页 | 查询、筛选、分页、批量操作 | 顶部筛选区 + 表格 + 新增按钮 + 行内查看/编辑 | +| 新增/编辑页 | 单表主数据录入 | 分组表单,必填字段靠前,外键使用搜索选择器 | +| 详情页 | 查看主表及下游数据 | 顶部摘要 + Tab:基本信息、关联数据、扩展信息、外部引用 | +| 关系选择器 | 选择上游依赖 | 支持按 ID、名称、作物、项目、study 搜索 | +| 批量导入 | 初始化和大批量数据 | 上传 CSV/Excel + 字段映射 + 预校验 + 导入结果 | + +## 全局字段规则 + +| 字段类型 | 录入规则 | +| --- | --- | +| `id` / `DbId` | 系统生成或用户提供;必须唯一;编辑时不可随意修改 | +| 名称字段 | 必填或强建议必填;用于下拉选择展示 | +| 外键字段 | 页面展示名称,提交保存实际 ID | +| 枚举字段 | 使用下拉框或单选控件 | +| 日期时间 | 使用日期/时间选择器,保存前统一格式 | +| `additional_info` | 作为高级配置,用键值编辑器录入 | +| `external_references` | 作为外部系统引用,用可增删表格录入 | + +## 总体录入顺序 + +1. 录入 Core:`crop`、`person`、`program`、`location`、`trial`、`season`、`study`、`list`。 +2. 录入 Germplasm/Seed:`breeding_method`、`germplasm_attribute_definition`、`germplasm`、`germplasm_attribute_value`、`crossing_project`、`cross_entity`、`cross_parent`、`pedigree_node`、`pedigree_edge`、`seed_lot`、`seed_lot_content_mixture`、`seed_lot_transaction`。 +3. 录入 Phenotyping:`ontology`、`trait`、`method`、`scale`、`observation_variable`、`observation_unit`、`event`、`image`、`observation`。 +4. 录入 Genotyping:`plate`、`sample`、`reference_set`、`reference`、`reference_bases`、`variantset`、`variant`、`callset`、`allele_call`、`genome_map`、`linkageGroup`、`marker_position`。 + +## 通用验收标准 + +1. 每张表都有列表、新增、编辑、详情能力。 +2. 每个外键字段都能通过名称搜索选择,而不是要求用户手填数据库 ID。 +3. 下游表新增时必须校验上游数据是否存在。 +4. 删除或禁用上游数据时,需要提示被哪些下游表引用。 +5. 支持按模块导入 CSV/Excel,并在导入前给出错误行和错误原因。 +6. 详情页能看到关键下游关联,例如 `study` 下的 observation unit、sample、variantset。 diff --git a/docs/requirements/01-core-data-entry-requirements.md b/docs/requirements/01-core-data-entry-requirements.md new file mode 100644 index 0000000..1768e5a --- /dev/null +++ b/docs/requirements/01-core-data-entry-requirements.md @@ -0,0 +1,755 @@ +# Core 模块业务需求文档 + +## 1. 模块定位 + +Core 是项目、试验、地点、人员、季节的基础上下文模块,是所有业务数据的总上游。它不是普通字典维护,而是为后续 Germplasm/Seed、Phenotyping、Genotyping 提供“这批数据属于哪个项目、哪次试验、哪个地点、哪个年度、谁负责”的业务台账。 + +推荐先完成 Core 录入,再录入 Germplasm、Phenotyping、Genotyping。 + +## 2. 真实业务理解 + +Core 不只是“基础字典”,它在真实育种业务里是项目台账:回答“谁在什么项目、什么地点、什么年度、做哪一次试验”。后续所有材料、田间布置、表型采集、样本检测、基因型结果,都需要回到 Core 上下文里解释。 + +最常见的业务链路是: + +```text +crop 作物 + -> program 长期育种项目 + -> trial 某一批试验/区域试验 + -> study 一次真正落地执行的试验 +``` + +例如: + +```text +crop: 水稻 +program: 水稻抗倒伏育种项目 +trial: 2026 湖北区域试验 +location: 荆门试验站 +season: 2026 春季 +study: 2026 荆门水稻抗倒伏田间试验 +``` + +这里最关键的是 `study`。`program` 更像长期项目,`trial` 是试验批次,`study` 才是具体执行单元。后续 `observation_unit`、`event`、`observation`、`plate`、`sample`、`variantset` 都会直接或间接挂到 `study` 上。 + +## 3. 用户角色和使用场景 + +| 角色 | 主要职责 | 典型入口 | 权限重点 | +| --- | --- | --- | --- | +| 项目管理员 | 维护作物、项目、人员、地点等基础台账 | crop、person、program、location 列表页 | 可新增/编辑基础数据,可停用未被引用的数据 | +| 试验负责人 | 创建 trial 和 study,维护联系人、季节、试验设计 | trial 列表、study 列表、Study 工作台 | 可创建试验上下文,可配置 study 联系人、season、变量清单 | +| 数据录入员 | 进入 Study 工作台继续录观测单元、表型、样本 | Study 工作台 | 主要使用下游入口,一般不能删除 Core 主数据 | +| 数据管理员 | 处理导入、导出、停用、删除校验、数据修复 | 导入导出页、数据质量页 | 可批量导入、导出、停用,有删除前引用检查权限 | + +### 3.1 项目管理员场景 + +项目管理员先建立 `crop`、`person`、`program`、`location`。例如创建“水稻抗倒伏育种项目”,选择 crop=水稻,负责人=张三。录完后,试验负责人才能在该项目下创建 trial 和 study。 + +### 3.2 试验负责人场景 + +试验负责人创建“2026 湖北区域试验”和“2026 荆门水稻抗倒伏田间试验”。创建 study 时选择 program、trial、location、season,保存后进入 Study 工作台。 + +### 3.3 数据录入员场景 + +数据录入员通常不从 crop/program 开始,而是从 Study 工作台进入,继续录 observation unit、observation、sample 等下游数据。 + +### 3.4 数据管理员场景 + +数据管理员负责批量导入 Core 数据、处理重复数据、做删除/停用前检查。例如某 trial 已有关联 study 时,不允许物理删除,只能停用。 + +## 用户为什么要录 Core + +| 用户问题 | 由哪些表回答 | 录完能做什么 | +| --- | --- | --- | +| 我们做什么作物? | `crop` | 限定项目、材料、变量、图谱的作物范围 | +| 谁负责项目和试验? | `person`、`program.lead_person_id`、`trial_contact`、`study_contact` | 项目责任追踪、联系人展示、权限和通知 | +| 这是哪个长期育种项目? | `program` | 聚合多个 trial/study、材料、样本、结果 | +| 这是哪一批试验? | `trial` | 管理区域试验、年度试验、批次试验 | +| 试验在哪里、哪一季做? | `location`、`season`、`study_season` | 支持多地点、多季节筛选和统计 | +| 哪一次试验真正落地执行? | `study` | 进入 Study 工作台,继续录观测单元、表型、样本、基因型 | + +## 6. 页面需求 + +Core 的核心页面不应该只是表单 CRUD,而应该围绕 `study` 做“Study 工作台”。 + +### 6.1 Study 工作台 + +```text +Study 工作台 +├─ 基本信息:名称、编号、类型、起止日期、负责人 +├─ 上下文:crop、program、trial、location、season +├─ 试验设计:experimental design、observation level、growth facility +├─ 联系人/附件:study_contact、data_link、external_references +├─ 观测单元:跳转 observation_unit +├─ 表型数据:跳转 observation / matrix +├─ 样本与样本板:跳转 sample / plate +├─ 基因型数据:跳转 variantset / callset / allele_call +└─ 导入导出:按 study 导入观测单元、表型、样本、基因型 +``` + +### 6.2 Crop 列表页 + +| 项目 | 需求 | +| --- | --- | +| 功能 | 查询 crop;新增、编辑、查看详情、停用 | +| 筛选 | 关键词搜索,按 crop_name 模糊匹配 | +| 表格字段 | `crop_name`、下游 program 数、下游 study 数、状态 | +| 操作 | 查看、编辑、停用 | +| 验收标准 | crop 已被 program/germplasm/study 引用时,不允许物理删除 | + +### 6.3 Person 列表页 + +| 项目 | 需求 | +| --- | --- | +| 功能 | 查询人员;新增、编辑、查看详情、停用 | +| 筛选 | 姓名、邮箱、机构 | +| 表格字段 | 姓名、邮箱、电话、机构、负责项目数、参与 study 数 | +| 操作 | 查看、编辑、停用 | +| 验收标准 | 邮箱格式正确;作为负责人或联系人被引用时,不允许物理删除 | + +### 6.4 Program 列表页 + +| 项目 | 需求 | +| --- | --- | +| 功能 | 查询 program;新增、编辑、查看详情、停用 | +| 筛选 | crop、负责人、program_type、关键词 | +| 表格字段 | `name`、`abbreviation`、crop、负责人、trial 数、study 数 | +| 操作 | 查看、编辑、停用、查看 trial、查看 study | +| 验收标准 | 创建 program 时必须选择 crop;负责人来自 person 选择器 | + +### 6.5 Trial 列表页 + +| 项目 | 需求 | +| --- | --- | +| 功能 | 查询 trial;新增、编辑、查看详情、停用 | +| 筛选 | crop、program、active、起止日期、关键词 | +| 表格字段 | `trial_name`、program、crop、`start_date`、`end_date`、active | +| 操作 | 查看、编辑、停用、查看 study | +| 验收标准 | 选择 program 后自动带出 crop;已被 study 引用时不允许物理删除 | + +### 6.6 Location 列表页 + +| 项目 | 需求 | +| --- | --- | +| 功能 | 查询 location;新增、编辑、查看详情、停用 | +| 筛选 | crop、program、location_type、country、关键词 | +| 表格字段 | `location_name`、location_type、country、program、crop、父级地点 | +| 操作 | 查看、编辑、停用、地图查看 | +| 验收标准 | 父级地点不能选择自己;坐标字段格式合法 | + +### 6.7 Season 列表页 + +| 项目 | 需求 | +| --- | --- | +| 功能 | 查询 season;新增、编辑、查看详情、停用 | +| 筛选 | year、season | +| 表格字段 | `year`、`season`、关联 study 数 | +| 操作 | 查看、编辑、停用 | +| 验收标准 | 同一年份内 season 名称不建议重复 | + +### 6.8 Study 列表页 + +| 项目 | 需求 | +| --- | --- | +| 功能 | 查询 study;新增、编辑、查看详情、停用;点击 study 名称进入 Study 工作台 | +| 筛选 | crop、program、trial、location、season、active、study_type、关键词 | +| 表格字段 | `study_name`、`study_code`、program、trial、location、`start_date`、`end_date`、active | +| 操作 | 查看、编辑、停用、进入工作台 | +| 验收标准 | 选择 program 后只展示该 program 下的 trial;选择 trial 后自动带出 crop;已有 observation_unit/sample/observation 时不能物理删除 | + +### 6.9 Study 新增/编辑页 + +| 区域 | 字段/功能 | 要求 | +| --- | --- | --- | +| 基本信息 | study_name、study_code、study_type、active、start_date、end_date | study_name 必填;结束日期不早于开始日期 | +| 上下文 | crop、program、trial、location、season | program、trial、crop、location 必选;program -> trial 联动 | +| 说明信息 | study_description、cultural_practices、observation_units_description、license、documentationurl | URL 格式校验 | +| 联系人 | study_contact | 多选 person,可增删 | +| 季节 | study_season | 可多选 season | +| 提交后 | 保存成功进入 Study 工作台 | 新建成功后自动跳转 | + +### 6.10 List 详情页 + +| 项目 | 需求 | +| --- | --- | +| 功能 | 维护 list 和 list_item | +| 页面形态 | 顶部 list 基本信息,下方 list_item 表格 | +| 操作 | 新增 item、批量导入 item、删除 item、排序 | +| 验收标准 | 同一 list 内 item 不重复 | + +## 哪些表是业务动作痕迹 + +| 动作 | 痕迹表 | 说明 | +| --- | --- | --- | +| 给 trial 添加联系人 | `trial_contact` | trial 与 person 的关系,不是独立主数据 | +| 给 study 添加联系人 | `study_contact` | study 与 person 的关系 | +| 给 study 绑定季节 | `study_season` | 一个 study 可关联多个 season | +| 给 study 绑定观测变量 | `study_variable` | 表示该 study 要采集哪些指标 | +| 给 trial 记录出版物 | `trial_publication` | trial 的文献/报告引用 | +| 给 list 添加成员 | `list_item` | list 的明细项,是分组动作留下的记录 | + +## 4. 核心业务流程 + +### 4.1 新建 Core 试验上下文流程 + +1. 用户创建 `crop`。 +2. 用户创建 `person`。 +3. 用户创建 `program`,并选择 `crop` 和负责人 `lead_person_id`。 +4. 用户创建 `location`,可选绑定 `program` 和 `crop`。 +5. 用户创建 `trial`,选择 `program` 后自动带出 `crop`。 +6. 用户创建 `season`。 +7. 用户创建 `study`,按 `program -> trial -> location -> season` 的顺序选择上下文。 +8. `study` 创建成功后,系统自动进入 Study 工作台。 + +### 4.2 业务流程图 + +```mermaid +flowchart TD + A["创建 crop"] --> B["创建 person"] + B --> C["创建 program
选择 crop / lead person"] + C --> D["创建 location
可选 program / crop"] + C --> E["创建 trial
选择 program 自动带出 crop"] + E --> F["创建 season"] + D --> G["创建 study
选择 program -> trial -> location -> season"] + F --> G + G --> H["进入 Study 工作台"] +``` + +## 5. 核心对象说明 + +### 5.1 crop 作物 + +#### 数据库录入 + +| 项目 | 要求 | +| --- | --- | +| 表 | `crop` | +| 前置依赖 | 无 | +| 主要字段 | 作物 ID、作物名称、公共作物名、描述 | +| 下游引用 | `program`、`location`、`trial`、`study`、`germplasm`、`observation_variable`、`genome_map` | + +#### 界面形态 + +列表页展示作物名称、描述、更新时间、下游引用数量。新增页是轻量表单,重点是作物名称唯一校验。详情页展示该作物下的项目、试验、研究、种质、图谱入口。 + +#### 校验规则 + +1. 作物名称必填且唯一。 +2. 已被下游引用的作物不能直接删除,只允许停用或提示引用关系。 + +### 5.2 person 人员 + +#### 数据库录入 + +| 项目 | 要求 | +| --- | --- | +| 表 | `person` | +| 前置依赖 | 无 | +| 主要字段 | 姓名、邮箱、机构、联系方式、角色 | +| 下游引用 | `program.lead_person_id`、`trial_contact`、`study_contact`、`list.list_owner_person_id` | + +#### 界面形态 + +列表页支持姓名、邮箱、机构筛选。新增/编辑页用联系人表单。详情页展示负责的 program、参与的 trial/study、拥有的 list。 + +#### 校验规则 + +1. 姓名必填。 +2. 邮箱格式正确。 +3. 同一邮箱不建议重复录入。 + +### 5.3 program 项目 + +#### 数据库录入 + +| 项目 | 要求 | +| --- | --- | +| 表 | `program` | +| 前置依赖 | `crop`,可选 `person` | +| 主要字段 | 项目名称、作物、负责人、缩写、目标、描述 | +| 下游引用 | `trial`、`study`、`location`、`crossing_project`、`seed_lot`、`plate`、`sample` | + +#### 界面形态 + +新增页分为“基本信息”和“负责人/作物”两组。作物和负责人使用搜索选择器。详情页 Tab 展示 trial、study、location、seed lot、sample。 + +#### 校验规则 + +1. 项目名称必填。 +2. 作物必选。 +3. 选择负责人时必须存在于 `person`。 + +### 5.4 location 地点 + +#### 数据库录入 + +| 项目 | 要求 | +| --- | --- | +| 表 | `location` | +| 前置依赖 | 可选 `crop`、`program`、父级 `location`、坐标 | +| 主要字段 | 地点名称、地点类型、国家/地区、经纬度、父级地点、所属项目 | +| 下游引用 | `study`、`seed_lot` | + +#### 界面形态 + +列表页支持地图/表格两种视图。新增页包含基本信息、行政区、坐标、父级地点。详情页展示该地点下的 study 和 seed lot。 + +#### 校验规则 + +1. 地点名称必填。 +2. 经纬度格式合法。 +3. 父级地点不能选择自己。 + +### 5.5 trial 试验批次 + +#### 数据库录入 + +| 项目 | 要求 | +| --- | --- | +| 表 | `trial` | +| 前置依赖 | `program`、`crop` | +| 主要字段 | 试验名称、项目、作物、开始/结束日期、联系人、出版物 | +| 下游引用 | `study`、`observation_unit`、`observation`、`plate`、`sample` | + +#### 界面形态 + +新增页包含基本信息、项目作物、联系人、出版物四个区域。联系人用可增删表格选择 `person`。详情页展示 study 列表和 phenotyping/genotyping 入口。 + +#### 校验规则 + +1. 试验名称必填。 +2. program 与 crop 必须匹配。 +3. 结束日期不能早于开始日期。 + +### 5.6 season 季节 + +#### 数据库录入 + +| 项目 | 要求 | +| --- | --- | +| 表 | `season` | +| 前置依赖 | 无 | +| 主要字段 | 季节名称、年份、开始日期、结束日期 | +| 下游引用 | `study_season`、部分 `observation` | + +#### 界面形态 + +列表页按年份和季节筛选。新增页是简单表单。详情页展示关联 study。 + +#### 校验规则 + +1. 名称和年份必填。 +2. 同一年份内季节名称不建议重复。 + +### 5.7 study 研究/试验实施单元 + +#### 数据库录入 + +| 项目 | 要求 | +| --- | --- | +| 表 | `study` | +| 前置依赖 | `crop`、`program`、`trial`、`location`,可选 `season` | +| 主要字段 | study 名称、study 类型、项目、试验、地点、季节、开始/结束日期、实验设计 | +| 下游引用 | `observation_unit`、`event`、`observation`、`plate`、`sample`、`variantset` | + +#### 界面形态 + +新增页是分组表单:基本信息、上下文、地点季节、实验设计、联系人。上下文字段按顺序联动:选择 program 后过滤 trial 和 crop,选择 trial 后带出 crop。详情页作为工作台,展示 observation unit、observation、sample、variantset。 + +#### 校验规则 + +1. study 名称必填。 +2. program、trial、crop、location 必选。 +3. trial 必须属于所选 program。 +4. study 是下游核心引用,删除必须强提示。 + +### 5.8 list / list_item 通用列表 + +#### 数据库录入 + +| 项目 | 要求 | +| --- | --- | +| 表 | `list`、`list_item` | +| 前置依赖 | 可选 `person` | +| 主要字段 | 列表名称、列表类型、owner、项目项 ID、项目项名称 | +| 下游引用 | 业务查询和分组 | + +#### 界面形态 + +列表页展示 list。详情页内嵌 list item 表格,支持手动添加、批量导入、排序。新增 item 时可选择目标类型,例如 germplasm、study、sample。 + +#### 校验规则 + +1. list 名称必填。 +2. list item 在同一 list 内不应重复。 +3. 如果绑定 owner,owner 必须存在于 `person`。 + +## 7. 字段需求 + +### crop + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 作物主键,新增时系统生成,也可导入时指定 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `crop_name` | varchar(255) | 文本框 | 作物名称,用于所有下拉选择展示 | 必填、建议唯一 | + +### person + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 人员主键 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `description` | varchar(255) | 多行文本 | 人员说明、职责补充 | 可选 | +| `email_address` | varchar(255) | 邮箱输入框 | 邮箱地址 | 邮箱格式校验,建议唯一 | +| `first_name` | varchar(255) | 文本框 | 名 | 与 `last_name` 至少填写一个 | +| `institute_name` | varchar(255) | 文本框/选择器 | 所属机构 | 可选 | +| `last_name` | varchar(255) | 文本框 | 姓 | 与 `first_name` 至少填写一个 | +| `mailing_address` | varchar(255) | 多行文本 | 通讯地址 | 可选 | +| `middle_name` | varchar(255) | 文本框 | 中间名 | 可选 | +| `phone_number` | varchar(255) | 电话输入框 | 联系电话 | 可选,格式提示 | +| `userid` | varchar(255) | 文本框 | 外部用户 ID 或登录名 | 可选,建议唯一 | + +### program + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 项目主键 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `abbreviation` | varchar(255) | 文本框 | 项目缩写 | 可选 | +| `documentationurl` | varchar(255) | URL 输入框 | 项目文档链接 | URL 格式校验 | +| `funding_information` | varchar(255) | 多行文本 | 经费来源说明 | 可选 | +| `name` | varchar(255) | 文本框 | 项目名称 | 必填 | +| `objective` | varchar(255) | 多行文本 | 项目目标 | 可选 | +| `program_type` | integer | 下拉框 | 项目类型枚举 | 可选,按 BrAPI 枚举 | +| `crop_id` | varchar(255) | 作物选择器 | 所属作物 | 必选,来源 `crop.id` | +| `lead_person_id` | varchar(255) | 人员选择器 | 项目负责人 | 可选,来源 `person.id` | + +### location + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 地点主键 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `abbreviation` | varchar(255) | 文本框 | 地点缩写 | 可选 | +| `coordinate_description` | varchar(255) | 多行文本 | 坐标描述 | 可选 | +| `coordinate_uncertainty` | varchar(255) | 文本框 | 坐标不确定性 | 可选 | +| `country_code` | varchar(255) | 国家代码选择器 | 国家代码 | 可选,建议 ISO 代码 | +| `country_name` | varchar(255) | 文本框 | 国家名称 | 可选,可由国家代码带出 | +| `documentationurl` | varchar(255) | URL 输入框 | 地点文档链接 | URL 格式校验 | +| `environment_type` | varchar(255) | 下拉框/文本框 | 环境类型 | 可选 | +| `exposure` | varchar(255) | 文本框 | 暴露条件 | 可选 | +| `institute_address` | varchar(255) | 多行文本 | 机构地址 | 可选 | +| `institute_name` | varchar(255) | 文本框 | 机构名称 | 可选 | +| `location_name` | varchar(255) | 文本框 | 地点名称 | 必填 | +| `location_type` | varchar(255) | 下拉框 | 地点类型,如 field、greenhouse、storage | 可选 | +| `site_status` | varchar(255) | 下拉框 | 地点状态 | 可选 | +| `slope` | varchar(255) | 文本框 | 坡度 | 可选 | +| `topography` | varchar(255) | 文本框 | 地形 | 可选 | +| `coordinates_id` | varchar(255) | 坐标选择器/地图取点 | 坐标对象 | 可选,来源 `geojson/coordinate` | +| `crop_id` | varchar(255) | 作物选择器 | 关联作物 | 可选,来源 `crop.id` | +| `parent_location_id` | varchar(255) | 地点选择器 | 父级地点 | 可选,不能选择自己 | +| `program_id` | varchar(255) | 项目选择器 | 所属项目 | 可选,来源 `program.id` | + +### trial + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 试验主键 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `active` | boolean | 开关 | 是否启用 | 默认启用 | +| `documentationurl` | varchar(255) | URL 输入框 | 试验文档链接 | URL 格式校验 | +| `end_date` | timestamp | 日期选择器 | 结束日期 | 不早于 `start_date` | +| `start_date` | timestamp | 日期选择器 | 开始日期 | 可选 | +| `trial_description` | varchar(255) | 多行文本 | 试验描述 | 可选 | +| `trial_name` | varchar(255) | 文本框 | 试验名称 | 必填 | +| `trialpui` | varchar(255) | 文本框 | 试验永久标识 | 可选,建议唯一 | +| `crop_id` | varchar(255) | 作物选择器 | 所属作物 | 必选,来源 `crop.id` | +| `program_id` | varchar(255) | 项目选择器 | 所属项目 | 必选,来源 `program.id` | + +### season + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 季节主键 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `season` | varchar(255) | 文本框/下拉框 | 季节名称,如 Spring、Summer | 必填 | +| `year` | integer | 年份选择器 | 年份 | 必填,四位年份 | + +### study + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | study 主键 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `active` | boolean | 开关 | 是否启用 | 默认启用 | +| `cultural_practices` | varchar(255) | 多行文本 | 栽培管理说明 | 可选 | +| `documentationurl` | varchar(255) | URL 输入框 | study 文档链接 | URL 格式校验 | +| `end_date` | timestamp | 日期选择器 | 结束日期 | 不早于 `start_date` | +| `license` | varchar(255) | 文本框 | 数据许可证 | 可选 | +| `observation_units_description` | varchar(255) | 多行文本 | 观测单元说明 | 可选 | +| `start_date` | timestamp | 日期选择器 | 开始日期 | 可选 | +| `study_code` | varchar(255) | 文本框 | study 编码 | 可选,建议同项目内唯一 | +| `study_description` | varchar(255) | 多行文本 | study 描述 | 可选 | +| `study_name` | varchar(255) | 文本框 | study 名称 | 必填 | +| `studypui` | varchar(255) | 文本框 | study 永久标识 | 可选,建议唯一 | +| `study_type` | varchar(255) | 下拉框 | study 类型 | 可选 | +| `crop_id` | varchar(255) | 作物选择器 | 所属作物 | 必选,来源 `crop.id` | +| `location_id` | varchar(255) | 地点选择器 | 实施地点 | 必选,来源 `location.id` | +| `program_id` | varchar(255) | 项目选择器 | 所属项目 | 必选,来源 `program.id` | +| `trial_id` | varchar(255) | 试验选择器 | 所属 trial | 必选,来源 `trial.id` | + +### list + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 列表主键 | 必填、唯一 | +| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 | +| `date_created` | timestamp | 只读日期时间 | 创建时间 | 系统自动写入 | +| `date_modified` | timestamp | 只读日期时间 | 修改时间 | 系统自动更新 | +| `description` | varchar(255) | 多行文本 | 列表描述 | 可选 | +| `list_name` | varchar(255) | 文本框 | 列表名称 | 必填 | +| `list_owner_name` | varchar(255) | 文本框 | 列表 owner 名称 | 可选,可由 owner person 带出 | +| `list_source` | varchar(255) | 文本框 | 列表来源 | 可选 | +| `list_type` | integer | 下拉框 | 列表类型 | 必填,按 BrAPI 枚举 | +| `list_owner_person_id` | varchar(255) | 人员选择器 | 列表 owner | 可选,来源 `person.id` | + +### list_item + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 列表项主键 | 必填、唯一 | +| `item` | varchar(255) | 文本框/对象选择器 | 列表项值,可存目标对象 ID 或文本 | 必填,同一 list 内不重复 | +| `list_id` | varchar(255) | List 选择器 | 所属列表 | 必选,来源 `list.id` | + +### trial_contact + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `trial_db_id` | varchar(255) | Trial 选择器 | 所属 trial | 必选,来源 `trial.id` | +| `person_db_id` | varchar(255) | 人员选择器 | 联系人 | 必选,来源 `person.id` | + +### trial_publication + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `id` | varchar(255) | 隐藏/只读 | 出版物记录主键 | 必填、唯一 | +| `publicationpui` | varchar(255) | 文本框 | 出版物 PUI | 可选 | +| `publication_reference` | varchar(255) | 文本框/URL | 出版物引用 | 可选 | +| `trial_id` | varchar(255) | Trial 选择器 | 所属 trial | 必选,来源 `trial.id` | + +### study_contact + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `study_db_id` | varchar(255) | Study 选择器 | 所属 study | 必选,来源 `study.id` | +| `person_db_id` | varchar(255) | 人员选择器 | 联系人 | 必选,来源 `person.id` | + +### study_season + +| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 | +| --- | --- | --- | --- | --- | +| `study_db_id` | varchar(255) | Study 选择器 | 所属 study | 必选,来源 `study.id` | +| `season_db_id` | varchar(255) | Season 选择器 | 关联季节 | 必选,来源 `season.id` | + +## 8. 接口需求 + +接口命名可以按项目现有 BrAPI Controller 适配;本节描述前端页面需要的能力,不强制限定最终 URL 必须完全一致。 + +### 8.1 CRUD 接口 + +| 对象 | 接口 | 用途 | +| --- | --- | --- | +| crop | `GET /crops` | crop 分页查询 | +| crop | `POST /crops` | 新增 crop | +| crop | `GET /crops/{id}` | crop 详情 | +| crop | `PUT /crops/{id}` | 编辑 crop | +| crop | `DELETE /crops/{id}` | 删除或停用 crop,需做引用检查 | +| person | `GET /persons` | person 分页查询 | +| person | `POST /persons` | 新增 person | +| person | `GET /persons/{id}` | person 详情 | +| person | `PUT /persons/{id}` | 编辑 person | +| person | `DELETE /persons/{id}` | 删除或停用 person,需做引用检查 | +| program | `GET /programs` | program 分页查询 | +| program | `POST /programs` | 新增 program | +| program | `GET /programs/{id}` | program 详情 | +| program | `PUT /programs/{id}` | 编辑 program | +| program | `DELETE /programs/{id}` | 删除或停用 program,需做引用检查 | +| location | `GET /locations` | location 分页查询 | +| location | `POST /locations` | 新增 location | +| location | `GET /locations/{id}` | location 详情 | +| location | `PUT /locations/{id}` | 编辑 location | +| trial | `GET /trials` | trial 分页查询 | +| trial | `POST /trials` | 新增 trial | +| trial | `GET /trials/{id}` | trial 详情 | +| trial | `PUT /trials/{id}` | 编辑 trial | +| trial | `DELETE /trials/{id}` | 删除或停用 trial,需检查 study 引用 | +| season | `GET /seasons` | season 查询 | +| season | `POST /seasons` | 新增 season | +| season | `PUT /seasons/{id}` | 编辑 season | +| study | `GET /studies` | study 分页查询 | +| study | `POST /studies` | 新增 study | +| study | `GET /studies/{id}` | study 详情 | +| study | `PUT /studies/{id}` | 编辑 study | +| study | `DELETE /studies/{id}` | 删除或停用 study,需检查下游引用 | +| list | `GET /lists` | list 查询 | +| list | `POST /lists` | 新增 list | +| list | `PUT /lists/{id}` | 编辑 list | +| list item | `POST /lists/{id}/items` | 给 list 添加 item | +| list item | `DELETE /lists/{id}/items/{itemId}` | 删除 list item | + +### 8.2 详情聚合接口 + +| 接口 | 用途 | 返回重点 | +| --- | --- | --- | +| `GET /programs/{id}/trials` | 查询 program 下 trial | trial 基础信息、active | +| `GET /programs/{id}/studies` | 查询 program 下 study | study 基础信息、location、trial | +| `GET /trials/{id}/studies` | 查询 trial 下 study | study 列表 | +| `GET /studies/{id}/workbench` | Study 工作台聚合信息 | study 基本信息、联系人、season、observation_unit 数、observation 数、sample 数、variantset 数 | +| `GET /studies/{id}/contacts` | study 联系人 | person 列表 | +| `GET /studies/{id}/seasons` | study 季节 | season 列表 | + +### 8.3 选择器接口 + +| 接口 | 用途 | 参数 | +| --- | --- | --- | +| `GET /selectors/crops` | crop 搜索下拉 | `keyword` | +| `GET /selectors/persons` | person 搜索下拉 | `keyword`、`instituteName` | +| `GET /selectors/programs` | program 搜索下拉 | `keyword`、`cropId` | +| `GET /selectors/trials` | trial 搜索下拉 | `keyword`、`programId`、`cropId`、`active` | +| `GET /selectors/locations` | location 搜索下拉 | `keyword`、`programId`、`cropId`、`locationType` | +| `GET /selectors/seasons` | season 搜索下拉 | `year`、`keyword` | +| `GET /selectors/studies` | study 搜索下拉 | `keyword`、`programId`、`trialId`、`locationId`、`active` | + +### 8.4 导入导出接口 + +| 接口 | 用途 | +| --- | --- | +| `GET /imports/templates/core/{objectType}` | 下载 Core 对象导入模板 | +| `POST /imports/core/{objectType}/preview` | 上传文件并预校验,不落库 | +| `POST /imports/core/{objectType}/commit` | 确认导入 | +| `GET /exports/core/{objectType}` | 导出当前筛选结果 | +| `GET /imports/{jobId}/errors` | 获取导入错误报告 | + +## 9. 联动规则 + +| 场景 | 联动规则 | +| --- | --- | +| 创建 program | 必须先选择 crop;负责人从 person 选择器选择 | +| 创建 trial | 选择 program 后自动带出 crop;trial 的 crop 必须与 program.crop_id 一致 | +| 创建 location | 选择 program 后可自动带出 crop,但允许地点作为公共地点不绑定 program | +| 创建 study | 先选 program,再按 program 过滤 trial;选择 trial 后自动带出 crop | +| 创建 study | location 可按 program/crop 过滤;season 支持多选 | +| 编辑 study | 如果已有 observation_unit/sample/observation,变更 program/trial/crop/location 前必须二次确认并检查一致性 | +| Study 工作台 | 入口必须携带 study_id;下游新增 observation_unit、sample、variantset 默认继承该 study 上下文 | +| 外键显示 | 前端展示名称,提交保存 ID;表格列需要同时支持名称展示和 ID 调试查看 | + +## 10. 删除/停用规则 + +| 对象 | 删除/停用规则 | +| --- | --- | +| crop | 已被 program、location、trial、study、germplasm、observation_variable、genome_map 引用时,不允许物理删除 | +| person | 已作为 program 负责人、trial 联系人、study 联系人、list owner 时,不允许物理删除 | +| program | 已有关联 trial、study、location、crossing_project、seed_lot、plate、sample 时,不允许物理删除 | +| location | 已被 study 或 seed_lot 引用时,不允许物理删除 | +| trial | 已被 study、observation_unit、observation、plate、sample 引用时,不允许物理删除 | +| season | 已被 study_season 或 observation 引用时,不允许物理删除 | +| study | 已被 observation_unit、event、observation、plate、sample、variantset 引用时,不允许物理删除 | +| list | 已有 list_item 时,删除前必须提示;可先清空明细再删除 | + +删除失败时,后端应返回被引用对象类型和数量,前端弹窗展示,例如: + +```text +该 study 已被以下数据引用,不能删除: +- observation_unit: 120 +- observation: 560 +- sample: 96 +请先处理下游数据,或将 study 停用。 +``` + +## 11. 导入导出需求 + +### 11.1 导入对象 + +Core 模块至少支持以下对象导入: + +```text +crop +person +program +location +trial +season +study +list +list_item +trial_contact +study_contact +study_season +``` + +### 11.2 导入流程 + +1. 用户选择对象类型,例如 `study`。 +2. 下载 CSV/Excel 模板。 +3. 用户填写模板并上传。 +4. 系统做预校验:字段完整性、必填、格式、外键是否存在、联动关系是否一致。 +5. 预览页展示可导入行、错误行、警告行。 +6. 用户确认后提交导入。 +7. 导入完成后生成导入报告。 + +### 11.3 导入模板要求 + +| 要求 | 说明 | +| --- | --- | +| 字段名 | 使用数据库字段名作为模板列名 | +| 外键列 | 支持填写 ID;可额外支持名称匹配,但名称重复时必须报错 | +| 错误报告 | 返回行号、字段名、错误原因、建议修正方式 | +| 幂等策略 | 可按 ID 更新;无 ID 时新增 | +| 权限 | 数据录入员可导入 study 下游数据;Core 主数据导入建议仅项目管理员/数据管理员可操作 | + +### 11.4 导出需求 + +1. 所有列表页支持导出当前筛选结果。 +2. 导出文件应包含 ID 和展示名称。 +3. study 导出应支持导出工作台摘要,包括联系人、season、下游数据数量。 +4. 导出大数据量时应走异步任务。 + +## 12. 验收标准 + +### 12.1 前端验收标准 + +1. 所有列表页支持分页、关键词搜索、基础筛选。 +2. 所有外键字段前端展示名称,提交保存 ID。 +3. 创建 program 时,必须选择 crop。 +4. 创建 trial 时,选择 program 后自动带出 crop。 +5. 创建 study 时,必须选择 program、trial、crop、location。 +6. Study 列表页支持按 crop、program、trial、location、season、active 筛选。 +7. 点击 study 名称或“进入工作台”按钮后进入 Study 工作台。 +8. Study 工作台可以看到观测单元、表型、样本、基因型入口。 +9. 删除被引用数据时,前端展示引用详情,不允许静默失败。 +10. 导入预校验结果能展示错误行、错误字段、错误原因。 + +### 12.2 后端验收标准 + +1. 创建 program 时校验 `crop_id` 存在。 +2. 创建 trial 时校验 `program_id` 存在,且 trial.crop_id 与 program.crop_id 一致。 +3. 创建 study 时校验 `program_id`、`trial_id`、`crop_id`、`location_id` 存在。 +4. 创建 study 时校验 trial 属于所选 program。 +5. 删除 crop/program/trial/study 前必须检查下游引用。 +6. `GET /studies/{id}/workbench` 能返回工作台所需聚合数量。 +7. 选择器接口支持 keyword 模糊搜索和上游过滤参数。 +8. 导入接口支持 preview 和 commit 两阶段。 + +### 12.3 测试点 + +1. 选择 program 后,trial 下拉框只展示该 program 下的数据。 +2. 选择 trial 后,系统自动带出 crop。 +3. 已被 study 引用的 trial 不能直接删除。 +4. 已被 observation_unit、sample、observation 引用的 study 不能直接删除。 +5. study 创建成功后自动进入 Study 工作台。 +6. 修改 study 的 program/trial/crop 时,如果已有下游数据,必须提示风险。 +7. 导入 study 时,如果 trial 不属于 program,应报错并指出行号。 +8. 导入 location 时,如果 parent_location_id 指向自己,应报错。 +9. 导出列表时,筛选条件必须生效。 +10. 所有接口返回的分页字段一致,便于前端表格复用。 diff --git a/docs/requirements/02-germplasm-seed-entry-requirements.md b/docs/requirements/02-germplasm-seed-entry-requirements.md new file mode 100644 index 0000000..784ff84 --- /dev/null +++ b/docs/requirements/02-germplasm-seed-entry-requirements.md @@ -0,0 +1,609 @@ +# Germplasm / Seed 模块专业数据录入需求文档 V2 + +## 1. 文档目的 + +本文档用于指导 Germplasm / Seed 模块的前端页面、后端接口、字段校验、数据导入、测试验收设计。本文档不再只描述数据库表关系,而是从真实育种业务出发,解释每个字段的业务意义、录入方式、控件建议、校验规则和上下游影响。 + +## 2. 模块定位 + +Germplasm / Seed 模块描述育种材料的生命周期: + +```text +材料身份 -> 材料属性 -> 杂交计划/实际杂交 -> 亲本 -> 系谱 -> 种子批次 -> 库存流转 -> 被 study / observation_unit 使用 +``` + +核心概念如下: + +| 概念 | 业务含义 | 主要表 | +| ---------------------------- | --------------------------------------------------------- | ------------------------------------------------------------ | +| Germplasm 材料身份 | 一个品种、品系、后代材料、种质资源的身份信息 | `germplasm` | +| Germplasm Attribute 材料属性 | 材料自身稳定特征,如抗性、熟期、籽粒硬度、基因/QTL 标记等 | `germplasm_attribute_definition`、`germplasm_attribute_value` | +| Cross 杂交动作 | 一次计划杂交或实际杂交,包含亲本、状态、项目归属 | `crossing_project`、`cross_entity`、`cross_parent` | +| Pedigree 系谱 | 材料之间的亲子、同胞、后代关系 | `pedigree_node`、`pedigree_edge` | +| SeedLot 种子批次 | 某个材料或杂交组合对应的一批实物种子,有数量、单位、库位 | `seed_lot`、`seed_lot_content_mixture` | +| Transaction 库存流转 | 入库、出库、转移、分装、合并、消耗等动作流水 | `seed_lot_transaction` | + +## 3. 推荐业务流程 + +```text +1. 维护育种方法 breeding_method +2. 维护材料属性定义 germplasm_attribute_definition +3. 创建材料 germplasm +4. 给材料补充属性值 germplasm_attribute_value +5. 如涉及杂交,创建 crossing_project +6. 创建计划杂交 cross_entity(planned=true) +7. 给计划杂交录入亲本 cross_parent +8. 实际完成杂交后,创建实际杂交 cross_entity(planned=false, planned_cross_id=计划杂交) +9. 如产生后代材料,创建新的 germplasm,并补充 pedigree_node / pedigree_edge +10. 如产生实物种子,创建 seed_lot +11. 在 seed_lot_content_mixture 中描述批次组成 +12. 后续库存变化通过 seed_lot_transaction 记录 +13. seed_lot / germplasm / cross_entity 后续可作为 observation_unit 的材料来源 +``` + +## 4. 页面总体设计 + +### 4.1 Germplasm 材料详情页 + +```text +Germplasm 详情页 +├─ 基本信息:名称、默认显示名、PUI、accession、作物、育种方法 +├─ 分类信息:genus、species、subtaxa、country_of_origin_code +├─ 来源信息:acquisition_date、seed_source、seed_source_description、collection +├─ 属性值:germplasm_attribute_value +├─ 系谱:pedigree_node / pedigree_edge 树图 +├─ 作为亲本:cross_parent +├─ 种子批次:seed_lot_content_mixture -> seed_lot +└─ 下游使用:observation_unit、reference_set、sample 追踪 +``` + +### 4.2 SeedLot 库存详情页 + +```text +SeedLot 详情页 +├─ 当前库存:amount、units、location、storage_location +├─ 批次组成:germplasm / cross / mixture_percentage +├─ 出入库操作:入库、出库、转移、分装、合并、消耗 +├─ 流转流水:seed_lot_transaction +└─ 下游使用:哪些 study / observation_unit 使用了该批种子 +``` + +### 4.3 CrossingProject 杂交项目工作台 + +```text +CrossingProject 工作台 +├─ 项目信息:name、program、description +├─ 计划杂交:cross_entity(planned=true) +├─ 实际杂交:cross_entity(planned=false) +├─ 亲本:cross_parent +├─ 后代材料:germplasm / pedigree_node +└─ 产生种子批次:seed_lot +``` + +--- + +# 5. 字段级专业录入需求 + +## 5.1 breeding_method 育种方法 + +### 业务说明 + +`breeding_method` 是育种方法字典,用来说明某个 germplasm 是通过什么方式形成的,例如杂交选育、回交、自交系选育、诱变、转基因、克隆选择等。它不是一次具体杂交动作,而是材料来源方法的分类。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| -------------- | -------------------------------------------- | -------------------------------- | --------- | -------------------------------- | +| `id` | 育种方法主键,系统内部唯一标识 | 新增时系统生成;导入时可允许指定 | 隐藏/只读 | 必填、唯一;编辑时不允许修改 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许前端手填 | +| `abbreviation` | 方法缩写,如 MB、BC、DH | 用户录入 | 文本框 | 可选;建议同一用户下唯一 | +| `description` | 方法解释,如“回交用于恢复目标基因” | 用户录入 | 多行文本 | 可选,限制长度 | +| `name` | 方法名称,如 Male Backcross、Doubled Haploid | 用户录入 | 文本框 | 必填;建议唯一;作为下拉展示名称 | + +### 页面与交互 + +- 列表页展示:方法名称、缩写、描述、使用材料数量。 +- 新增页为简单字典表单。 +- 删除前检查是否被 `germplasm.breeding_method_id` 引用;已引用时不允许物理删除,只允许停用。 + +--- + +## 5.2 germplasm 种质 / 材料身份 + +### 业务说明 + +`germplasm` 是材料身份证,描述一个品种、品系、亲本、后代材料、遗传资源或研究材料“是谁”。它不表示库存数量,库存数量由 `seed_lot` 表达。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------------------------- | ------------------------------------------------------------ | -------------------------------------- | ----------------- | --------------------------------------------------------- | +| `id` | 种质主键,系统内部唯一标识 | 系统生成;导入可指定 | 隐藏/只读 | 必填、唯一;编辑不可随意修改 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `accession_number` | 材料在种质库/机构内的 accession 编号,如 PI 113869 | 用户录入或导入 | 文本框 | 可选;建议同一 crop / institution 下唯一 | +| `acquisition_date` | 材料进入本系统或本机构的获取日期 | 用户录入 | 日期选择器 | 可选;不得晚于当前日期太多,导入时允许缺月/缺日需统一规则 | +| `acquisition_source_code` | 获取来源编码,如采集、引进、交换、繁殖等 | 用户选择 | 下拉框 | 可选;值来自 BrAPI/MCPD 枚举或系统字典 | +| `biological_status_of_accession_code` | 材料生物状态,如野生、地方品种、育种材料、改良品种、突变体等 | 用户选择 | 下拉框 | 可选;使用受控枚举,不建议自由输入 | +| `collection` | 材料所属集合、群体、panel 或 collection | 用户录入/选择 | 文本框/选择器 | 可选;可用于分组筛选 | +| `country_of_origin_code` | 原产国或育成/选育国家代码 | 用户选择 | 国家代码选择器 | 可选;建议使用 ISO 3166-1 三字母代码 | +| `default_display_name` | 系统默认展示名,给下拉框、表格、详情标题使用 | 用户录入,可由 germplasm_name 自动带出 | 文本框 | 与 `germplasm_name` 至少填一个;建议必填 | +| `documentationurl` | 材料说明文档、外部数据库页面或 DOI 链接 | 用户录入 | URL 输入框 | 可选;校验 URL 格式 | +| `genus` | 属名,如 Oryza、Triticum | 用户录入/字典选择 | 文本框/物种选择器 | 可选;建议首字母大写 | +| `germplasm_name` | 材料名称,可以是品种名、品系名、后代编号 | 用户录入 | 文本框 | 与 `default_display_name` 至少填一个;不强制全局唯一 | +| `germplasmpui` | 永久唯一标识,通常是 DOI、URI 或全局唯一编码 | 用户录入/外部导入 | 文本框/URL 输入框 | 可选;若填写必须唯一;建议用于跨系统交换 | +| `germplasm_preprocessing` | 材料用于试验前的统一处理说明,如消毒、催芽、低温处理 | 用户录入 | 文本框/多行文本 | 可选 | +| `mls_status` | 多边系统 MLS 状态,涉及植物遗传资源交换协议 | 用户选择 | 下拉框 | 可选;普通业务可隐藏到高级信息 | +| `seed_source` | 材料来源标识,如来源机构+accession,或亲本组合描述 | 用户录入 | 文本框 | 可选;注意它不是库存批次,不等于 seed_lot | +| `seed_source_description` | 材料来源详细说明 | 用户录入 | 多行文本 | 可选 | +| `species` | 种名,如 sativa、aestivum | 用户录入/物种字典 | 文本框 | 可选;建议小写 | +| `species_authority` | 种名命名权威,如 L. | 用户录入 | 文本框 | 可选 | +| `subtaxa` | 亚种、变种、品种群、line 等更细分类 | 用户录入 | 文本框 | 可选 | +| `subtaxa_authority` | 亚种/变种命名权威 | 用户录入 | 文本框 | 可选 | +| `breeding_method_id` | 该材料形成所使用的育种方法 | 从 breeding_method 选择 | 搜索选择器 | 可选;必须引用存在的 breeding_method | +| `crop_id` | 所属作物 | 从 crop 选择 | 作物选择器 | 必填;后续 trial/study/attribute 应尽量同 crop | + +### 录入建议 + +- 新建材料时,第一屏只放核心字段:`crop_id`、`germplasm_name`、`default_display_name`、`germplasmpui`、`accession_number`、`breeding_method_id`。 +- 分类与来源信息放在“高级信息”或“来源信息”分组。 +- `germplasmpui`、`accession_number`、`germplasm_name` 三者不要混为一谈: + - `germplasm_name` 是人看的名字; + - `accession_number` 是机构内编号; + - `germplasmpui` 是跨系统长期唯一标识。 + +### 验收标准 + +1. 新增 germplasm 时,必须选择 crop。 +2. `germplasm_name` 和 `default_display_name` 至少填写一个。 +3. 下拉选择材料时展示 `default_display_name`,辅助展示 accession number / PUI。 +4. 如果 germplasm 已被 seed lot、cross parent、observation unit 引用,不允许物理删除。 + +--- + +## 5.3 germplasm_attribute_definition 属性定义 + +### 业务说明 + +属性定义描述“材料可以有哪些稳定属性”。这些属性通常不是环境依赖的田间观测值,而是材料自身特征,例如籽粒颜色、抗病基因、硬度、熟期类型、特定 QTL、分子标记结果等。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ---------------------- | ------------------------------------------------------------ | ---------------------------- | ----------------- | ------------------------------------- | +| `id` | 属性定义主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `default_value` | 属性默认值 | 用户录入 | 动态输入框 | 可选;按 datatype / scale 校验 | +| `documentationurl` | 属性说明文档链接 | 用户录入 | URL 输入框 | 可选;校验 URL | +| `growth_stage` | 属性适用生长阶段,如 flowering | 用户录入/选择 | 下拉框/文本框 | 可选 | +| `institution` | 提交或维护该属性定义的机构 | 用户录入 | 文本框 | 可选 | +| `language` | 定义语言,如 zh、en | 用户选择 | 下拉框 | 可选;建议 ISO 639-1 | +| `scientist` | 提交该属性定义的科学家或负责人 | 用户录入/人员选择 | 文本框/人员选择器 | 可选 | +| `status` | 属性状态,如 recommended、obsolete、legacy | 用户选择 | 下拉框 | 可选;推荐使用枚举 | +| `submission_timestamp` | 属性定义提交时间 | 系统默认当前时间,可手动调整 | 日期时间选择器 | 可选;新增默认当前时间 | +| `crop_id` | 适用作物 | 从 crop 选择 | 作物选择器 | 可选;若填写,下游材料应同 crop | +| `method_id` | 属性测定方法 | 从 method 选择 | 方法选择器 | 可选;若填写,属性值录入按该方法解释 | +| `ontology_id` | 所属本体 | 从 ontology 选择 | 本体选择器 | 可选 | +| `scale_id` | 值标尺/单位/有效值范围 | 从 scale 选择 | 标尺选择器 | 可选;若填写,属性值必须按 scale 校验 | +| `trait_id` | 关联性状 | 从 trait 选择 | 性状选择器 | 可选 | +| `attribute_category` | 属性分类,如 Morphological、Genetic、Quality | 用户选择/录入 | 下拉框/文本框 | 可选;建议字典化 | +| `code` | 属性代码,便于导入导出 | 用户录入 | 文本框 | 可选;建议唯一 | +| `datatype` | 属性值数据类型,如 text、numeric、date、boolean、categorical | 用户选择 | 下拉框 | 必填 | +| `description` | 属性解释 | 用户录入 | 多行文本 | 可选 | +| `name` | 属性名称 | 用户录入 | 文本框 | 必填;作为属性选择器展示名称 | +| `pui` | 属性永久标识 | 用户录入 | 文本框/URL 输入框 | 可选;建议唯一 | +| `uri` | 属性 URI | 用户录入 | URL 输入框 | 可选;校验 URL | + +### 录入建议 + +- 属性定义页面本质是“属性字典配置”。 +- 前端应根据 `datatype` 动态决定属性值录入控件: + - numeric:数字输入框; + - categorical:下拉框; + - date:日期选择器; + - boolean:开关; + - text:文本框。 +- 若绑定了 `scale_id`,则优先按 scale 的单位、上下限、有效分类值校验。 + +--- + +## 5.4 germplasm_attribute_value 属性值 + +### 业务说明 + +属性值是“某个 germplasm 在某个属性上的实际取值”。它不是属性定义,也不是 observation。它适合记录材料相对稳定、不强依赖环境的特征。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ----------------- | ------------------------------ | -------------------------------------- | ---------- | ---------------------------------------- | +| `id` | 属性值主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `determined_date` | 属性值被测定或确认的日期 | 用户录入 | 日期选择器 | 可选;多次测定时必须填写以区分记录 | +| `value` | 某个材料在某个属性上的实际取值 | 用户录入 | 动态控件 | 必填;按 attribute datatype / scale 校验 | +| `attribute_id` | 属性定义 | 从 germplasm_attribute_definition 选择 | 属性选择器 | 必选;必须存在 | +| `germplasm_id` | 所属材料 | 从 germplasm 选择 | 材料选择器 | 必选;必须存在 | + +### 录入建议 + +- 推荐嵌入 Germplasm 详情页的“属性值”Tab。 +- 支持批量导入,模板列建议为:`germplasm_id/germplasm_name`、`attribute_code/attribute_name`、`value`、`determined_date`。 +- 同一个 germplasm + attribute 可以允许多次测定,但页面必须显示测定日期、来源和最新值标记。 + +--- + +## 5.5 crossing_project 杂交项目 + +### 业务说明 + +`crossing_project` 是某个育种项目下的一组杂交任务集合。它不是一次杂交,而是一个杂交工作台,例如“2026 抗倒伏杂交项目”。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| -------------- | ------------------ | ------------------ | ---------- | ----------------------------- | +| `id` | 杂交项目主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `description` | 杂交项目说明 | 用户录入 | 多行文本 | 可选 | +| `name` | 杂交项目名称 | 用户录入 | 文本框 | 必填;同一 program 下建议唯一 | +| `program_id` | 所属育种项目 | 从 program 选择 | 项目选择器 | 必选;必须存在 | + +### 页面与交互 + +- 详情页应展示计划杂交、实际杂交、潜在亲本、后代材料、产生的 seed lot。 +- 创建 cross 时应自动带入 crossing_project_id。 + +--- + +## 5.6 cross_entity 计划杂交 / 实际杂交 + +### 业务说明 + +`cross_entity` 统一承载计划杂交和实际杂交。通过 `planned` 字段区分计划与实际,通过 `planned_cross_id` 指向来源计划。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| --------------------- | ------------------------------------------- | ------------------------ | -------------- | --------------------------------------- | +| `id` | cross 主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `cross_type` | 杂交类型,如 biparental、self、backcross 等 | 用户选择 | 下拉框 | 可选;值来自枚举字典 | +| `name` | cross 名称,如 A × B、A/B、Cross-2026-001 | 用户录入或自动生成 | 文本框 | 必填;同一 crossing project 下建议唯一 | +| `planned` | 是否为计划杂交 | 页面根据入口自动设置 | 开关/分段控件 | 必填;计划杂交为 true,实际杂交为 false | +| `status` | 状态,如 TODO、DONE、SKIPPED、FAILED | 用户选择/系统更新 | 下拉框 | 可选;计划杂交常用 TODO/DONE/SKIPPED | +| `crossing_project_id` | 所属杂交项目 | 从 crossing_project 选择 | 杂交项目选择器 | 必选 | +| `planned_cross_id` | 实际杂交来源的计划杂交 | 从 cross_entity 选择 | Cross 选择器 | 可选;不能选择自己;实际杂交建议填写 | + +### 录入建议 + +- 页面上分成“计划杂交”和“实际杂交”两个入口,但后端都保存到 `cross_entity`。 +- 创建计划杂交时:`planned=true`,`planned_cross_id=null`。 +- 完成实际杂交时:`planned=false`,`planned_cross_id=原计划杂交 id`。 +- 亲本不要直接塞在 cross 主表字段中,应通过 `cross_parent` 维护,便于支持多亲本和 observation_unit 亲本来源。 + +--- + +## 5.7 cross_parent 杂交亲本 + +### 业务说明 + +`cross_parent` 表示某个 cross 的亲本。亲本可以来自 `germplasm`,也可以来自某个 `observation_unit`,例如田间某一株实际被选作父本/母本。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| --------------------- | --------------------------------------------------- | ---------------------------------- | ----------------- | ------------------------------------- | +| `id` | 亲本记录主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `parent_type` | 亲本角色,如 MALE、FEMALE、SELF、POPULATION、CLONAL | 用户选择 | 下拉框 | 必填;使用枚举 | +| `cross_id` | 所属 cross | 从 cross_entity 选择或由详情页带入 | Cross 选择器/隐藏 | 必选 | +| `crossing_project_id` | 所属 crossing project | 由 cross 自动带出 | 只读/隐藏 | 可选;如填写必须与 cross 一致 | +| `germplasm_id` | 亲本材料 | 从 germplasm 选择 | 材料选择器 | 与 `observation_unit_id` 至少一个必填 | +| `observation_unit_id` | 亲本观测单元 | 从 observation_unit 选择 | 观测单元选择器 | 与 `germplasm_id` 至少一个必填 | + +### 录入建议 + +- 在 Cross 详情页内嵌“亲本列表”。 +- 常见快捷录入:Parent1 / Parent2。 +- 对于田间选株杂交,优先记录 observation_unit_id,同时可带出 germplasm_id,保证可追溯到具体植株。 + +--- + +## 5.8 pedigree_node 系谱节点 + +### 业务说明 + +`pedigree_node` 是系谱图中的节点,通常对应一个 germplasm。它用于描述材料在系谱树中的位置,不等同于一次杂交记录。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| --------------------- | --------------------- | ------------------------ | -------------- | ----------------------------------------- | +| `id` | 系谱节点主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `crossing_year` | 亲本最初杂交年份 | 用户录入 | 年份选择器 | 可选;四位年份 | +| `family_code` | 家系编号 | 用户录入 | 文本框 | 可选;同一 crossing_project 下建议唯一 | +| `pedigree_string` | 系谱字符串,如 A/B//C | 用户录入/系统生成 | 文本框 | 可选;建议支持 Purdy notation | +| `crossing_project_id` | 产生该节点的杂交项目 | 从 crossing_project 选择 | 杂交项目选择器 | 可选 | +| `germplasm_id` | 该系谱节点对应的材料 | 从 germplasm 选择 | 材料选择器 | 建议必填;同一 germplasm 不建议重复建节点 | + +### 录入建议 + +- Germplasm 详情页提供“系谱”Tab。 +- 支持两种维护方式:树图拖拽维护、表格维护节点和边。 +- 如果 cross 完成后产生后代 germplasm,应自动或半自动创建 pedigree_node。 + +--- + +## 5.9 pedigree_edge 系谱边 + +### 业务说明 + +`pedigree_edge` 是系谱图中的边,描述节点之间的父子、同胞等关系。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------- | ------------------------------------------ | --------------------- | ---------- | ------------------------------------ | +| `id` | 系谱边主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `edge_type` | 边类型,如 parent、child、sibling | 用户选择 | 下拉框 | 必填 | +| `parent_type` | 如果是亲本关系,标识 MALE、FEMALE、SELF 等 | 用户选择 | 下拉框 | 可选;当 edge_type=parent 时建议必填 | +| `connceted_node_id` | 被连接节点 | 从 pedigree_node 选择 | 节点选择器 | 必选 | +| `this_node_id` | 当前节点 | 从 pedigree_node 选择 | 节点选择器 | 必选;不能等于 connected node | + +### 录入建议 + +- 前端展示时不要暴露“this_node_id / connected_node_id”这种技术词,应该显示为“当前材料”和“关联材料”。 +- 添加父本/母本时,系统自动创建 edge_type=parent。 +- 需要避免明显循环,例如 A 是 B 的父本,同时 B 又是 A 的父本。 + +--- + +## 5.10 seed_lot 种子批次 + +### 业务说明 + +`seed_lot` 是实物库存批次,描述某一批种子当前有多少、放在哪里、属于哪个项目。它不是 germplasm 身份;同一个 germplasm 可以有多个 seed_lot。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------- | ---------------------------------------------------- | --------------------- | ------------------- | ----------------------------- | +| `id` | SeedLot 主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `amount` | 当前库存数量,可以是粒数、重量、株数等 | 用户录入/交易自动更新 | 数字输入框 | 必填;非负;交易后自动更新 | +| `created_date` | 批次创建时间 | 系统默认,可导入 | 日期时间选择器/只读 | 默认当前时间 | +| `description` | 批次说明 | 用户录入 | 多行文本 | 可选 | +| `last_updated` | 最后更新时间,包含交易变化 | 系统自动更新 | 只读 | 不允许手动改 | +| `name` | 批次名称,如 华占-2026-荆门-扩繁批 | 用户录入或自动生成 | 文本框 | 必填;同一 program 下建议唯一 | +| `source_collection` | 原始来源 collection,如野外采集、nursery、种质库集合 | 用户录入 | 文本框 | 可选 | +| `storage_location` | 具体库位描述,如 冰箱A-2层-盒03 | 用户录入 | 文本框 | 可选 | +| `units` | 数量单位,如 seeds、g、kg、plants | 用户选择 | 下拉框/文本框 | 必填;交易单位需一致或可换算 | +| `location_id` | 库存所在地点 | 从 location 选择 | 地点选择器 | 可选 | +| `program_id` | 所属项目 | 从 program 选择 | 项目选择器 | 可选;用于项目库存筛选 | + +### 录入建议 + +- 创建 seed_lot 后必须进入“批次组成”Tab,至少录入一条 `seed_lot_content_mixture`。 +- 普通用户不要直接编辑 amount;amount 应通过入库、出库、转移、分装等交易动作更新。 +- 支持库存状态:充足、低库存、耗尽,可由 amount 和阈值计算。 + +--- + +## 5.11 seed_lot_content_mixture 批次组成 + +### 业务说明 + +`seed_lot_content_mixture` 描述一个 seed_lot 由哪些材料或 cross 组成。单一材料批次也需要有一条组成记录,比例为 100%。混合批次则多条记录占比合计为 100%。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| -------------------- | ----------------------------- | -------------------- | ------------------- | ---------------------------------------- | +| `id` | 批次组成主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `mixture_percentage` | 该材料或 cross 在批次中的占比 | 用户录入 | 百分比输入框 | 0 到 100;同一 seed lot 总和建议等于 100 | +| `cross_id` | 来源 cross | 从 cross_entity 选择 | Cross 选择器 | 与 `germplasm_id` 至少一个必填 | +| `germplasm_id` | 来源 germplasm | 从 germplasm 选择 | 材料选择器 | 与 `cross_id` 至少一个必填 | +| `seed_lot_id` | 所属 seed lot | 由详情页带入或选择 | SeedLot 选择器/隐藏 | 必选 | + +### 录入建议 + +- 新建 seed_lot 时,如果用户选择了单个 germplasm,系统自动生成一条 mixture:`germplasm_id=所选材料`,`mixture_percentage=100`。 +- 如果来源是某次杂交产生的种子,优先填写 `cross_id`。 +- 如果既能追溯 cross 又能追溯 germplasm,可按系统设计决定是否允许同时填写;若允许,同时展示“来源杂交”和“当前材料身份”。 + +--- + +## 5.12 seed_lot_transaction 批次流转 + +### 业务说明 + +`seed_lot_transaction` 记录库存变化。它不应该由用户像普通表单一样手动维护,而应该由“入库、出库、转移、分装、合并、消耗”等业务动作自动生成。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------ | -------------------------------------------- | ----------------------- | ------------------- | ------------------------------------------------- | +| `id` | 流转记录主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `amount` | 流转数量 | 用户在业务动作中录入 | 数字输入框 | 必填;大于 0 | +| `description` | 流转说明,如用于某 study、分装原因、报废原因 | 用户录入 | 多行文本 | 可选;出库/报废建议必填 | +| `timestamp` | 流转发生时间 | 默认当前时间,可调整 | 日期时间选择器 | 必填 | +| `units` | 流转单位 | 默认继承 seed_lot.units | 下拉框/只读 | 必填;需与 seed_lot 单位一致或有换算关系 | +| `from_seed_lot_id` | 来源批次 | 按动作自动设置 | SeedLot 选择器/隐藏 | 与 `to_seed_lot_id` 至少一个存在 | +| `to_seed_lot_id` | 目标批次 | 按动作自动设置 | SeedLot 选择器/隐藏 | 与 `from_seed_lot_id` 至少一个存在;不能等于 from | + +### 业务动作映射 + +| 动作 | from_seed_lot_id | to_seed_lot_id | amount 对库存影响 | +| --------- | ---------------- | -------------- | -------------------------------------------- | +| 入库 | 空 | 目标批次 | 目标批次增加 | +| 出库 | 来源批次 | 空 | 来源批次减少 | +| 转移 | 来源批次 | 目标批次 | 来源减少,目标增加 | +| 分装 | 原批次 | 新批次 | 原批次减少,新批次增加 | +| 合并 | 多个来源批次 | 目标批次 | 来源减少,目标增加;可能生成多条 transaction | +| 消耗/报废 | 来源批次 | 空 | 来源减少,并记录原因 | + +### 验收标准 + +1. amount 必须大于 0。 +2. 出库/消耗时,amount 不得超过来源批次当前库存。 +3. from 和 to 不能相同。 +4. transaction 创建后应自动更新 seed_lot.amount 和 last_updated。 +5. 已生成的 transaction 原则上不允许随意修改;如需纠错,应通过反向交易或更正记录处理。 + +--- + +# 6. 跨表联动与关键校验 + +## 6.1 选择器联动 + +| 场景 | 联动规则 | +| ------------------------- | ------------------------------------------------------------ | +| 创建 germplasm | 必须先选择 crop;breeding_method 可选 | +| 创建 attribute definition | crop 可选;若选择 trait/method/scale/ontology,则必须引用存在记录 | +| 创建 attribute value | 选择 germplasm 后,attribute 选择器优先展示同 crop 或未限定 crop 的属性 | +| 创建 crossing project | 必须选择 program | +| 创建 cross | 必须先选择 crossing_project;计划杂交和实际杂交使用不同入口 | +| 创建 cross parent | 从 cross 详情页进入时自动带出 cross 和 crossing_project | +| 创建 seed lot | 可选择 program、location;保存后必须维护 content mixture | +| 创建 transaction | 从 seed lot 详情页进入时自动带出 from/to seed lot | + +## 6.2 删除规则 + +| 对象 | 删除限制 | +| -------------------- | ------------------------------------------------------------ | +| breeding_method | 已被 germplasm 引用时不可删除 | +| germplasm | 已被 attribute value、cross parent、seed lot mixture、pedigree、observation unit 引用时不可删除 | +| attribute definition | 已有 attribute value 时不可删除 | +| crossing_project | 已有 cross / cross parent / pedigree node 时不可删除 | +| cross_entity | 已有 parent、seed lot mixture、observation unit 引用时不可删除 | +| seed_lot | 已有 mixture、transaction、observation unit 引用时不可删除 | +| transaction | 原则上不可物理删除;只能冲销或作废 | + +## 6.3 批量导入要求 + +### Germplasm 导入 + +必需列建议: + +```text +crop_id 或 crop_name +germplasm_name 或 default_display_name +``` + +强烈建议列: + +```text +accession_number +germplasmpui +breeding_method_name +country_of_origin_code +genus +species +seed_source +``` + +### SeedLot 导入 + +必需列建议: + +```text +seed_lot_name +amount +units +germplasm_id/germplasm_name 或 cross_id/cross_name +mixture_percentage +``` + +### Attribute Value 导入 + +必需列建议: + +```text +germplasm_id 或 germplasm_name +attribute_id 或 attribute_name/code +value +``` + +可选列: + +```text +determined_date +source +remark +``` + +导入必须先预校验,错误报告至少包含:行号、字段、错误原因、建议修复方式。 + +--- + +# 7. 后端接口建议 + +## 7.1 主数据接口 + +```text +GET /germplasm +POST /germplasm +GET /germplasm/{id} +PUT /germplasm/{id} +DELETE /germplasm/{id} + +GET /breeding-methods +POST /breeding-methods + +GET /germplasm-attributes +POST /germplasm-attributes + +GET /germplasm/{id}/attribute-values +POST /germplasm/{id}/attribute-values + +GET /crossing-projects +POST /crossing-projects +GET /crossing-projects/{id}/crosses + +GET /crosses +POST /crosses +GET /crosses/{id}/parents +POST /crosses/{id}/parents + +GET /seed-lots +POST /seed-lots +GET /seed-lots/{id}/mixtures +POST /seed-lots/{id}/mixtures +GET /seed-lots/{id}/transactions +POST /seed-lots/{id}/transactions +``` + +## 7.2 选择器接口 + +```text +GET /selectors/crops +GET /selectors/breeding-methods +GET /selectors/germplasm?cropId=&keyword= +GET /selectors/germplasm-attributes?cropId=&datatype=&keyword= +GET /selectors/crossing-projects?programId=&keyword= +GET /selectors/crosses?crossingProjectId=&planned=&keyword= +GET /selectors/seed-lots?programId=&locationId=&keyword= +GET /selectors/observation-units?studyId=&germplasmId=&keyword= +``` + +--- + +# 8. 测试验收清单 + +1. 创建 germplasm 时,未选择 crop 不允许保存。 +2. `germplasm_name` 和 `default_display_name` 都为空时不允许保存。 +3. `germplasmpui` 重复时提示唯一性冲突。 +4. attribute value 的 value 必须按 attribute datatype / scale 校验。 +5. 创建 cross 时必须选择 crossing project。 +6. 实际杂交选择 planned_cross_id 时不能选择自身。 +7. cross parent 必须选择 parent_type。 +8. cross parent 的 germplasm_id 和 observation_unit_id 至少填写一个。 +9. seed lot 的 amount 不允许小于 0。 +10. seed lot 的 units 必填。 +11. seed lot content mixture 的 percentage 必须在 0 到 100 之间。 +12. 同一 seed lot 的 mixture_percentage 总和不为 100 时,保存前应提示或禁止保存,具体取决于业务配置。 +13. 出库数量不能超过当前库存。 +14. transaction 创建后自动更新 seed_lot.amount。 +15. 已有关联下游数据的 germplasm、cross、seed_lot 不允许物理删除。 + diff --git a/docs/requirements/03-phenotyping-entry-requirements.md b/docs/requirements/03-phenotyping-entry-requirements.md new file mode 100644 index 0000000..a40c4c5 --- /dev/null +++ b/docs/requirements/03-phenotyping-entry-requirements.md @@ -0,0 +1,778 @@ +# Phenotyping 模块专业数据录入需求文档 V2 + +## 1. 文档目的 + +本文档用于指导 Phenotyping 表型模块的前端页面、后端接口、字段校验、数据导入和测试验收设计。本文档不只描述表关系,而是从真实育种表型采集业务出发,解释每个对象和字段的业务意义、录入方式、控件建议、校验规则和上下游影响。 + +## 2. 模块定位 + +Phenotyping 模块用于管理田间、温室、实验室或高通量设备采集到的表型数据。它的核心不是“录一张 observation 表”,而是完整描述: + +```text +在哪个 study 里 +对哪个 observation_unit +按照哪个 observation_variable +在什么时间、由谁、用什么方法 +采集到了什么 value +并能关联事件、图片、坐标和上下文 +``` + +## 3. 核心业务概念 + +| 概念 | 业务含义 | 关键表 | +| ---------------------------- | ---------------------------------------------------------- | ------------------------------------------------- | +| Ontology 本体 | 性状、方法、标尺、变量的术语来源,可以是本地本体或公开本体 | `ontology` | +| Trait 性状 | 测什么。例如株高、穗长、病害等级、籽粒颜色 | `trait` | +| Method 方法 | 怎么测。例如尺测、人工评分、无人机图像分析、实验室检测 | `method` | +| Scale 标尺 | 用什么单位或尺度表达。例如 cm、kg、0-5 等级、文本、布尔值 | `scale`、`scale_valid_value_category` | +| ObservationVariable 观测变量 | Trait + Method + Scale 的组合,表示一个可采集指标 | `observation_variable` | +| StudyVariable 研究采集指标 | 某个 study 计划采集哪些 observation variable | `study_variable` | +| ObservationUnit 观测单元 | 被观测对象。通常是 plot、plant、block、field、sample | `observation_unit` | +| Event 事件 | 管理动作或环境事件,如施肥、灌溉、打药、移栽、极端天气 | `event`、`event_param`、`event_observation_units` | +| Image 图像 | 田间图片、无人机图片、病害图片、长势图像等 | `image`、`image_observations` | +| Observation 观测值 | 某个观测单元在某个变量上的一次实际测量结果 | `observation` | + +## 4. 推荐业务流程 + +```text +1. 准备 Core 上下文:crop、program、trial、study、location、season +2. 维护或导入本体 ontology +3. 定义 trait / method / scale +4. 组合 observation_variable +5. 把 observation_variable 绑定到 study_variable,形成本 study 的采集指标清单 +6. 根据田间设计生成 observation_unit,例如 block / plot / plant +7. 给 observation_unit 绑定材料来源:germplasm / seed_lot / cross +8. 记录 event,例如播种、移栽、施肥、灌溉、打药、采样、极端天气 +9. 通过矩阵方式批量录入 observation +10. 上传 image,并关联 observation_unit 或 observation +11. 做数据质控:缺失值、异常值、重复采集、单位和分类值校验 +``` + +## 5. 推荐页面形态 + +### 5.1 Study 表型工作台 + +```text +Study 表型工作台 +├─ 试验上下文:study / trial / program / crop / location / season +├─ 指标清单:study_variable +├─ 田间设计:observation_unit 层级、block、plot、plant、row、column +├─ 材料布置:germplasm、seed_lot、cross +├─ 表型矩阵:observation_unit × observation_variable +├─ 田间事件:event + event_param + event_observation_units +├─ 图片证据:image + image_observations +├─ 数据质控:缺失值、异常值、重复值、单位校验、分类值校验 +└─ 导入导出:观测单元模板、表型采集模板、图片元数据模板 +``` + +### 5.2 表型矩阵录入 + +真实采集时不应只提供单条 observation 表单,而应支持矩阵录入: + +| observation_unit | germplasm | 株高 cm | 穗长 cm | 倒伏等级 | 备注 | +| ---------------- | --------- | ------: | ------: | -------: | ---------- | +| Plot-001 | 华占 | 112.5 | 24.1 | 1 | 正常 | +| Plot-002 | 抗倒伏A | 98.3 | 22.8 | 0 | 正常 | +| Plot-003 | F2-001 | 101.6 | 23.0 | 4 | 台风后倒伏 | + +保存时,系统按每个非空单元格生成一条 `observation`,每一列对应一个 `observation_variable`。 + +--- + +# 6. 字段级专业录入需求 + +## 6.1 ontology 本体 + +### 业务说明 + +`ontology` 表示术语体系来源,用来说明 trait、method、scale、observation_variable 的定义来自哪里。它可以是本地维护的术语库,也可以是公开 Crop Ontology、Trait Ontology 或机构内部标准。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------ | ----------------------------------------- | -------------------------------- | --------------- | -------------------------------- | +| `id` | 本体主键,系统内部唯一标识 | 新增时系统生成;导入时可允许指定 | 隐藏/只读 | 必填、唯一;编辑时不可修改 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许前端手填 | +| `authors` | 本体作者或维护者 | 用户录入 | 文本框 | 可选;多个作者可用分号或数组扩展 | +| `copyright` | 本体版权说明 | 用户录入 | 文本框/多行文本 | 可选 | +| `description` | 本体用途说明 | 用户录入 | 多行文本 | 可选,建议填写 | +| `documentationurl` | 本体文档链接 | 用户录入 | URL 输入框 | 可选;若填写必须是合法 URL | +| `licence` | 本体许可证,例如 CC-BY、MIT、机构内部许可 | 用户录入/选择 | 文本框/下拉框 | 可选 | +| `ontology_name` | 本体名称,例如 Rice Trait Ontology | 用户录入 | 文本框 | 必填;同一用户下建议唯一 | +| `version` | 本体版本,例如 2024.1、v2.0 | 用户录入 | 文本框 | 可选;建议与外部本体版本一致 | + +### 录入建议 + +- 若系统只是内部使用,可先创建一个“默认本体”或“本地表型本体”。 +- 若对接 Crop Ontology,建议保存 ontology 名称、版本、文档 URL。 +- 本体删除前必须检查是否被 trait、method、scale、observation_variable 引用。 + +--- + +## 6.2 trait 性状 + +### 业务说明 + +`trait` 描述“测什么”。例如株高、叶长、穗长、倒伏等级、病害严重度、籽粒颜色。专业上 trait 可以拆成 `entity` 和 `attribute`: + +```text +Trait = Entity + Attribute +例如:grain colour +entity = grain +attribute = colour +``` + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------- | ----------------------------------------------------- | -------------------- | ----------------- | ---------------------------------------- | +| `id` | 性状主键,系统内部唯一标识 | 系统生成;导入可指定 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `attribute` | 性状属性,即被观察的特征,如 height、colour、severity | 用户录入/本体选择 | 文本框/本体选择器 | 可选;若填写 PUI,建议同步填写 attribute | +| `attributepui` | 属性永久唯一标识,通常是 URI | 用户录入/本体带出 | 文本框/URL 输入框 | 可选;若填写建议唯一 | +| `entity` | 性状实体,即观察对象部位,如 plant、leaf、grain、root | 用户录入/本体选择 | 文本框/本体选择器 | 可选 | +| `entitypui` | 实体永久唯一标识,通常是 URI | 用户录入/本体带出 | 文本框/URL 输入框 | 可选;若填写建议唯一 | +| `main_abbreviation` | 性状主缩写,如 PH、GY、DTH | 用户录入 | 文本框 | 可选;同一 ontology 下建议唯一 | +| `status` | 性状状态,如 recommended、obsolete、legacy | 用户选择 | 下拉框 | 可选;推荐使用枚举 | +| `trait_class` | 性状分类,如 phenology、morphology、disease、quality | 用户选择/录入 | 下拉框/文本框 | 可选;建议字典化 | +| `trait_description` | 性状定义和说明 | 用户录入 | 多行文本 | 可选,但专业使用强烈建议填写 | +| `trait_name` | 性状名称,给用户看的名称 | 用户录入 | 文本框 | 必填;作为下拉展示名称 | +| `traitpui` | 性状永久唯一标识 | 用户录入/本体带出 | 文本框/URL 输入框 | 可选;若填写必须唯一 | +| `ontology_id` | 所属本体 | 从 ontology 选择 | 本体选择器 | 可选;必须引用存在的 ontology | + +### 录入建议 + +- 简单业务可只填:`trait_name`、`trait_description`、`trait_class`。 +- 专业数据交换场景建议填写:`entity`、`attribute`、`traitpui`、`ontology_id`。 +- 不要把单位写进 trait。比如“株高 cm”不应该作为 trait,正确拆分是 trait=株高,scale=cm。 + +--- + +## 6.3 method 方法 + +### 业务说明 + +`method` 描述“怎么测”。同一个 trait 用不同方法测,会形成不同 observation_variable。例如: + +```text +Trait: plant height +Method 1: tape measure +Method 2: drone image processing +Scale: cm +``` + +这两个变量不能混为一谈,因为它们的数据来源、误差和可比性不同。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| -------------- | ------------------------------------------------------------ | ------------------ | ----------------- | -------------------------------- | +| `id` | 方法主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `description` | 方法说明,如测量位置、工具、采样规则 | 用户录入 | 多行文本 | 可选;建议必填 | +| `formula` | 如果该方法通过计算得到结果,填写公式 | 用户录入 | 文本框/公式编辑器 | 可选;需要格式提示 | +| `method_class` | 方法分类,如 measurement、estimation、computed、image_analysis | 用户选择/录入 | 下拉框/文本框 | 可选;建议字典化 | +| `methodpui` | 方法永久唯一标识 | 用户录入/本体带出 | 文本框/URL 输入框 | 可选;若填写建议唯一 | +| `name` | 方法名称 | 用户录入 | 文本框 | 必填;同一 ontology 下不建议重复 | +| `reference` | 方法参考文献、SOP、标准链接 | 用户录入 | 文本框/URL 输入框 | 可选;若为 URL 需校验格式 | +| `ontology_id` | 所属本体 | 从 ontology 选择 | 本体选择器 | 可选;必须引用存在的 ontology | + +### 录入建议 + +- 方法页面应重点让用户说明“测量步骤”,否则后续数据不可复现。 +- 对于人工评分,必须在 description 中说明评分标准,并通过 scale 配置有效分类值。 +- 对于高通量图像方法,reference 建议填写算法版本、模型版本或 SOP 链接。 + +--- + +## 6.4 scale 标尺 + +### 业务说明 + +`scale` 描述“用什么尺度表达结果”。它决定 observation.value 的合法值、单位、数据类型、小数位和分类值。常见类型: + +```text +Numerical: 株高 cm、产量 kg/ha +Ordinal: 倒伏等级 0-5、病害等级 1-9 +Categorical: 籽粒颜色 red/yellow/white +Text: 描述性备注 +Boolean: 是否开花、是否倒伏 +Date: 开花日期 +``` + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ----------------- | ----------------------------------------------------- | ------------------ | ----------------- | --------------------------------- | +| `id` | 标尺主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `data_type` | 数据类型,决定 observation.value 的输入控件和校验方式 | 用户选择 | 下拉框 | 必填;使用系统枚举 | +| `decimal_places` | 小数位数 | 用户录入 | 数字输入框 | 数值型可填;必须为非负整数 | +| `scale_name` | 标尺名称,如 cm、0-5 lodging score、disease score 1-9 | 用户录入 | 文本框 | 必填;同一 ontology 下建议唯一 | +| `scalepui` | 标尺永久唯一标识 | 用户录入/本体带出 | 文本框/URL 输入框 | 可选;若填写建议唯一 | +| `units` | 单位,如 cm、mm、kg/ha、days | 用户选择/录入 | 单位选择器/文本框 | 数值型建议必填;分类型可为空 | +| `valid_value_max` | 最大有效值 | 用户录入 | 数字输入框/文本框 | 数值/等级型校验;必须大于等于 min | +| `valid_value_min` | 最小有效值 | 用户录入 | 数字输入框/文本框 | 数值/等级型校验;必须小于等于 max | +| `ontology_id` | 所属本体 | 从 ontology 选择 | 本体选择器 | 可选;必须引用存在 ontology | + +### 录入建议 + +- `data_type` 是动态表单核心字段。前端要根据它渲染 observation.value: + - Numerical:数字输入框,按 min/max/decimal_places 校验; + - Ordinal:数字或下拉,通常有 min/max; + - Categorical:下拉框,选项来自 `scale_valid_value_category`; + - Text:文本框; + - Boolean:开关/单选; + - Date:日期选择器。 +- 如果 data_type 是分类型,必须至少配置一个 `scale_valid_value_category`。 +- 如果 data_type 是数值型,建议填写单位和有效范围。 + +--- + +## 6.5 scale_valid_value_category 分类有效值 + +### 业务说明 + +`scale_valid_value_category` 是分类型或等级型 scale 的合法取值表。例如倒伏等级 0-5、病害等级 1-9、颜色 red/yellow/white。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ---------- | --------------------------------- | --------------------------- | ----------------- | ------------------------ | +| `id` | 分类值主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `label` | 用户界面展示文案,如 “0 - 不倒伏” | 用户录入 | 文本框 | 必填 | +| `value` | 实际保存值,如 0、1、red | 用户录入 | 文本框 | 必填;同一 scale 内唯一 | +| `scale_id` | 所属 scale | 从 scale 选择或由详情页带出 | Scale 选择器/隐藏 | 必选;必须引用存在 scale | + +### 录入建议 + +- 在 scale 详情页中以内嵌表格维护。 +- 保存 observation 时,如果 scale 是分类型,value 必须在该表中存在。 +- label 可以给用户看,value 用于保存和导出,二者可以不同。 + +--- + +## 6.6 observation_variable 观测变量 + +### 业务说明 + +`observation_variable` 是真正进入采集模板的指标。它由 trait、method、scale 组合而成。 + +```text +observation_variable = trait + method + scale +例如:株高 + 尺子测量 + cm = 株高-尺测法-cm +``` + +它不是观测结果,而是“可以被测的一列数据”。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ---------------------- | ------------------------------------------ | ------------------------ | ----------------- | ---------------------------------------- | +| `id` | 观测变量主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `default_value` | 默认观测值,例如默认等级、默认文本 | 用户录入 | 动态控件 | 可选;按 scale 校验 | +| `documentationurl` | 变量说明文档链接 | 用户录入 | URL 输入框 | 可选;若填写必须合法 URL | +| `growth_stage` | 建议采集生育期,如 flowering、maturity | 用户录入/选择 | 下拉框/文本框 | 可选;建议字典化 | +| `institution` | 定义该变量的机构 | 用户录入 | 文本框 | 可选 | +| `language` | 变量语言,如 zh、en | 用户选择 | 下拉框 | 可选;建议 ISO 639-1 | +| `scientist` | 提交或维护该变量的科学家/负责人 | 用户录入/人员选择 | 文本框/选择器 | 可选 | +| `status` | 变量状态,如 recommended、obsolete、legacy | 用户选择 | 下拉框 | 可选;obsolete 不建议用于新 study | +| `submission_timestamp` | 变量提交时间 | 系统默认当前时间,可调整 | 日期时间选择器 | 可选;新增默认当前时间 | +| `crop_id` | 适用作物 | 从 crop 选择 | 作物选择器 | 可选;如果填写,下游 study 应尽量同 crop | +| `method_id` | 测定方法 | 从 method 选择 | 方法选择器 | 必选 | +| `ontology_id` | 所属本体 | 从 ontology 选择 | 本体选择器 | 可选 | +| `scale_id` | 标尺 | 从 scale 选择 | 标尺选择器 | 必选 | +| `trait_id` | 被测性状 | 从 trait 选择 | 性状选择器 | 必选 | +| `name` | 变量名称,采集矩阵列名 | 自动生成后允许修改 | 文本框 | 必填;同一 crop/ontology 下建议唯一 | +| `pui` | 变量永久唯一标识 | 用户录入/本体带出 | 文本框/URL 输入框 | 可选;若填写建议唯一 | + +### 录入建议 + +- 新建变量采用“三段式选择”:先选 trait,再选 method,再选 scale。 +- 系统自动生成 name,例如:`trait_name + method_name + scale_name`,用户可修改。 +- 变量详情页应展示:被哪些 study 使用、已有多少 observation、是否仍推荐使用。 +- 如果变量已经产生 observation,不建议修改 trait/method/scale,只能停用旧变量并创建新变量。 + +--- + +## 6.7 study_variable 研究采集指标 + +### 业务说明 + +`study_variable` 是 study 和 observation_variable 的多对多关系,表示某个 study 计划采集哪些指标。它是“配置采集模板”的动作痕迹。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ---------------- | --------------------- | ---------------------------- | ----------------- | --------------------------- | +| `study_db_id` | 所属 study | 从 study 选择或由工作台带出 | Study 选择器/隐藏 | 必选;必须引用存在 study | +| `variable_db_id` | 该 study 要采集的变量 | 从 observation_variable 选择 | 变量多选器 | 必选;必须引用存在 variable | + +### 录入建议 + +- 在 Study 表型工作台的“指标清单”Tab 中维护。 +- 支持批量选择变量和从历史 study 复制变量清单。 +- 若某变量已有 observation,不允许从 study_variable 中直接移除,除非先处理已有观测数据。 + +--- + +## 6.8 observation_unit 观测单元 + +### 业务说明 + +`observation_unit` 是“被观测对象”。通常是 plot 或 plant,也可以是 field、block、sub-plot、sample 等。它连接了 Core 上下文和材料来源,是表型数据的入口,也可以成为后续 genotyping sample 的来源。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ----------------------- | ------------------------------------ | ------------------------- | ----------------- | -------------------------------------------------- | +| `id` | 观测单元主键 | 系统生成;导入可指定 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `observation_unit_name` | 观测单元名称,如 Plot-001、Plant-003 | 用户录入/批量生成 | 文本框 | 必填;同一 study 内建议唯一 | +| `observation_unitpui` | 观测单元永久唯一标识 | 用户录入/系统生成 | 文本框/URL 输入框 | 可选;若填写建议唯一 | +| `crop_id` | 作物 | 由 study 自动带出,可只读 | 作物选择器/只读 | 可选但建议有;需与 study.crop 一致 | +| `cross_id` | 材料来源 cross | 从 cross_entity 选择 | Cross 选择器 | 可选;与 germplasm/seed_lot 至少建议选一个材料来源 | +| `germplasm_id` | 材料来源 germplasm | 从 germplasm 选择 | 材料选择器 | 可选;常用字段 | +| `program_id` | 项目 | 由 study 自动带出 | 项目选择器/只读 | 可选但建议有;需与 study.program 一致 | +| `seed_lot_id` | 种子批次来源 | 从 seed_lot 选择 | SeedLot 选择器 | 可选;田间播种建议填写 | +| `study_id` | 所属 study | 从 study 选择或工作台带出 | Study 选择器/隐藏 | 必选 | +| `trial_id` | 所属 trial | 由 study 自动带出 | Trial 选择器/只读 | 可选但建议有;需与 study.trial 一致 | + +### 录入建议 + +- 推荐从 Study 表型工作台批量生成,不建议逐条手工建。 +- 必须支持田间布局字段:field、block、rep、plot、row、column、plant。若当前表没有这些字段,应通过 position/level 附属表或 additional_info 保存。 +- `germplasm_id` 代表材料身份;`seed_lot_id` 代表实际播种批次;二者建议都保留。 +- 选择 seed_lot 时可自动带出 germplasm,但不应强行覆盖用户选择。 + +### 观测层级建议 + +| 层级 | 含义 | 例子 | +| ------ | -------------- | ---------- | +| field | 田块或试验场 | Field-01 | +| block | 区组 | Block-01 | +| rep | 重复 | Rep-01 | +| plot | 小区 | Plot-001 | +| plant | 单株 | Plant-001 | +| sample | 样本级观测对象 | Sample-001 | + +--- + +## 6.9 event 事件 + +### 业务说明 + +`event` 记录发生在 study 或 observation_unit 上的离散事件。它既可以是处理的一部分,也可以是影响结果的外部背景。例如播种、移栽、施肥、灌溉、打药、收获、采样、暴雨、台风、病害暴发等。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------- | ---------------------------------------------------------- | ------------------------- | ----------------- | -------------------------- | +| `id` | 事件主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `event_description` | 事件说明,如“台风后倒伏严重” | 用户录入 | 多行文本 | 可选;异常事件建议必填 | +| `event_type` | 事件类型名称,如 fertilizer、irrigation、planting、harvest | 用户选择/录入 | 下拉框/文本框 | 必填;建议使用事件类型字典 | +| `event_type_db_id` | 事件类型 ID,可引用标准事件类型字典 | 用户选择/系统带出 | 选择器/隐藏 | 可选 | +| `study_id` | 事件所属 study | 从 study 选择或工作台带出 | Study 选择器/隐藏 | 必选 | + +### 录入建议 + +- 在 Study 表型工作台的“事件”Tab 中维护。 +- 新增事件时先选择作用范围:整个 study 或部分 observation_unit。 +- 如果只作用于部分 plot/plant,必须通过 `event_observation_units` 记录范围。 +- 事件发生时间字段在你当前表结构中未显式列出,但业务上非常重要;如果数据库已有 event_date/time 字段,应强制录入;如果没有,建议补字段或放入 additional_info。 + +--- + +## 6.10 event_param 事件参数 + +### 业务说明 + +`event_param` 是事件的参数明细。例如施肥事件可以有肥料类型、剂量、单位;灌溉事件可以有水量、持续时间;打药事件可以有药剂名称、浓度、剂量。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------- | ------------------------------------- | ----------------- | ----------------- | ------------------------- | +| `id` | 事件参数主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `code` | 参数代码,如 N_RATE、IRR_VOL | 用户录入/字典带出 | 文本框 | 可选;同一事件内建议唯一 | +| `description` | 参数解释 | 用户录入 | 多行文本 | 可选 | +| `key` | 参数键,如 fertilizerType、dose、unit | 用户录入/字典选择 | 文本框 | 必填;同一事件内不应重复 | +| `name` | 参数名称,如 氮肥施用量 | 用户录入 | 文本框 | 可选 | +| `rdf_value` | RDF 或本体语义值 | 用户录入/系统带出 | 文本框 | 可选;普通业务可隐藏 | +| `units` | 参数单位,如 kg/ha、mm、L | 用户选择/录入 | 单位选择器 | 可选;数值参数建议必填 | +| `value` | 参数值 | 用户录入 | 动态控件 | 可选;若 key 需要值则必填 | +| `value_description` | 参数值说明 | 用户录入 | 多行文本 | 可选 | +| `event_id` | 所属事件 | 由事件详情页带出 | Event 选择器/隐藏 | 必选 | + +### 录入建议 + +- 前端以键值表格方式维护。 +- 可以为常用事件类型配置参数模板,例如 fertilizer 事件默认出现 fertilizerType、amount、units。 +- 参数值是否必填取决于事件类型配置。 + +--- + +## 6.11 event_observation_units 事件作用范围 + +### 业务说明 + +`event_observation_units` 记录事件作用到哪些 observation_unit。比如一次施肥作用于整个 study,则可以不逐个绑定;如果某个处理只作用于部分 plot,则必须明确绑定。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ---------------------- | ------------ | --------------------------------------- | ----------------- | ------------------------ | +| `event_entity_id` | 事件 | 由事件详情页带出 | Event 选择器/隐藏 | 必选 | +| `observation_units_id` | 事件作用对象 | 从当前 study 的 observation_unit 中选择 | 多选器/表格 | 必选;必须属于同一 study | + +### 录入建议 + +- 支持按 block、rep、plot 范围批量选择。 +- 如果事件作用于整个 study,可以在 event 上记录 scope=study,不必生成大量关系记录。 +- 如果 event_observation_units 有记录,所有 observation_units 必须属于 event.study_id。 + +--- + +## 6.12 image 图像 + +### 业务说明 + +`image` 用于保存图片或图片元数据。图片可以是人工拍摄、无人机、固定相机、显微图、病害图片等。图片可以直接关联 observation_unit,也可以通过 `image_observations` 关联具体 observation。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| --------------------- | ------------------------------------- | ------------------------ | ------------------- | -------------------------------------- | +| `id` | 图片主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `copyright` | 图片版权或授权说明 | 用户录入 | 文本框 | 可选 | +| `description` | 图片说明,如“Plot-003 台风后倒伏照片” | 用户录入 | 多行文本 | 可选,建议填写 | +| `image_data` | 图片二进制内容 | 用户上传 | 文件上传 | 与 `imageurl` 至少一个;限制大小和格式 | +| `image_file_name` | 图片文件名 | 上传时自动填充 | 只读/文本框 | 可选;上传时自动生成 | +| `image_file_size` | 文件大小 | 上传时自动识别 | 只读 | 可选;应限制最大大小 | +| `image_height` | 图片高度 | 上传后自动识别 | 只读/数字框 | 可选;必须为非负整数 | +| `imagemimetype` | MIME 类型,如 image/jpeg、image/png | 上传后自动识别 | 只读/下拉框 | 可选;只允许图片 MIME 类型 | +| `imageurl` | 外部图片 URL 或对象存储 URL | 用户填写/上传后生成 | URL 输入框 | 与 `image_data` 至少一个;URL 格式校验 | +| `image_width` | 图片宽度 | 上传后自动识别 | 只读/数字框 | 可选;必须为非负整数 | +| `name` | 图片名称 | 用户录入/文件名带出 | 文本框 | 必填 | +| `time_stamp` | 拍摄或上传时间 | 用户录入/系统默认 | 日期时间选择器 | 可选;建议填写拍摄时间 | +| `coordinates_id` | 图片坐标 | 地图取点/坐标对象选择 | 地图控件/坐标选择器 | 可选;建议支持 Point/Polygon | +| `observation_unit_id` | 所属观测单元 | 从 observation_unit 选择 | 观测单元选择器 | 可选;若填写应属于同一 study 上下文 | + +### 录入建议 + +- 推荐图片文件存对象存储,数据库只存 URL 和元数据,避免 bytea 过大。 +- 手机拍照或无人机导入时,应自动提取文件名、大小、宽高、MIME、拍摄时间和 GPS 信息。 +- 对无人机俯拍图,可保存图像中心点,也可保存图像覆盖范围 Polygon。 + +--- + +## 6.13 image_observations 图片与观测值关系 + +### 业务说明 + +`image_observations` 表示某张图片支持或关联哪些 observation。比如一张倒伏照片可以关联一个 plot 的倒伏等级 observation。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ----------------- | ---------- | ------------------- | ------------------ | ---------------------------------------- | +| `image_entity_id` | 图片 | 由图片详情页带出 | Image 选择器/隐藏 | 必选 | +| `observations_id` | 关联观测值 | 从 observation 选择 | Observation 选择器 | 必选;建议与图片的 observation_unit 一致 | + +### 录入建议 + +- 图片详情页支持关联多个 observation。 +- observation 详情页也支持反向查看相关图片。 +- 如果图片已绑定 observation_unit,则关联 observation 时应过滤为同一 observation_unit 下的 observation。 + +--- + +## 6.14 observation 观测值 + +### 业务说明 + +`observation` 是 Phenotyping 模块最核心的事实表。它表示“某个 observation_unit 在某个 observation_variable 上的一次实际采集结果”。 + +```text +observation_unit = Plot-003 +observation_variable = 株高-尺测法-cm +value = 112.5 +observation_time_stamp = 2026-07-12 09:30 +``` + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------------- | ------------------ | ---------------------------------------- | ------------------- | ------------------------------------------ | +| `id` | 观测值主键 | 系统生成;导入可指定 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `collector` | 采集人 | 用户选择/登录用户带出 | 人员选择器/文本框 | 可选;人工采集建议填写 | +| `observation_time_stamp` | 观测时间 | 用户录入/采集设备带出 | 日期时间选择器 | 建议必填;按 ISO 8601 保存 | +| `uploaded_by` | 上传人 | 登录用户自动写入 | 只读 | 可选;批量导入时自动填 | +| `value` | 实际观测值 | 用户录入/导入/设备上传 | 动态控件 | 必填;按 variable.scale 校验 | +| `crop_id` | 作物上下文 | 由 observation_unit 或 study 自动带出 | 只读/隐藏 | 可选但建议保存;需与 observation_unit 一致 | +| `geo_coordinates_id` | 观测发生坐标 | 地图取点/设备 GPS | 地图控件/坐标选择器 | 可选;坐标需合法 | +| `observation_unit_id` | 被观测对象 | 从 observation_unit 选择或矩阵行带出 | 观测单元选择器/隐藏 | 必选 | +| `observation_variable_id` | 观测变量 | 从 observation_variable 选择或矩阵列带出 | 变量选择器/隐藏 | 必选 | +| `program_id` | 项目上下文 | 由 observation_unit/study 自动带出 | 只读/隐藏 | 可选但建议保存 | +| `season_id` | 季节 | 从 study season 或用户选择 | Season 选择器 | 可选;多季节 study 建议填写 | +| `study_id` | 所属 study | 由 observation_unit 自动带出 | 只读/隐藏 | 必选;需与 observation_unit.study 一致 | +| `trial_id` | 所属 trial | 由 observation_unit/study 自动带出 | 只读/隐藏 | 可选但建议保存 | + +### 录入建议 + +- 普通录入优先走矩阵:行是 observation_unit,列是 observation_variable。 +- 单条表单只用于补录、纠错、详情编辑。 +- value 必须使用 observation_variable.scale 决定控件和校验: + - 数值型:数字、小数位、范围; + - 分类型:只能从 valid category 中选; + - 日期型:日期格式; + - 布尔型:true/false; + - 文本型:长度限制。 +- 同一 `observation_unit_id + observation_variable_id + observation_time_stamp` 可允许重复,但必须在页面展示重复标记、采集人和上传来源。 + +--- + +# 7. 跨表联动规则 + +| 场景 | 联动规则 | +| ------------------------- | ------------------------------------------------------------ | +| 创建 observation_variable | 必须先选择 trait、method、scale;系统自动生成变量名称 | +| 配置 study_variable | 变量选择器优先展示同 crop 或未限定 crop 的变量 | +| 创建 observation_unit | 选择 study 后自动带出 crop、program、trial | +| 选择 seed_lot | 可自动带出 germplasm,但允许用户确认 | +| 录入 observation | 选择 observation_unit 后自动带出 study、trial、program、crop | +| 录入 observation.value | 根据 observation_variable.scale 动态渲染控件和校验 | +| 新增 event | 从 study 工作台进入时自动带出 study_id | +| 选择事件作用范围 | 只能选择同一 study 下的 observation_unit | +| 上传 image | 如果从 observation_unit 详情页上传,自动绑定 observation_unit_id | +| 关联 image_observations | 如果图片已绑定 observation_unit,只能关联同 observation_unit 的 observation | + +--- + +# 8. 删除和停用规则 + +| 对象 | 删除限制 | +| -------------------- | ------------------------------------------------------------ | +| ontology | 已被 trait/method/scale/variable 引用时不可物理删除 | +| trait | 已被 observation_variable 或 germplasm_attribute_definition 引用时不可物理删除 | +| method | 已被 observation_variable 引用时不可物理删除 | +| scale | 已被 observation_variable 或 attribute definition 引用时不可物理删除 | +| observation_variable | 已产生 observation 时不可删除,只能停用或设为 obsolete | +| study_variable | 若已有该变量的 observation,不允许直接移除 | +| observation_unit | 已有 observation、image、event、sample 时不可删除 | +| event | 已有关联 event_param 或 event_observation_units 时删除需级联确认或禁止 | +| image | 已关联 observation 时不可直接删除,应先解除关系或作废 | +| observation | 原始数据原则上不建议物理删除;建议保留修改历史或作废状态 | + +--- + +# 9. 批量导入要求 + +## 9.1 ObservationUnit 导入模板 + +必需列建议: + +```text +study_id 或 study_name +observation_unit_name +observation_level_name +observation_level_code +``` + +强烈建议列: + +```text +germplasm_id 或 germplasm_name +seed_lot_id 或 seed_lot_name +block +rep +plot +row +column +``` + +## 9.2 ObservationVariable 导入模板 + +必需列建议: + +```text +trait_name +method_name +scale_name +variable_name +``` + +可选列: + +```text +trait_pui +method_pui +scale_pui +variable_pui +crop_name +growth_stage +status +``` + +## 9.3 Observation 矩阵导入模板 + +推荐格式: + +```text +observation_unit_name,germplasm_name,collection_time,collector,PlantHeight_cm,LodgingScore_0_5,DiseaseScore_1_9 +Plot-001,华占,2026-07-12T09:30:00+08:00,张三,112.5,1,2 +Plot-002,抗倒伏A,2026-07-12T09:35:00+08:00,张三,98.3,0,1 +``` + +导入规则: + +1. 每个变量列必须能映射到一个 observation_variable。 +2. 每一行必须能匹配一个 observation_unit。 +3. 每个非空单元格生成一条 observation。 +4. 空值不生成 observation,除非用户选择“导入为空观测”。 +5. 导入前必须预校验,错误报告包含行号、列名、错误原因、建议修复方式。 + +## 9.4 Image 导入模板 + +必需列建议: + +```text +image_name +image_url 或 image_file +``` + +可选列: + +```text +observation_unit_name +observation_id +capture_time +longitude +latitude +polygon +copyright +description +``` + +--- + +# 10. 后端接口建议 + +## 10.1 主数据接口 + +```text +GET /ontologies +POST /ontologies +GET /ontologies/{id} +PUT /ontologies/{id} +DELETE /ontologies/{id} + +GET /traits +POST /traits +GET /traits/{id} +PUT /traits/{id} + +GET /methods +POST /methods +GET /methods/{id} +PUT /methods/{id} + +GET /scales +POST /scales +GET /scales/{id} +PUT /scales/{id} +GET /scales/{id}/valid-values +POST /scales/{id}/valid-values + +GET /observation-variables +POST /observation-variables +GET /observation-variables/{id} +PUT /observation-variables/{id} + +GET /studies/{studyId}/variables +POST /studies/{studyId}/variables +DELETE /studies/{studyId}/variables/{variableId} + +GET /studies/{studyId}/observation-units +POST /studies/{studyId}/observation-units/batch-generate +POST /observation-units +PUT /observation-units/{id} + +GET /studies/{studyId}/events +POST /studies/{studyId}/events +GET /events/{id}/params +POST /events/{id}/params +POST /events/{id}/observation-units + +GET /images +POST /images +POST /images/upload +POST /images/{id}/observations + +GET /observations +POST /observations +POST /observations/matrix +POST /observations/import +``` + +## 10.2 选择器接口 + +```text +GET /selectors/ontologies +GET /selectors/traits?ontologyId=&keyword= +GET /selectors/methods?ontologyId=&keyword= +GET /selectors/scales?ontologyId=&dataType=&keyword= +GET /selectors/observation-variables?cropId=&studyId=&keyword= +GET /selectors/studies?programId=&trialId=&keyword= +GET /selectors/observation-units?studyId=&level=&germplasmId=&keyword= +GET /selectors/events?studyId=&eventType=&keyword= +GET /selectors/images?observationUnitId=&keyword= +``` + +--- + +# 11. 测试验收清单 + +1. 创建 trait 时,`trait_name` 必填。 +2. 创建 method 时,`name` 必填。 +3. 创建 scale 时,`scale_name` 和 `data_type` 必填。 +4. 分类型 scale 没有 valid value 时,应提示或禁止用于 observation_variable。 +5. 创建 observation_variable 时,trait、method、scale 必选。 +6. observation_variable 名称可自动生成,也允许用户手动修改。 +7. 配置 study_variable 时,不能重复绑定同一个 variable。 +8. 创建 observation_unit 时,study 必选。 +9. 选择 study 后,crop、program、trial 自动带出。 +10. 同一 study 内 observation_unit_name 重复时提示。 +11. observation_unit 至少建议绑定 germplasm、seed_lot、cross 中一个材料来源。 +12. 创建 event 时,study 和 event_type 必填。 +13. event 绑定 observation_unit 时,所有 observation_unit 必须属于该 event 的 study。 +14. 图片上传时,image_data 和 imageurl 至少一个存在。 +15. 图片 MIME 类型必须是允许的图片类型。 +16. observation 创建时,observation_unit、observation_variable、value 必填。 +17. observation.value 必须按 scale.data_type 校验。 +18. 数值型 value 必须在 valid_value_min 和 valid_value_max 范围内。 +19. 分类型 value 必须属于 scale_valid_value_category。 +20. observation 的 study/trial/program/crop 必须与 observation_unit 上下文一致。 +21. 矩阵录入保存时,每个有效单元格生成一条 observation。 +22. 导入失败时必须返回行号、字段、错误原因和修复建议。 +23. 已产生 observation 的 observation_variable 不允许直接删除。 +24. 已被 observation、image、event、sample 引用的 observation_unit 不允许物理删除。 +25. 所有日期时间字段按 ISO 8601 保存,并保留时区或统一转换为 UTC。 + +--- + +# 12. 开发实现重点 + +1. `observation_variable` 是表型指标定义,不是观测值。 +2. `observation_unit` 是被观测对象,不是实际测量值。 +3. `observation` 才是真正的表型事实数据。 +4. `value` 字段虽然数据库是字符串,但前端和后端必须按 scale 做类型校验。 +5. `study` 是 Phenotyping 的核心上下文入口,几乎所有表型数据都应能追溯到 study。 +6. 表型采集页面应优先做矩阵录入,不要只做单条 CRUD。 +7. 图片不要直接塞数据库大字段为主,建议走对象存储 URL + 元数据。 +8. Event 不只是备注,它是解释表型差异的重要上下文,应支持作用范围和参数化。 + diff --git a/docs/requirements/04-genotyping-entry-requirements.md b/docs/requirements/04-genotyping-entry-requirements.md new file mode 100644 index 0000000..739c699 --- /dev/null +++ b/docs/requirements/04-genotyping-entry-requirements.md @@ -0,0 +1,890 @@ +# Genotyping 模块专业数据录入需求文档 V2 + +## 1. 文档目的 + +本文档用于指导 Genotyping 基因型模块的前端页面、后端接口、字段校验、文件导入、矩阵展示和测试验收设计。本文档不只描述数据库表关系,而是从真实基因型检测业务出发,解释每个对象和字段的业务意义、录入方式、控件建议、校验规则和上下游影响。 + +## 2. 模块定位 + +Genotyping 模块描述从田间或实验对象取样,到实验室检测,再到导入基因型结果矩阵的完整流程。 + +它的核心业务不是“录 allele_call 表”,而是: + +```text +从哪个 study / observation_unit 取样 +样本放在哪块 plate 的哪个 well +检测结果基于哪个 reference_set +结果文件包含哪些 variantset / variant +每个 sample 在每个 variant 上的 genotype 是什么 +这些位点是否还映射到 genome_map / linkage_group +``` + +## 3. 核心业务主线 + +Genotyping 有三条主线: + +```text +样本线:study / observation_unit -> plate -> sample -> callset +位点线:reference_set -> reference -> reference_bases -> variantset -> variant +结果线:callset + variant -> allele_call +图谱线:crop -> genome_map -> linkage_group -> marker_position -> variant +``` + +### 3.1 样本线 + +样本线回答: + +```text +从哪个材料、哪个 plot、哪株植物或哪个样本来源取样? +样本叫什么? +谁采的?什么时候采的? +放在哪块样本板哪个孔? +``` + +对应核心表: + +```text +plate +sample +callset +``` + +### 3.2 位点线 + +位点线回答: + +```text +检测结果基于哪个参考基因组? +参考序列有哪些 chromosome / scaffold / contig? +检测了哪些 SNP / Indel / SV 位点? +这些位点属于哪个 variantset? +``` + +对应核心表: + +```text +reference_set +reference +reference_bases +variantset +variant +``` + +### 3.3 结果线 + +结果线回答: + +```text +某个 sample 在某个 variant 上的 genotype 是什么? +测序深度是多少? +是否 phased? +是否有 likelihood? +``` + +对应核心表: + +```text +callset +allele_call +``` + +### 3.4 图谱线 + +图谱线回答: + +```text +某个位点在遗传图谱或物理图谱上的位置是什么? +它属于哪个 linkage group / chromosome / scaffold? +单位是 cM 还是 Mb? +``` + +对应核心表: + +```text +genome_map +linkage_group +marker_position +``` + +--- + +# 4. 推荐业务流程 + +```text +1. 准备 Core / Phenotyping 上游数据:crop、program、trial、study、observation_unit +2. 从 observation_unit 或 study 批量生成 sample +3. 创建 plate,并把 sample 分配到 well / row / column +4. 送检或接收实验室结果文件 +5. 创建 reference_set,维护 reference / reference_bases +6. 创建 variantset,导入 variant +7. 为 sample 生成 callset +8. 绑定 callset 与 variantset +9. 导入 allele_call,形成 sample × variant 基因型矩阵 +10. 如有遗传图谱,创建 genome_map / linkage_group / marker_position +11. 做结果质控:缺失率、重复位点、重复样本、read depth、genotype 格式、reference 一致性 +``` + +--- + +# 5. 推荐页面形态 + +## 5.1 Genotyping 工作台 + +建议以 study 或 genotyping project 为入口组织页面: + +```text +Genotyping 工作台 +├─ 样本管理:sample 列表、从 observation_unit 生成 sample +├─ 样本板:plate 列表、96/384 孔位布局 +├─ 参考基因组:reference_set、reference、reference_bases +├─ 变异集合:variantset、variant、analysis、format +├─ 检测结果:callset、allele_call、genotype matrix +├─ 遗传图谱:genome_map、linkage_group、marker_position +├─ 导入导出:sample 模板、VCF/Hapmap/CSV、allele matrix +└─ 质控:缺失率、重复率、深度、位点过滤、样本过滤 +``` + +## 5.2 Plate 孔位布局页 + +样本板不建议只做普通 CRUD,应提供 96 孔 / 384 孔布局视图: + +```text +Plate-96-001 + 01 02 03 04 +A SAMPLE-001 SAMPLE-002 SAMPLE-003 SAMPLE-004 +B SAMPLE-013 SAMPLE-014 SAMPLE-015 SAMPLE-016 +C SAMPLE-025 SAMPLE-026 EMPTY EMPTY +... +``` + +用户在孔位上放置 sample,系统保存: + +```text +sample.plate_id +sample.well +sample.plate_row +sample.plate_column +``` + +## 5.3 Genotype Matrix 结果页 + +`allele_call` 是基因型矩阵中的一个格子: + +| sample / callset | SNP001 | SNP002 | SNP003 | +| ---------------- | ------ | ------ | ------ | +| SAMPLE-001 | A/G | C/C | T/G | +| SAMPLE-002 | A/A | C/T | T/T | +| SAMPLE-003 | ./ . | C/C | T/G | + +导入时: + +```text +样本列 / 样本行 -> sample / callset +位点列 / 位点行 -> variant +单元格 genotype -> allele_call.genotype +``` + +--- + +# 6. 字段级专业录入需求 + +## 6.1 plate 样本板 + +### 业务说明 + +`plate` 用于管理承载样本的样本板或样本容器组。真实业务里,它通常是一块 96 孔板、384 孔板,也可能是一组试管或实验室提交批次。plate 的核心价值是支持实验室送样、扫码、孔位追踪和结果回填。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| -------------------------- | ----------------------------------------------- | ------------------------------ | ----------------- | -------------------------------------------- | +| `id` | 样本板主键,系统内部唯一标识 | 系统生成;导入可指定 | 隐藏/只读 | 必填、唯一;编辑不可修改 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许前端手填 | +| `client_plate_barcode` | 客户侧或外部实验室给出的板条码 | 用户录入/扫码/导入 | 文本框/扫码框 | 可选;建议唯一 | +| `client_plate_db_id` | 客户侧系统中的 plate ID | 用户录入/导入 | 文本框 | 可选;用于外部系统对接 | +| `plate_barcode` | 本系统样本板条码 | 系统生成/扫码录入 | 文本框/扫码框 | 可选;建议唯一 | +| `plate_format` | 样本板规格,如 96、384 | 用户选择 | 下拉框 | 可选;若填写,应限制为系统支持规格 | +| `plate_name` | 样本板名称,如 Plate-2026-001 | 用户录入/系统生成 | 文本框 | 与 barcode 至少一个必填 | +| `sample_submission_format` | 样本提交格式,如 DNA、tissue、seed、tube、plate | 用户选择 | 下拉框 | 可选;建议枚举化 | +| `sample_type` | 该板默认样本类型 | 用户选择 | 下拉框 | 可选;可作为 sample 默认值 | +| `status_time_stamp` | 样本板状态更新时间 | 系统自动写入 | 只读日期时间 | 可选;状态变化时自动更新 | +| `program_id` | 所属项目 | 从 program 选择或由 study 带出 | 项目选择器/只读 | 可选;若 study 已选,应与 study.program 一致 | +| `study_id` | 所属 study | 从 study 选择 | Study 选择器 | 可选;若选择则带出 trial/program | +| `submission_id` | 外部 vendor submission 或送检单 ID | 用户录入/选择 | 选择器/文本框 | 可选;用于实验室送检对接 | +| `trial_id` | 所属 trial | 从 trial 选择或由 study 带出 | Trial 选择器/只读 | 可选;若 study 已选,应与 study.trial 一致 | + +### 录入建议 + +- 创建 plate 时,用户先选择 `plate_format`,系统生成孔位矩阵。 +- 如果从 study 工作台创建,`program_id`、`trial_id`、`study_id` 自动带出。 +- 如果通过实验室返回文件创建,应优先用 `plate_barcode` 或 `client_plate_barcode` 匹配。 + +### 验收标准 + +1. `plate_name` 和 `plate_barcode` 至少填写一个。 +2. 同一系统中 `plate_barcode` 不应重复。 +3. 选择 study 后,trial/program 自动带出且保持一致。 +4. plate_format 为 96 时,只允许 A01-H12;384 时只允许 A01-P24。 + +--- + +## 6.2 sample 样本 + +### 业务说明 + +`sample` 是 genotyping 流程的样本入口,表示从田间、温室、实验室或库存材料中取出的一个物理生物样本,例如叶片、种子、DNA、组织、提取液等。sample 应尽可能关联 observation_unit,这样才能把表型和基因型打通。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| --------------------- | ------------------------------------ | -------------------------------------- | ---------------------- | --------------------------------------------------------- | +| `id` | 样本主键,系统内部唯一标识 | 系统生成;导入可指定 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `concentration` | 样本浓度,如 DNA 浓度 50 ng/µL | 用户录入/仪器导入 | 数字+单位输入 | 可选;建议拆分数值和单位,当前字段可暂存文本 | +| `plate_column` | 样本在 plate 中的列号 | 孔位选择自动生成 | 数字输入/孔位控件 | 绑定 plate 时必填或由 well 解析;范围受 plate_format 限制 | +| `plate_row` | 样本在 plate 中的行号 | 孔位选择自动生成 | 下拉/孔位控件 | 绑定 plate 时必填或由 well 解析;范围受 plate_format 限制 | +| `sample_barcode` | 样本条码 | 扫码/系统生成/导入 | 文本框/扫码框 | 可选;建议唯一 | +| `sample_description` | 样本说明 | 用户录入 | 多行文本 | 可选 | +| `sample_group_db_id` | 样本组 ID,用于批次、送检组或处理组 | 用户选择/导入 | 选择器/文本框 | 可选 | +| `sample_name` | 样本名称,如 SAMPLE-001 | 用户录入/系统生成 | 文本框 | 必填;同一 study 下建议唯一 | +| `samplepui` | 样本永久唯一标识 | 用户录入/系统生成 | 文本框/URL 输入框 | 可选;若填写建议唯一 | +| `sample_timestamp` | 采样时间 | 用户录入/设备带出 | 日期时间选择器 | 可选;建议必填 | +| `sample_type` | 样本类型,如 tissue、DNA、seed、leaf | 用户选择 | 下拉框 | 可选;建议枚举化 | +| `taken_by` | 采样人 | 用户选择/登录用户带出 | 人员选择器/文本框 | 可选 | +| `tissue_type` | 组织类型,如 leaf、root、grain、stem | 用户选择/录入 | 下拉框/文本框 | 可选;样本为组织时建议填写 | +| `volume` | 样本体积,如 50 µL | 用户录入/仪器导入 | 数字+单位输入 | 可选;当前字段可暂存文本 | +| `well` | 孔位,如 A01、H12 | 孔位布局选择 | 孔位选择器 | 绑定 plate 时建议必填;同一 plate 内唯一 | +| `observation_unit_id` | 来源观测单元 | 从 observation_unit 选择或批量生成带出 | ObservationUnit 选择器 | 可选但强烈建议填写;必须存在 | +| `plate_id` | 所属样本板 | 从 plate 选择 | Plate 选择器 | 可选;若填写需校验孔位唯一 | +| `program_id` | 项目上下文 | 由 observation_unit/study 带出 | 只读/隐藏 | 可选但建议保存;需与 study 一致 | +| `study_id` | 所属 study | 由 observation_unit 带出 | Study 选择器/只读 | 可选但建议保存;若有 observation_unit 必须一致 | +| `taxon_id_id` | 分类单元 ID | 从 taxon 选择 | Taxon 选择器 | 可选 | +| `trial_id` | 所属 trial | 由 observation_unit/study 带出 | Trial 选择器/只读 | 可选但建议保存 | + +### 录入建议 + +- 推荐从 observation_unit 批量生成 sample:一行 observation_unit 生成一个或多个 sample。 +- 如果样本放入 plate,应通过孔位布局页设置 well,不建议手填 A01。 +- `well`、`plate_row`、`plate_column` 应保持一致:well=A01,则 row=A,column=1。 +- sample 是物理样本,不是 genotype 结果;结果必须进入 callset / allele_call。 + +### 验收标准 + +1. `sample_name` 必填。 +2. 同一 plate 中 `well` 不允许重复。 +3. 绑定 observation_unit 时,study/trial/program 自动带出。 +4. 绑定 plate 时,well 必须符合 plate_format。 +5. 样本已有 callset 时,不允许物理删除。 + +--- + +## 6.3 reference_set 参考集 + +### 业务说明 + +`reference_set` 表示一个参考基因组集合,也就是一套 reference genome assembly。它定义 variant 坐标体系。不同参考基因组版本之间的位点坐标不可随意混用。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| -------------------------- | ---------------------------------------------- | ------------------ | ----------------- | ---------------------------- | +| `id` | 参考集主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `assemblypui` | 参考组装永久标识,如 DOI、INSDC accession、URI | 用户录入/导入 | 文本框/URL 输入框 | 可选;建议唯一 | +| `description` | 参考集说明 | 用户录入 | 多行文本 | 可选 | +| `is_derived` | 是否由其他参考集派生 | 用户选择 | 开关 | 可选 | +| `md5checksum` | 参考集校验值 | 用户录入/文件计算 | 文本框 | 可选;若填写应符合 MD5 格式 | +| `reference_set_name` | 参考集名称,如 IRGSP-1.0、B73 RefGen_v5 | 用户录入 | 文本框 | 必填;建议唯一 | +| `sourceuri` | 参考集来源 URI 或下载地址 | 用户录入 | URL 输入框 | 可选;校验 URL | +| `species_ontology_term` | 物种本体术语名称 | 用户录入/选择 | 文本框/本体选择器 | 可选 | +| `species_ontology_termuri` | 物种本体 URI | 用户录入/本体带出 | URL 输入框 | 可选;校验 URL | +| `source_germplasm_id` | 参考基因组来源材料 | 从 germplasm 选择 | Germplasm 选择器 | 可选;必须引用存在 germplasm | + +### 录入建议 + +- reference_set 是 variantset 和 variant 的上游,不能随意改。 +- 如果导入 VCF,必须先明确该 VCF 的 reference_set。 +- 同一 crop 可能存在多个 reference_set,应在页面上清楚显示版本和来源。 + +--- + +## 6.4 reference 参考序列 + +### 业务说明 + +`reference` 是 reference_set 中的一条参考序列,例如 chromosome、contig、scaffold。variant 的坐标通常是基于某条 reference 的位置。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------- | --------------------------------------------- | --------------------- | ------------------- | ------------------------------- | +| `id` | 参考序列主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `length` | 序列长度 | 用户录入/FASTA 导入 | 数字输入框 | 必填或建议必填;非负整数 | +| `md5checksum` | 单条 reference 的 MD5 校验值 | 用户录入/文件计算 | 文本框 | 可选;若填写应符合 MD5 格式 | +| `reference_name` | 参考序列名称,如 chr1、1、Chr01、scaffold_001 | 用户录入/FASTA 导入 | 文本框 | 必填;同一 reference_set 内唯一 | +| `source_divergence` | 与来源序列的差异程度 | 用户录入/导入 | 数字输入框 | 可选;范围建议 0-1 或按业务定义 | +| `reference_set_id` | 所属 reference_set | 从 reference_set 选择 | ReferenceSet 选择器 | 必选 | + +### 录入建议 + +- 建议通过 FASTA 索引文件批量导入 chromosome / contig。 +- 同一 reference_set 下 reference_name 必须唯一。 +- variant 导入时,文件中的 CHROM 必须能匹配 reference.reference_name。 + +--- + +## 6.5 reference_bases 参考片段 + +### 业务说明 + +`reference_bases` 保存 reference 的序列片段或分页。通常不建议人工录入,而是由 FASTA 导入或接口生成。对于大型基因组,不建议把完整序列全部塞入普通业务表,除非系统确实需要序列查询。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| -------------- | ------------------ | ------------------- | --------------------- | --------------------------------------- | +| `id` | 参考片段主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `bases` | 碱基序列片段 | FASTA 导入/接口生成 | 多行文本/只读 | 必填;只能包含合法碱基字符 A/C/G/T/N 等 | +| `page_number` | 分页编号 | 系统生成 | 数字输入/只读 | 必填;非负整数;同一 reference 下唯一 | +| `reference_id` | 所属 reference | 由导入带出 | Reference 选择器/隐藏 | 必选 | + +### 录入建议 + +- 不建议前端做手工新增入口,只提供查看和导入。 +- 每页 bases 长度应固定或可配置,例如 2KB、10KB、100KB。 +- 导入时校验页码连续性和字符合法性。 + +--- + +## 6.6 variantset 变异集合 + +### 业务说明 + +`variantset` 是一批 variant 和相关 call 的集合,通常对应一个 VCF 文件、一个 SNP 芯片结果、一个测序分析批次或一个 study 的基因型数据集。它必须明确 reference_set,否则 variant 坐标无意义。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------ | ---------------------------------------------- | --------------------- | ------------------- | ----------------------------------------- | +| `id` | 变异集合主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `variant_set_name` | 变异集合名称,如 Rice-2026-SNPSet、VCF-Run-001 | 用户录入/文件名带出 | 文本框 | 必填;建议唯一 | +| `reference_set_id` | 使用的参考集 | 从 reference_set 选择 | ReferenceSet 选择器 | 必选 | +| `study_id` | 关联 study | 从 study 选择 | Study 选择器 | 可选;若填写,应与样本来源 study 保持一致 | + +### 录入建议 + +- 导入 VCF/CSV 前必须先选择 reference_set。 +- 如果 variantset 来源于某次 study 的样本检测,应填写 study_id。 +- variantset 详情页应展示:variant 数量、callset 数量、分析信息、文件格式、导入状态。 + +--- + +## 6.7 variant 变异位点 + +### 业务说明 + +`variant` 表示一个遗传序列上的位点或区间,可以是 SNP、INDEL、SV,也可以是传统 marker。它通常由 reference_set、reference_name、start/end、reference_bases、alternate_bases 共同定义。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------ | ------------------------------------ | ------------------------------------- | ------------------------ | ------------------------------------------------ | +| `id` | 变异主键 | 系统生成/导入 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `created` | 创建时间 | 系统默认/文件导入 | 日期时间只读 | 可选;默认当前时间 | +| `variant_end` | 变异结束位置 | 文件导入/用户录入 | 数字输入框 | 可选;非负整数;不小于 start | +| `filters_applied` | 是否执行过过滤 | 文件导入/系统写入 | 开关/只读 | 可选 | +| `filters_passed` | 是否通过过滤 | 文件导入/系统写入 | 开关/只读 | 可选 | +| `reference_bases` | 参考等位基因或参考碱基 | 文件导入/用户录入 | 文本框 | 可选;合法碱基字符 | +| `variant_start` | 变异起始位置 | 文件导入/用户录入 | 数字输入框 | 必填;非负整数 | +| `svlen` | 结构变异长度 | 文件导入/用户录入 | 数字输入框 | 可选;SV 类型时建议填写 | +| `updated` | 更新时间 | 系统自动写入 | 只读日期时间 | 自动更新 | +| `variant_name` | 变异名称,如 SNP001、chr1_123456_A_G | 用户录入/自动生成 | 文本框 | 必填或由位置自动生成;同一 variantset 下建议唯一 | +| `variant_type` | 变异类型,如 SNP、INDEL、SV、marker | 用户选择/文件导入 | 下拉框 | 可选;建议枚举化 | +| `reference_set_id` | 所属参考集 | 从 reference_set 选择/variantset 带出 | ReferenceSet 选择器/隐藏 | 必选;需与 variantset.reference_set 一致 | +| `variant_set_id` | 所属变异集合 | 从 variantset 选择 | VariantSet 选择器/隐藏 | 必选 | + +### 录入建议 + +- 大量 variant 应通过 VCF、HapMap、CSV 批量导入,不建议手工录入。 +- 如果当前表缺少 alternate_bases、reference_name 字段,应在导入逻辑或扩展表中补齐,否则变异定义不完整。 +- variant 的唯一性建议使用:`reference_set_id + reference_name + start + reference_bases + alternate_bases`。 + +--- + +## 6.8 callset 调用集合 + +### 业务说明 + +`callset` 表示某个 sample 参与一次测序、芯片或分析事件后形成的一组 genotype calls。多数情况下,一个 sample 对应一个 callset;但如果同一个 sample 多次送检或使用不同分析流程,则可以有多个 callset。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| --------------- | -------------------------------- | ----------------------- | ------------- | ---------------------------- | +| `id` | CallSet 主键 | 系统生成/导入 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `call_set_name` | 调用集合名称,如 SAMPLE-001_Run1 | 用户录入/系统生成 | 文本框 | 必填;同一 sample 下建议唯一 | +| `created` | 创建时间 | 系统默认/导入 | 日期时间 | 默认当前时间 | +| `updated` | 更新时间 | 系统自动写入 | 只读日期时间 | 自动更新 | +| `sample_id` | 所属样本 | 从 sample 选择/导入匹配 | Sample 选择器 | 必选 | + +### 录入建议 + +- 从 genotype 文件导入时,可以按样本列自动创建 callset。 +- callset 需要通过 `callset_variant_sets` 绑定它覆盖的 variantset。 +- sample 详情页应展示 callset 列表和每个 callset 的 variantset、call 数量、导入时间。 + +--- + +## 6.9 callset_variant_sets 调用集合与变异集合关系 + +### 业务说明 + +`callset_variant_sets` 表示某个 callset 覆盖哪些 variantset。它是一次检测结果和一批位点集合之间的关系。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ----------------- | -------- | ------------------------------- | ---------------------- | -------- | +| `call_sets_id` | 调用集合 | 从 callset 选择/导入自动创建 | CallSet 选择器/隐藏 | 必选 | +| `variant_sets_id` | 变异集合 | 从 variantset 选择/导入自动绑定 | VariantSet 选择器/隐藏 | 必选 | + +### 录入建议 + +- 从 variantset 导入 genotype matrix 时,系统应自动绑定所有新建 callset 到该 variantset。 +- 同一 callset + variantset 不允许重复。 + +--- + +## 6.10 allele_call 基因型结果 + +### 业务说明 + +`allele_call` 是最终基因型事实数据,表示某个 callset 在某个 variant 上的 genotype 结果。它是 genotype matrix 的一个格子。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| --------------------- | --------------------------------------------------------- | ------------------------ | --------------------- | ------------------------------ | +| `id` | 基因型结果主键 | 系统生成/导入 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `genotype` | genotype 结果,如 A/G、0/1、0 | 1、AA、./. | 文件导入/少量手工修正 | 文本框/基因型控件 | +| `genotype_likelihood` | genotype likelihood,表示不同 genotype 的可能性或置信信息 | 文件导入 | 数字输入框 | 可选;范围和含义按导入格式定义 | +| `phase_set` | phase set,用于 phased genotype 分组 | 文件导入 | 文本框 | 可选;phased 数据时使用 | +| `read_depth` | 测序深度 | 文件导入/用户录入 | 数字输入框 | 可选;非负整数 | +| `call_set_id` | 所属 callset | 从 callset 选择/导入匹配 | CallSet 选择器/隐藏 | 必选 | +| `variant_id` | 对应 variant | 从 variant 选择/导入匹配 | Variant 选择器/隐藏 | 必选 | + +### 录入建议 + +- allele_call 不建议大量手工新增,主入口应该是 VCF/HapMap/CSV 文件导入。 +- 同一 `call_set_id + variant_id` 不应重复。 +- `genotype` 需要兼容常见格式: + - Allele 格式:A/G、C/C、T/G; + - VCF 编码:0/0、0/1、1/1、0|1; + - 缺失:./.、NA、N、--,具体缺失字符串由 variantset_format 配置。 +- 导入后应支持按 sample/callset 或 variant 两种视角查询。 + +--- + +## 6.11 variantset_analysis 变异集合分析信息 + +### 业务说明 + +`variantset_analysis` 记录 variantset 的分析流程、软件、版本、参数、时间等信息。它是数据可追溯的重要来源。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ---------------- | ------------------------------------------------------------ | ------------------ | ---------------------- | -------------- | +| `id` | 分析记录主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `analysis_name` | 分析名称,如 GATK SNP Calling Run 2026-01 | 用户录入/导入带出 | 文本框 | 必填 | +| `created` | 创建时间 | 系统默认 | 日期时间 | 默认当前时间 | +| `description` | 分析说明,如软件版本、过滤参数 | 用户录入 | 多行文本 | 可选,建议填写 | +| `type` | 分析类型,如 SNP calling、imputation、filtering、chip genotyping | 用户选择/录入 | 下拉框/文本框 | 可选 | +| `updated` | 更新时间 | 系统自动写入 | 只读日期时间 | 自动更新 | +| `variant_set_id` | 所属 variantset | 从 variantset 选择 | VariantSet 选择器/隐藏 | 必选 | + +### 录入建议 + +- 导入结果文件时,应生成或要求填写 analysis 信息。 +- 推荐记录:软件名称、版本、参数、过滤阈值、执行人、执行日期。 + +--- + +## 6.12 variantset_format 变异集合文件格式 + +### 业务说明 + +`variantset_format` 描述 variantset 数据文件格式,例如 VCF、HapMap、CSV、TSV、DArTSeq 等。它影响 genotype 的解析方式、缺失值、phased/unphased 分隔符等。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| -------------------- | --------------------------------------------- | ------------------- | ---------------------- | ------------------------ | +| `id` | 格式记录主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `data_format` | 数据内部结构,如 VCF、HapMap、DArTSeq、matrix | 用户选择/文件识别 | 下拉框 | 可选;建议枚举化 | +| `expand_homozygotes` | 是否展开纯合 genotype,例如 A -> A/A | 用户选择 | 开关 | 可选 | +| `file_format` | 文件 MIME 或外层格式,如 csv、tsv、zip、excel | 用户选择/文件识别 | 下拉框 | 可选 | +| `fileurl` | 原始结果文件链接 | 上传后生成/用户填写 | URL 输入框 | 可选;URL 格式校验 | +| `sep_phased` | phased genotype 分隔符,如 `|` | 用户录入/默认 | 文本框 | 可选;通常为 `|` | +| `sep_unphased` | unphased genotype 分隔符,如 `/` | 用户录入/默认 | 文本框 | 可选;通常为 `/` | +| `unknown_string` | 缺失值字符串,如 `./.`、NA、-- | 用户录入/默认 | 文本框 | 可选;导入时用于缺失识别 | +| `variant_set_id` | 所属 variantset | 从 variantset 选择 | VariantSet 选择器/隐藏 | 必选 | + +### 录入建议 + +- 文件上传时应自动识别 file_format 和 data_format。 +- 缺失值字符串必须参与 allele_call 导入解析。 +- 如果格式是 VCF,应支持 phased/unphased 分隔符。 + +--- + +## 6.13 genome_map 遗传图谱 + +### 业务说明 + +`genome_map` 描述遗传图谱或物理图谱。遗传图谱通常单位为 cM,物理图谱通常单位为 Mb 或 bp。它用于把 variant / marker 映射到 linkage group 上。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------ | ------------------------------ | ------------------ | ----------------- | ------------------------------------------ | +| `id` | 图谱主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `comments` | 图谱备注 | 用户录入 | 多行文本 | 可选 | +| `documentationurl` | 图谱说明文档或论文链接 | 用户录入 | URL 输入框 | 可选;URL 格式校验 | +| `map_name` | 图谱名称 | 用户录入 | 文本框 | 必填;建议唯一 | +| `mappui` | 图谱永久唯一标识 | 用户录入 | 文本框/URL 输入框 | 可选;若填写建议唯一 | +| `published_date` | 发布时间 | 用户录入 | 日期选择器 | 可选 | +| `scientific_name` | 物种学名 | 用户录入 | 文本框 | 可选 | +| `type` | 图谱类型,如 Genetic、Physical | 用户选择 | 下拉框 | 可选;建议枚举 | +| `unit` | 图谱单位,如 cM、Mb、bp | 用户选择 | 单位选择器 | 可选;Genetic 常用 cM,Physical 常用 Mb/bp | +| `crop_id` | 所属作物 | 从 crop 选择 | Crop 选择器 | 必选 | + +### 录入建议 + +- 图谱命名应能体现是 consensus map、mapping population map、reference genome map 还是 pan-genome map。 +- 图谱类型和单位必须匹配:Genetic + cM,Physical + Mb/bp。 + +--- + +## 6.14 linkage_group 连锁群 + +### 业务说明 + +`linkage_group` 是 genome_map 中的分组。它可能对应 chromosome、scaffold、contig 或传统遗传连锁群。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| --------------------- | -------------------------------------- | ------------------ | --------------------- | ---------------------------- | +| `id` | 连锁群主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `linkage_group_name` | 连锁群名称,如 Chr1、LG1、scaffold_001 | 用户录入/导入 | 文本框 | 必填;同一 genome_map 下唯一 | +| `max_marker_position` | 该连锁群最大 marker 位置 | 用户录入/导入计算 | 数字输入框 | 可选;非负数 | +| `genome_map_id` | 所属 genome_map | 从 genome_map 选择 | GenomeMap 选择器/隐藏 | 必选 | + +### 录入建议 + +- 在 genome_map 详情页维护。 +- 支持从 marker_position 文件批量导入时自动创建 linkage_group。 +- 若填写 max_marker_position,marker_position.position 不得超过该值。 + +--- + +## 6.15 marker_position 图谱位置 + +### 业务说明 + +`marker_position` 把 variant 映射到某个 linkage_group 上的位置。它用于遗传图谱定位、QTL、marker 辅助选择和图谱展示。 + +### 字段说明 + +| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 | +| ------------------ | --------------------------------- | --------------------- | ------------------- | ---------------------------------------- | +| `id` | 图谱位置主键 | 系统生成 | 隐藏/只读 | 必填、唯一 | +| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 | +| `position` | variant 在 linkage_group 上的位置 | 用户录入/文件导入 | 数字输入框 | 必填;非负;不得超过 max_marker_position | +| `linkage_group_id` | 所属 linkage_group | 从 linkage_group 选择 | LinkageGroup 选择器 | 必选 | +| `variant_id` | 对应 variant | 从 variant 选择 | Variant 选择器 | 必选 | + +### 录入建议 + +- 不建议逐条手工录入,优先通过 map 文件批量导入。 +- 同一 genome_map 下,同一 variant 不应重复映射,除非业务允许多图谱位置。 + +--- + +# 7. 跨表联动规则 + +| 场景 | 联动规则 | +| ------------------------------- | ------------------------------------------------------------ | +| 从 observation_unit 生成 sample | 自动带出 study、trial、program;可带出 germplasm 信息用于展示 | +| 给 sample 分配 plate | 选择 well 后自动生成 plate_row、plate_column | +| 创建 plate | 选择 study 后自动带出 trial/program | +| 创建 variantset | 必须先选择 reference_set | +| 导入 variant | variant.reference_set 必须与 variantset.reference_set 一致 | +| 创建 callset | 必须选择 sample;可按 sample_name 自动生成 call_set_name | +| 导入 allele_call | 先匹配 callset,再匹配 variant;匹配不到时进入错误报告 | +| 绑定 callset_variant_sets | 导入 genotype matrix 时自动绑定 callset 与 variantset | +| 创建 genome_map | 必须选择 crop | +| 创建 marker_position | variant 与 linkage_group 最好来自同一 crop/reference 背景 | + +--- + +# 8. 删除和停用规则 + +| 对象 | 删除限制 | +| --------------- | ------------------------------------------------------- | +| plate | 已有 sample 时不可删除 | +| sample | 已有 callset 时不可删除 | +| reference_set | 已有 reference、variantset、variant 时不可删除 | +| reference | 已有 reference_bases 或 variant 引用时不可删除 | +| variantset | 已有 variant、callset 绑定或 allele_call 结果时不可删除 | +| variant | 已有 allele_call 或 marker_position 时不可删除 | +| callset | 已有 allele_call 时不可删除 | +| allele_call | 原始检测结果原则上不建议物理删除,应支持作废或版本化 | +| genome_map | 已有 linkage_group 时不可删除 | +| linkage_group | 已有 marker_position 时不可删除 | +| marker_position | 可删除,但需记录操作日志 | + +--- + +# 9. 批量导入要求 + +## 9.1 Sample 导入模板 + +必需列建议: + +```text +sample_name +study_id 或 study_name +``` + +强烈建议列: + +```text +observation_unit_id 或 observation_unit_name +sample_barcode +sample_type +tissue_type +sample_timestamp +taken_by +plate_name +well +``` + +导入规则: + +1. 如果提供 observation_unit,系统自动带出 study/trial/program。 +2. 如果提供 plate + well,需校验同板孔位唯一。 +3. 如果 plate 不存在,可根据配置自动创建或报错。 + +## 9.2 Plate 导入模板 + +```text +plate_name +plate_barcode +plate_format +study_name +sample_name +well +``` + +导入规则: + +1. plate_format 决定合法 well 范围。 +2. 同一 plate + well 不允许重复。 +3. sample_name 不存在时,可配置是否自动创建 sample。 + +## 9.3 Variant 导入模板 + +建议支持 VCF / HapMap / CSV。 + +CSV 必需列建议: + +```text +variant_name +reference_name +variant_start +reference_bases +alternate_bases +variant_type +variant_set_name +reference_set_name +``` + +导入规则: + +1. 必须先选择或创建 reference_set。 +2. CHROM / reference_name 必须能匹配 reference。 +3. variant_start 必须非负。 +4. alternate_bases 不能为空,除非只是导入传统 marker。 + +## 9.4 Genotype Matrix / Allele Call 导入模板 + +推荐矩阵格式: + +```text +variant_name,SAMPLE-001,SAMPLE-002,SAMPLE-003 +SNP001,A/G,A/A,./. +SNP002,C/C,C/T,C/C +SNP003,T/G,T/T,T/G +``` + +导入规则: + +1. 每个样本列必须能匹配 sample 或 callset。 +2. 每个 variant 行必须能匹配 variant。 +3. 每个非空 genotype 单元格生成一条 allele_call。 +4. 缺失值按 variantset_format.unknown_string 处理。 +5. 同一 callset + variant 已存在时,根据导入策略选择覆盖、跳过或报错。 + +## 9.5 Genome Map 导入模板 + +```text +map_name +linkage_group_name +variant_name +position +unit +``` + +导入规则: + +1. map_name 不存在时可配置自动创建 genome_map。 +2. linkage_group 不存在时可配置自动创建。 +3. variant 必须存在。 +4. position 必须非负且不超过 max_marker_position。 + +--- + +# 10. 后端接口建议 + +## 10.1 主数据接口 + +```text +GET /plates +POST /plates +GET /plates/{id} +PUT /plates/{id} +GET /plates/{id}/layout +POST /plates/{id}/assign-samples + +GET /samples +POST /samples +POST /samples/batch-from-observation-units +GET /samples/{id} +PUT /samples/{id} +GET /samples/{id}/callsets + +GET /reference-sets +POST /reference-sets +GET /reference-sets/{id} +GET /reference-sets/{id}/references +POST /reference-sets/{id}/references + +GET /references +POST /references +GET /references/{id}/bases +POST /references/{id}/bases/import + +GET /variantsets +POST /variantsets +GET /variantsets/{id} +GET /variantsets/{id}/variants +POST /variantsets/{id}/variants/import +GET /variantsets/{id}/formats +POST /variantsets/{id}/formats +GET /variantsets/{id}/analyses +POST /variantsets/{id}/analyses + +GET /variants +POST /variants +GET /variants/{id} + +GET /callsets +POST /callsets +GET /callsets/{id} +POST /callsets/{id}/variantsets + +GET /allele-calls +POST /allele-calls/import +GET /allele-matrix + +GET /genome-maps +POST /genome-maps +GET /genome-maps/{id}/linkage-groups +POST /genome-maps/{id}/linkage-groups +POST /marker-positions/import +``` + +## 10.2 选择器接口 + +```text +GET /selectors/plates?studyId=&keyword= +GET /selectors/samples?studyId=&plateId=&keyword= +GET /selectors/reference-sets?cropId=&keyword= +GET /selectors/references?referenceSetId=&keyword= +GET /selectors/variantsets?referenceSetId=&studyId=&keyword= +GET /selectors/variants?variantSetId=&referenceName=&start=&end=&keyword= +GET /selectors/callsets?sampleId=&variantSetId=&keyword= +GET /selectors/genome-maps?cropId=&keyword= +GET /selectors/linkage-groups?genomeMapId=&keyword= +``` + +--- + +# 11. 测试验收清单 + +1. 创建 plate 时,plate_name 和 plate_barcode 至少填写一个。 +2. plate_barcode 重复时提示唯一性冲突。 +3. plate_format=96 时,只允许 A01-H12;plate_format=384 时,只允许 A01-P24。 +4. sample_name 必填。 +5. sample 绑定 plate 时,同一 plate + well 不允许重复。 +6. sample 绑定 observation_unit 后,study/trial/program 自动带出。 +7. reference_set_name 必填。 +8. reference_name 在同一 reference_set 内不允许重复。 +9. reference.length 必须为非负整数。 +10. reference_bases.bases 只能包含合法碱基字符。 +11. variantset 必须选择 reference_set。 +12. variant.reference_set_id 必须与 variantset.reference_set_id 一致。 +13. variant_start 必须非负。 +14. variant_end 不得小于 variant_start。 +15. variant_name 为空时系统能按 reference + position + allele 自动生成。 +16. callset 必须绑定 sample。 +17. 同一 sample 下 call_set_name 不建议重复。 +18. callset + variantset 关系不允许重复。 +19. allele_call 必须绑定 callset 和 variant。 +20. 同一 callset + variant 不允许重复,除非系统支持版本化。 +21. read_depth 必须为非负整数。 +22. genotype 必须符合导入格式规则。 +23. 缺失 genotype 必须按 unknown_string 统一处理。 +24. 导入 genotype matrix 时,无法匹配的 sample、variant 必须返回错误报告。 +25. 导入失败报告必须包含行号、列名、错误原因、建议修复方式。 +26. genome_map 必须选择 crop。 +27. linkage_group_name 在同一 genome_map 内不允许重复。 +28. marker_position.position 必须非负,且不超过 max_marker_position。 +29. 已有关联下游数据的 sample、variant、callset、variantset 不允许物理删除。 +30. 所有导入操作必须记录导入批次、上传人、上传时间和原始文件链接。 + +--- + +# 12. 开发实现重点 + +1. `sample` 是物理样本,不是基因型结果。 +2. `callset` 是 sample 参与一次检测/分析形成的结果集合。 +3. `allele_call` 才是某个 sample/callset 在某个 variant 上的 genotype 结果。 +4. `variantset` 必须绑定 reference_set,否则位点坐标没有意义。 +5. 大量 variant 和 allele_call 不应手工录入,主入口必须是文件导入。 +6. plate 必须做孔位布局,不要只做普通表单。 +7. genotype matrix 页面必须支持按 sample 和按 variant 两种方向查看。 +8. VCF/HapMap/CSV 的解析规则要独立成导入服务,不要散落在页面逻辑里。 +9. reference_set、variantset、callset、analysis、format 都是结果可追溯的关键上下文。 +10. 已导入的基因型结果建议支持版本化或作废,不建议直接物理删除。 + diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 0000000..d9cdaf7 --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"] +} \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..304a426 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,23 @@ +# Frontend + +## Start + +```bash +cd frontend +npm install +npm run dev +``` + +Default URL: http://localhost:3010 + +## Routes + +- /imports +- /imports/new +- /imports/[taskId]/mapping +- /imports/[taskId]/validate +- /imports/[taskId]/execute +- /imports/[taskId]/errors +- /imports/[taskId]/detail +- /imports/rollback +- /imports/history diff --git a/frontend/assets/main_page.png b/frontend/assets/main_page.png new file mode 100644 index 0000000..f26334c Binary files /dev/null and b/frontend/assets/main_page.png differ diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..39a81a1 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts new file mode 100644 index 0000000..c4b7818 --- /dev/null +++ b/frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/dev/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/frontend/next.config.js b/frontend/next.config.js new file mode 100644 index 0000000..7433a08 --- /dev/null +++ b/frontend/next.config.js @@ -0,0 +1,22 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + async rewrites() { + const apiBaseUrl = process.env.API_BASE_URL || "http://localhost:8081"; + return [ + { + source: "/auth/:path*", + destination: `${apiBaseUrl}/auth/:path*`, + }, + { + source: "/brapi/v2/:path*", + destination: `${apiBaseUrl}/brapi/v2/:path*`, + }, + { + source: "/api/:path*", + destination: `${apiBaseUrl}/api/:path*`, + }, + ]; + }, +}; + +module.exports = nextConfig; diff --git a/frontend/openapi-ts.config.ts b/frontend/openapi-ts.config.ts new file mode 100644 index 0000000..a2d022d --- /dev/null +++ b/frontend/openapi-ts.config.ts @@ -0,0 +1,37 @@ +import { defineConfig } from "@hey-api/openapi-ts"; +import { config } from "dotenv"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; + +const envPath = resolve(process.cwd(), ".env"); +if (existsSync(envPath)) { + config({ path: envPath }); +} + +const envLocalPath = resolve(process.cwd(), ".env.local"); +if (existsSync(envLocalPath)) { + config({ path: envLocalPath }); +} + +const apiBaseUrl = + process.env.API_BASE_URL || + process.env.NEXT_PUBLIC_API_BASE_URL || + "http://localhost:8081"; + +const openApiUrl = + process.env.OPENAPI_URL || + process.env.REACT_APP_OPENAPI_URL || + `${apiBaseUrl.replace(/\/$/, "")}/brapi/v2/openapi.json`; + +export default defineConfig({ + client: "@hey-api/client-fetch", + input: openApiUrl, + output: "./src/lib/api", + schemas: { + name: "types.gen.ts" + }, + services: { + name: "sdk.gen.ts" + }, + clientName: "client.gen.ts" +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..d4d7c92 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,8394 @@ +{ + "name": "agri-data-standard-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agri-data-standard-ui", + "version": "0.1.0", + "dependencies": { + "@hey-api/openapi-ts": "^0.87.1", + "@hookform/resolvers": "^4.1.3", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-table": "^8.21.2", + "axios": "^1.8.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.2.1", + "lucide-react": "^0.475.0", + "next": "^16.2.6", + "next-themes": "^0.4.6", + "openapi-fetch": "^0.15.0", + "react": "19.1.0", + "react-day-picker": "^10.0.1", + "react-dom": "19.1.0", + "react-hook-form": "^7.76.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.3.1", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.25.76", + "zustand": "5.0.5" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.11", + "@types/node": "22.15.21", + "@types/react": "19.1.2", + "@types/react-dom": "19.1.2", + "dotenv": "^16.4.7", + "eslint": "9.27.0", + "eslint-config-next": "15.3.2", + "tailwindcss": "^4.1.11", + "typescript": "5.8.3" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@date-fns/tz": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@date-fns/tz/-/tz-1.5.0.tgz", + "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==", + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.1", + "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.20.1.tgz", + "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.3", + "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", + "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.27.0", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@hey-api/codegen-core": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@hey-api/codegen-core/-/codegen-core-0.3.3.tgz", + "integrity": "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg==", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@hey-api/json-schema-ref-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.2.1.tgz", + "integrity": "sha512-inPeksRLq+j3ArnuGOzQPQE//YrhezQG0+9Y9yizScBN2qatJ78fIByhEgKdNAbtguDCn4RPxmEhcrePwHxs4A==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/openapi-ts": { + "version": "0.87.5", + "resolved": "https://registry.npmmirror.com/@hey-api/openapi-ts/-/openapi-ts-0.87.5.tgz", + "integrity": "sha512-WtmBCfbRKzsz578haTe1bh7FJWbqGyPmZ4dD8bynvQjQ6qtXk7mmI/3Nmev70Hl41VHoEoQNEL+BulfwJiJO/g==", + "license": "MIT", + "dependencies": { + "@hey-api/codegen-core": "^0.3.3", + "@hey-api/json-schema-ref-parser": "1.2.1", + "ansi-colors": "4.1.3", + "c12": "3.3.2", + "color-support": "1.1.3", + "commander": "14.0.1", + "open": "10.2.0", + "semver": "7.7.2" + }, + "bin": { + "openapi-ts": "bin/run.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@hey-api/openapi-ts/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@hookform/resolvers": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/@hookform/resolvers/-/resolvers-4.1.3.tgz", + "integrity": "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmmirror.com/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@next/env": { + "version": "16.2.6", + "resolved": "https://registry.npmmirror.com/@next/env/-/env-16.2.6.tgz", + "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.3.2", + "resolved": "https://registry.npmmirror.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.2.tgz", + "integrity": "sha512-ijVRTXBgnHT33aWnDtmlG+LJD+5vhc9AKTJPquGG5NKXjpKNjc62woIhFtrAcWdBobt8kqjCoaJ0q6sDQoX7aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.6", + "resolved": "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", + "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.6", + "resolved": "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", + "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", + "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", + "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", + "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", + "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.6", + "resolved": "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", + "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.6", + "resolved": "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", + "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmmirror.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.16.1", + "resolved": "https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz", + "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@tailwindcss/postcss/-/postcss-4.3.0.tgz", + "integrity": "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "postcss": "^8.5.10", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/postcss/node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmmirror.com/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmmirror.com/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.15.21", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.2", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.1.2.tgz", + "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.2", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.1.2.tgz", + "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.4", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", + "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/type-utils": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.4", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.4", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.59.4.tgz", + "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.4", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", + "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.4", + "@typescript-eslint/types": "^8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.4", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", + "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", + "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", + "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.4", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.4.tgz", + "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.4", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", + "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.4", + "@typescript-eslint/tsconfig-utils": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.4", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.4.tgz", + "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.4", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", + "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmmirror.com/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmmirror.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmmirror.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.4", + "resolved": "https://registry.npmmirror.com/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.31", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c12": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/c12/-/c12-3.3.2.tgz", + "integrity": "sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^17.2.3", + "exsolve": "^1.0.8", + "giget": "^2.0.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.0.0", + "pkg-types": "^2.3.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmmirror.com/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "14.0.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-4.2.1.tgz", + "integrity": "sha512-37RhSdxaG1suen6VDCza6rNrQfooyQh57HFVPwQGEq2QWliVLzPQZ8Oa017weOu+HZCnzI7N3Pf/wyoBKfEqrA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmmirror.com/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.27.0", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.27.0.tgz", + "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.27.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "15.3.2", + "resolved": "https://registry.npmmirror.com/eslint-config-next/-/eslint-config-next-15.3.2.tgz", + "integrity": "sha512-FerU4DYccO4FgeYFFglz0SnaKRe1ejXQrDb8kWUkTAg036YWi+jUsgg4sIGNCDhAsDITsZaL4MzBWKB6f4G1Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "15.3.2", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmmirror.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmmirror.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmmirror.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmmirror.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmmirror.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmmirror.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lucide-react": { + "version": "0.475.0", + "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.475.0.tgz", + "integrity": "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmmirror.com/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "16.2.6", + "resolved": "https://registry.npmmirror.com/next/-/next-16.2.6.tgz", + "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", + "license": "MIT", + "dependencies": { + "@next/env": "16.2.6", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.6", + "@next/swc-darwin-x64": "16.2.6", + "@next/swc-linux-arm64-gnu": "16.2.6", + "@next/swc-linux-arm64-musl": "16.2.6", + "@next/swc-linux-x64-gnu": "16.2.6", + "@next/swc-linux-x64-musl": "16.2.6", + "@next/swc-win32-arm64-msvc": "16.2.6", + "@next/swc-win32-x64-msvc": "16.2.6", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmmirror.com/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/nypm": { + "version": "0.6.6", + "resolved": "https://registry.npmmirror.com/nypm/-/nypm-0.6.6.tgz", + "integrity": "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==", + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.1.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmmirror.com/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-fetch": { + "version": "0.15.2", + "resolved": "https://registry.npmmirror.com/openapi-fetch/-/openapi-fetch-0.15.2.tgz", + "integrity": "sha512-rdYTzUmSsJevmNqg7fwUVGuKc2Gfb9h6ph74EVPkPfIGJaZTfqdIbJahtbJ3qg1LKinln30hqZniLnKpH0RJBg==", + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.0.15" + } + }, + "node_modules/openapi-typescript-helpers": { + "version": "0.0.15", + "resolved": "https://registry.npmmirror.com/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz", + "integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==", + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmmirror.com/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-day-picker": { + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/react-day-picker/-/react-day-picker-10.0.1.tgz", + "integrity": "sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "date-fns": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "@types/react": ">=16.8.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.76.0", + "resolved": "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.76.0.tgz", + "integrity": "sha512-eKtLGgFeSgkHqQD8J59AMZ9a4uD1D83iSIzt4YlTGD7liDen5rrjcUO1rVIGd9yC1gofryjtHbv+4ny4hkLWlw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmmirror.com/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmmirror.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmmirror.com/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "license": "MIT" + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.5", + "resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.5.tgz", + "integrity": "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..7269597 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,61 @@ +{ + "name": "agri-data-standard-ui", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev -p 3010", + "build": "next build", + "start": "next start -p 3010", + "lint": "next lint", + "api:generate": "node scripts/generate-api.cjs" + }, + "dependencies": { + "@hey-api/openapi-ts": "^0.87.1", + "@hookform/resolvers": "^4.1.3", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-table": "^8.21.2", + "axios": "^1.8.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.2.1", + "lucide-react": "^0.475.0", + "next": "^16.2.6", + "next-themes": "^0.4.6", + "openapi-fetch": "^0.15.0", + "react": "19.1.0", + "react-day-picker": "^10.0.1", + "react-dom": "19.1.0", + "react-hook-form": "^7.76.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.3.1", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.25.76", + "zustand": "5.0.5" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.11", + "@types/node": "22.15.21", + "@types/react": "19.1.2", + "@types/react-dom": "19.1.2", + "dotenv": "^16.4.7", + "eslint": "9.27.0", + "eslint-config-next": "15.3.2", + "tailwindcss": "^4.1.11", + "typescript": "5.8.3" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..483f378 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; diff --git a/frontend/scripts/generate-api.cjs b/frontend/scripts/generate-api.cjs new file mode 100644 index 0000000..cc49519 --- /dev/null +++ b/frontend/scripts/generate-api.cjs @@ -0,0 +1,42 @@ +const { execSync } = require("node:child_process"); +const { existsSync } = require("node:fs"); +const path = require("node:path"); +const dotenv = require("dotenv"); + +dotenv.config({ path: ".env" }); +dotenv.config({ path: ".env.local", override: true }); + +const apiBaseUrl = + process.env.API_BASE_URL || + process.env.NEXT_PUBLIC_API_BASE_URL || + "http://localhost:8081"; + +const openApiUrl = + process.env.OPENAPI_URL || + process.env.REACT_APP_OPENAPI_URL || + `${apiBaseUrl.replace(/\/$/, "")}/brapi/v2/openapi.json`; + +process.env.API_BASE_URL = apiBaseUrl; +if (!process.env.OPENAPI_URL && !process.env.REACT_APP_OPENAPI_URL) { + process.env.OPENAPI_URL = openApiUrl; +} + +console.log("API_BASE_URL:", apiBaseUrl); +console.log("OPENAPI_URL:", openApiUrl); +console.log("开始生成 OpenAPI 客户端..."); + +execSync("npx @hey-api/openapi-ts", { + stdio: "inherit", + env: process.env +}); + +const apiDir = path.resolve(process.cwd(), "src/lib/api"); +const required = ["types.gen.ts", "sdk.gen.ts", "index.ts", "client.gen.ts"]; +const missing = required.filter((file) => !existsSync(path.join(apiDir, file))); + +if (missing.length > 0) { + console.error("生成失败,缺失文件:", missing.join(", ")); + process.exit(1); +} + +console.log("OpenAPI 客户端生成完成:src/lib/api"); diff --git a/frontend/src/app/(app)/asset-config/asset/catalog/page.tsx b/frontend/src/app/(app)/asset-config/asset/catalog/page.tsx new file mode 100644 index 0000000..4c41743 --- /dev/null +++ b/frontend/src/app/(app)/asset-config/asset/catalog/page.tsx @@ -0,0 +1,5 @@ +import { PageHeader } from "@/components/common/PageHeader"; + +export default function CatalogPage() { + return ; +} diff --git a/frontend/src/app/(app)/asset-config/asset/supplier/page.tsx b/frontend/src/app/(app)/asset-config/asset/supplier/page.tsx new file mode 100644 index 0000000..84922ca --- /dev/null +++ b/frontend/src/app/(app)/asset-config/asset/supplier/page.tsx @@ -0,0 +1,5 @@ +import { PageHeader } from "@/components/common/PageHeader"; + +export default function SupplierPage() { + return ; +} diff --git a/frontend/src/app/(app)/basic-dictionary/base/crop/components/CropForm.tsx b/frontend/src/app/(app)/basic-dictionary/base/crop/components/CropForm.tsx new file mode 100644 index 0000000..4620a3a --- /dev/null +++ b/frontend/src/app/(app)/basic-dictionary/base/crop/components/CropForm.tsx @@ -0,0 +1,31 @@ +import { FormEvent } from "react"; +import { Button } from "@/components/ui/button"; +import type { CropFormValues } from "./types"; + +interface CropFormProps { + values: CropFormValues; + submitting: boolean; + onChange: (key: keyof CropFormValues, value: string) => void; + onSubmit: (event: FormEvent) => void; +} + +export function CropForm({ values, submitting, onChange, onSubmit }: CropFormProps) { + return ( +
+
+ 01 +
+

新增作物

+

填写作物名称即可写入数据库,创建用户将由后端从当前登录态自动写入。

+
+
+ + +
+ ); +} diff --git a/frontend/src/app/(app)/basic-dictionary/base/crop/components/CropGuide.tsx b/frontend/src/app/(app)/basic-dictionary/base/crop/components/CropGuide.tsx new file mode 100644 index 0000000..b0b1a2a --- /dev/null +++ b/frontend/src/app/(app)/basic-dictionary/base/crop/components/CropGuide.tsx @@ -0,0 +1,17 @@ +interface CropGuideProps { + error: string | null; +} + +export function CropGuide({ error }: CropGuideProps) { + return ( +
+

录入规则

+
    +
  • 作物名称建议使用标准英文名或平台统一编码。
  • +
  • 同一作物后续会关联 Program、Trial、Study。
  • +
  • 保存后会立即刷新右侧列表,确认接口已经写入。
  • +
+ {error ?
{error}
: null} +
+ ); +} diff --git a/frontend/src/app/(app)/basic-dictionary/base/crop/components/CropHeader.tsx b/frontend/src/app/(app)/basic-dictionary/base/crop/components/CropHeader.tsx new file mode 100644 index 0000000..1014906 --- /dev/null +++ b/frontend/src/app/(app)/basic-dictionary/base/crop/components/CropHeader.tsx @@ -0,0 +1,38 @@ +import { Button } from "@/components/ui/button"; + +const quickCrops = ["rice", "maize", "wheat", "soybean", "cotton"]; + +interface CropHeaderProps { + total: number; + loading: boolean; + onSelectQuickCrop: (name: string) => void; +} + +export function CropHeader({ total, loading, onSelectQuickCrop }: CropHeaderProps) { + return ( +
+
+
+
+ 农业基础字典入口 / Crop +
+

作物字典录入

+

+ 维护 BrAPI 标准 crop 表。这里录入的作物会成为 Program、Trial、Study、Germplasm 等数据的基础入口。 +

+
+ {quickCrops.map((name) => ( + + ))} +
+
+
+ 当前作物 + {total} + {loading ? "正在同步数据库..." : "已连接 crop 表"} +
+
+ ); +} diff --git a/frontend/src/app/(app)/basic-dictionary/base/crop/components/CropTable.tsx b/frontend/src/app/(app)/basic-dictionary/base/crop/components/CropTable.tsx new file mode 100644 index 0000000..a50defd --- /dev/null +++ b/frontend/src/app/(app)/basic-dictionary/base/crop/components/CropTable.tsx @@ -0,0 +1,48 @@ +import { Button } from "@/components/ui/button"; +import type { CropItem } from "./types"; + +interface CropTableProps { + rows: CropItem[]; + loading: boolean; + onRefresh: () => void; +} + +export function CropTable({ rows, loading, onRefresh }: CropTableProps) { + return ( +
+
+
+ 02 +

Crop 数据列表

+
+ +
+ {loading ? ( +
正在从后端读取 crop 表...
+ ) : rows.length > 0 ? ( +
+ + + + + + + + + + {rows.map((row) => ( + + + + + + ))} + +
ID作物名称创建用户
{row.id}{row.crop_name || "-"}{row.user_name || "-"}
+
+ ) : ( +
数据库里还没有作物。先在上方新增一个,比如 maize。
+ )} +
+ ); +} diff --git a/frontend/src/app/(app)/basic-dictionary/base/crop/components/cropApi.ts b/frontend/src/app/(app)/basic-dictionary/base/crop/components/cropApi.ts new file mode 100644 index 0000000..5e96b73 --- /dev/null +++ b/frontend/src/app/(app)/basic-dictionary/base/crop/components/cropApi.ts @@ -0,0 +1,12 @@ +import { createCrop, listCrops } from "@/services/dictionaryService"; +import type { CropFormValues, CropItem } from "./types"; + +export async function fetchCropList(): Promise { + return listCrops(); +} + +export async function saveCrop(values: CropFormValues): Promise { + return createCrop({ + crop_name: values.cropName.trim(), + }); +} diff --git a/frontend/src/app/(app)/basic-dictionary/base/crop/components/cropReducer.tsx b/frontend/src/app/(app)/basic-dictionary/base/crop/components/cropReducer.tsx new file mode 100644 index 0000000..c34a33b --- /dev/null +++ b/frontend/src/app/(app)/basic-dictionary/base/crop/components/cropReducer.tsx @@ -0,0 +1,28 @@ +import type { CropPageState } from "./types"; + +export type CropPageAction = + | { type: "SET_ROWS"; payload: CropPageState["rows"] } + | { type: "SET_FORM_FIELD"; payload: { key: keyof CropPageState["form"]; value: string } } + | { type: "RESET_FORM" } + | { type: "SET_LOADING"; payload: boolean } + | { type: "SET_SUBMITTING"; payload: boolean } + | { type: "SET_ERROR"; payload: string | null }; + +export function cropReducer(state: CropPageState, action: CropPageAction): CropPageState { + switch (action.type) { + case "SET_ROWS": + return { ...state, rows: action.payload }; + case "SET_FORM_FIELD": + return { ...state, form: { ...state.form, [action.payload.key]: action.payload.value } }; + case "RESET_FORM": + return { ...state, form: { cropName: "" } }; + case "SET_LOADING": + return { ...state, loading: action.payload }; + case "SET_SUBMITTING": + return { ...state, submitting: action.payload }; + case "SET_ERROR": + return { ...state, error: action.payload }; + default: + return state; + } +} diff --git a/frontend/src/app/(app)/basic-dictionary/base/crop/components/types.ts b/frontend/src/app/(app)/basic-dictionary/base/crop/components/types.ts new file mode 100644 index 0000000..45af0ad --- /dev/null +++ b/frontend/src/app/(app)/basic-dictionary/base/crop/components/types.ts @@ -0,0 +1,25 @@ +import type { CropRecord } from "@/services/dictionaryService"; + +export type CropItem = CropRecord; + +export interface CropFormValues { + cropName: string; +} + +export interface CropPageState { + rows: CropItem[]; + form: CropFormValues; + loading: boolean; + submitting: boolean; + error: string | null; +} + +export const initialCropState: CropPageState = { + rows: [], + form: { + cropName: "", + }, + loading: true, + submitting: false, + error: null, +}; diff --git a/frontend/src/app/(app)/basic-dictionary/base/crop/page.tsx b/frontend/src/app/(app)/basic-dictionary/base/crop/page.tsx new file mode 100644 index 0000000..42cd027 --- /dev/null +++ b/frontend/src/app/(app)/basic-dictionary/base/crop/page.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { Leaf } from "lucide-react"; +import { BrapiEntityPage } from "@/components/brapi/BrapiEntityPage"; +import { createCrop, deleteCrop, listCrops, updateCrop } from "@/services/dictionaryService"; + +const loadCropRows = async () => listCrops() as unknown as Record[]; + +const createCropRow = async (payload: Record) => createCrop({ + crop_name: String(payload.crop_name ?? ""), +}) as unknown as Record; + +const updateCropRow = async (id: string, payload: Record) => updateCrop(id, { + crop_name: String(payload.crop_name ?? ""), +}) as unknown as Record; + +const deleteCropRow = async (id: string) => { + await deleteCrop(id); +}; + +export default function CropDictionaryPage() { + return ( + + ); +} diff --git a/frontend/src/app/(app)/basic-dictionary/base/list/[listDbId]/page.tsx b/frontend/src/app/(app)/basic-dictionary/base/list/[listDbId]/page.tsx new file mode 100644 index 0000000..ed5420c --- /dev/null +++ b/frontend/src/app/(app)/basic-dictionary/base/list/[listDbId]/page.tsx @@ -0,0 +1,161 @@ +"use client"; + +import Link from "next/link"; +import { useCallback, useEffect, useState } from "react"; +import { useParams } from "next/navigation"; +import { ArrowLeft, List, Pencil } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { fetchListDetail, fetchPersonOptions } from "../api"; +import { ListItemPanel } from "../components/ListItemPanel"; +import { ListMetaDialog } from "../components/ListMetaDialog"; +import { listTypeLabel, type ListRecord, type SelectOption } from "../types"; + +function formatTimestamp(value: string | null | undefined) { + if (!value) return "—"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleString(); +} + +export default function ListDetailPage() { + const params = useParams<{ listDbId: string }>(); + const listDbId = decodeURIComponent(params.listDbId ?? ""); + const [record, setRecord] = useState(null); + const [personOptions, setPersonOptions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [editOpen, setEditOpen] = useState(false); + + const loadDetail = useCallback(async () => { + if (!listDbId) return; + setLoading(true); + setError(null); + try { + const [people, detail] = await Promise.all([fetchPersonOptions(), fetchListDetail(listDbId)]); + setPersonOptions(people); + setRecord(detail); + } catch (event) { + setError(event instanceof Error ? event.message : "加载失败"); + setRecord(null); + } finally { + setLoading(false); + } + }, [listDbId]); + + useEffect(() => { + let mounted = true; + loadDetail().catch(() => undefined); + return () => { + mounted = false; + }; + }, [loadDetail]); + + const handleItemsChange = useCallback((items: string[]) => { + setRecord((current) => (current ? { ...current, data: items, listSize: items.length } : current)); + }, []); + + if (!listDbId) { + return
缺少列表 ID
; + } + + return ( +
+
+
+ +
+
+ +
+
+

+ {record?.listName || "List 详情"} +

+

+ 维护列表基本信息与 list_item 明细 +

+
+
+
+ {record ? ( + + ) : null} +
+ + {error ? ( +
+ {error} +
+ ) : null} + +
+ {loading ? ( +
+ {Array.from({ length: 6 }).map((_, index) => ( + + ))} +
+ ) : record ? ( +
+
+
列表 ID
+
{record.id}
+
+
+
列表类型
+
{listTypeLabel(record.listType)}
+
+
+
Owner
+
{record.listOwnerName || record.listOwnerPersonDbId || "—"}
+
+
+
来源
+
{record.listSource || "—"}
+
+
+
创建时间
+
{formatTimestamp(record.dateCreated)}
+
+
+
修改时间
+
{formatTimestamp(record.dateModified)}
+
+
+
描述
+
{record.listDescription || "—"}
+
+
+ ) : ( +

未找到列表数据

+ )} +
+ + {record ? ( + + ) : null} + + {record ? ( + + ) : null} +
+ ); +} diff --git a/frontend/src/app/(app)/basic-dictionary/base/list/api.ts b/frontend/src/app/(app)/basic-dictionary/base/list/api.ts new file mode 100644 index 0000000..7aca43f --- /dev/null +++ b/frontend/src/app/(app)/basic-dictionary/base/list/api.ts @@ -0,0 +1,205 @@ +import { getAuthToken } from "@/utils/token"; +import type { ListDetails, ListNewRequest, ListSummary } from "@/lib/api/types.gen"; +import { NONE_SELECT_VALUE, type ListRecord, type ListType, type SelectOption } from "./types"; + +interface BrapiPagination { + currentPage: number; + pageSize: number; + totalCount: number; + totalPages: number; +} + +interface BrapiListResponse { + metadata: { + pagination: BrapiPagination; + status: Array>; + datafiles: Array>; + }; + result: { + data: T[]; + }; +} + +interface BrapiSingleResponse { + metadata: { + pagination: BrapiPagination; + status: Array>; + datafiles: Array>; + }; + result: T; +} + +interface PersonResponse { + personDbId: string; + firstName?: string | null; + lastName?: string | null; + emailAddress?: string | null; +} + +type ListPayload = Partial>; + +const apiBase = () => { + if (typeof window !== "undefined") return ""; + return process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"; +}; + +async function request(path: string, init?: RequestInit): Promise { + const token = getAuthToken(); + const response = await fetch(`${apiBase()}${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...(init?.headers || {}), + }, + }); + + if (!response.ok) { + const detail = await response.text(); + throw new Error(detail || `请求失败:${response.status}`); + } + return response.json() as Promise; +} + +const optionalText = (value: unknown) => { + const normalized = String(value ?? "").trim(); + if (!normalized || normalized === NONE_SELECT_VALUE) return undefined; + return normalized; +}; + +const requiredText = (value: unknown, message: string) => { + const normalized = optionalText(value); + if (!normalized) throw new Error(message); + return normalized; +}; + +const requiredListType = (value: unknown): ListType => { + const normalized = optionalText(value); + if (!normalized) throw new Error("请选择列表类型"); + return normalized as ListType; +}; + +export const mapListRecord = (list: ListSummary | ListDetails): ListRecord => ({ + ...list, + id: list.listDbId || "", + list_name: list.listName ?? null, + list_description: list.listDescription ?? null, + list_source: list.listSource ?? null, + list_owner_name: list.listOwnerName ?? null, + list_owner_person_id: list.listOwnerPersonDbId ?? null, + list_type: list.listType ?? null, + date_created: list.dateCreated ?? null, + date_modified: list.dateModified ?? null, + data: "data" in list && list.data ? list.data : [], +}); + +const toRequestBody = (payload: ListPayload, data?: string[]): ListNewRequest => ({ + listName: requiredText(payload.listName ?? payload.list_name, "请填写列表名称"), + listType: requiredListType(payload.listType ?? payload.list_type), + listDescription: optionalText(payload.listDescription ?? payload.list_description), + listSource: optionalText(payload.listSource ?? payload.list_source), + listOwnerName: optionalText(payload.listOwnerName ?? payload.list_owner_name), + listOwnerPersonDbId: optionalText(payload.listOwnerPersonDbId ?? payload.list_owner_person_id), + ...(data !== undefined ? { data } : {}), +}); + +export async function fetchListRows(): Promise { + const response = await request>("/brapi/v2/lists?page=0&pageSize=1000"); + return response.result.data.map(mapListRecord); +} + +export async function fetchListDetail(listDbId: string): Promise { + const response = await request>(`/brapi/v2/lists/${encodeURIComponent(listDbId)}`); + return mapListRecord(response.result); +} + +export async function fetchPersonOptions(): Promise { + const response = await request>("/brapi/v2/people?page=0&pageSize=1000"); + return response.result.data.map((person) => { + const name = [person.firstName, person.lastName].filter(Boolean).join(" ").trim(); + const label = name + ? `${name}${person.emailAddress ? ` / ${person.emailAddress}` : ""}` + : person.personDbId; + return { value: person.personDbId, label }; + }); +} + +export async function createListRow(payload: ListPayload): Promise { + const response = await request>("/brapi/v2/lists", { + method: "POST", + body: JSON.stringify([toRequestBody(payload)]), + }); + return mapListRecord(response.result.data[0]); +} + +export async function updateListRow(listDbId: string, payload: ListPayload, data?: string[]): Promise { + const response = await request>(`/brapi/v2/lists/${encodeURIComponent(listDbId)}`, { + method: "PUT", + body: JSON.stringify(toRequestBody(payload, data)), + }); + return mapListRecord(response.result); +} + +export async function appendListItems(listDbId: string, items: string[]): Promise { + const response = await request>(`/brapi/v2/lists/${encodeURIComponent(listDbId)}/items`, { + method: "POST", + body: JSON.stringify(items), + }); + return mapListRecord(response.result); +} + +export async function replaceListItems(listDbId: string, items: string[]): Promise { + const detail = await fetchListDetail(listDbId); + return updateListRow( + listDbId, + { + listName: detail.listName, + listType: detail.listType, + listDescription: detail.listDescription, + listSource: detail.listSource, + listOwnerName: detail.listOwnerName, + listOwnerPersonDbId: detail.listOwnerPersonDbId, + }, + items, + ); +} + +export function normalizeNewItems(existing: string[], incoming: string[]): string[] { + const seen = new Set(existing.map((item) => item.trim()).filter(Boolean)); + const duplicates: string[] = []; + const added: string[] = []; + + for (const raw of incoming) { + const item = raw.trim(); + if (!item) continue; + if (seen.has(item)) { + duplicates.push(item); + continue; + } + seen.add(item); + added.push(item); + } + + if (duplicates.length > 0) { + throw new Error(`以下列表项已存在:${duplicates.join("、")}`); + } + if (added.length === 0) { + throw new Error("请至少填写一个有效的列表项"); + } + return added; +} diff --git a/frontend/src/app/(app)/basic-dictionary/base/list/components/ListItemPanel.tsx b/frontend/src/app/(app)/basic-dictionary/base/list/components/ListItemPanel.tsx new file mode 100644 index 0000000..f0b223d --- /dev/null +++ b/frontend/src/app/(app)/basic-dictionary/base/list/components/ListItemPanel.tsx @@ -0,0 +1,234 @@ +"use client"; + +import { useCallback, useState } from "react"; +import { ArrowDown, ArrowUp, Plus, Trash2, Upload } from "lucide-react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Textarea } from "@/components/ui/textarea"; +import { + Dialog, + DialogBody, + DialogContent, + DialogFooter, +} from "@/components/common/shadcn-enhanced"; +import { appendListItems, normalizeNewItems, replaceListItems } from "../api"; + +type ListItemPanelProps = { + listDbId: string; + items: string[]; + onItemsChange: (items: string[]) => void; +}; + +export function ListItemPanel({ listDbId, items, onItemsChange }: ListItemPanelProps) { + const [addOpen, setAddOpen] = useState(false); + const [importOpen, setImportOpen] = useState(false); + const [newItem, setNewItem] = useState(""); + const [importText, setImportText] = useState(""); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [deleteIndex, setDeleteIndex] = useState(null); + + const runAction = useCallback(async (action: () => Promise<{ data?: string[] }>) => { + setSaving(true); + setError(null); + try { + const result = await action(); + onItemsChange(result.data ?? []); + } catch (event) { + setError(event instanceof Error ? event.message : "操作失败"); + } finally { + setSaving(false); + } + }, [onItemsChange]); + + const handleAddItem = async () => { + const added = normalizeNewItems(items, [newItem]); + await runAction(async () => appendListItems(listDbId, added)); + setNewItem(""); + setAddOpen(false); + }; + + const handleImport = async () => { + const lines = importText.split(/\r?\n/); + const added = normalizeNewItems(items, lines); + await runAction(async () => appendListItems(listDbId, added)); + setImportText(""); + setImportOpen(false); + }; + + const handleDelete = async () => { + if (deleteIndex === null) return; + const nextItems = items.filter((_, index) => index !== deleteIndex); + await runAction(async () => replaceListItems(listDbId, nextItems)); + setDeleteIndex(null); + }; + + const moveItem = async (index: number, direction: -1 | 1) => { + const target = index + direction; + if (target < 0 || target >= items.length) return; + const nextItems = [...items]; + [nextItems[index], nextItems[target]] = [nextItems[target], nextItems[index]]; + await runAction(async () => replaceListItems(listDbId, nextItems)); + }; + + return ( +
+
+
+

列表项 (list_item)

+

同一列表内项值不可重复;删除与排序通过 PUT 整表替换实现

+
+
+ + +
+
+ + {error ? ( +
+ {error} +
+ ) : null} + + + + + # + 项值 (item) + 操作 + + + + {items.length === 0 ? ( + + + 暂无列表项,可新增或批量导入 + + + ) : ( + items.map((item, index) => ( + + {index + 1} + {item} + +
+ + + +
+
+
+ )) + )} +
+
+ + + + +

填写目标对象 ID 或文本值,同一列表内不可重复

+ + setNewItem(event.target.value)} + placeholder="如 germplasm ID、study ID 或备注文本" + /> +
+ + + + +
+
+ + + + +

每行一个项值,重复项将拒绝导入

+