commit c670b9e454edc15080439e5eb9b3df17cbef2634 Author: freedakgmail Date: Sat Jun 13 01:06:39 2026 +0800 外包风险评估系统:领域引擎+前端+服务端持久化与生产部署 - 确定性领域引擎(分类/评分/分级/红线/费用/裁决)+LLM(通义千问)语言理解 - 6步评估向导、服务端草稿持久化(跨设备/编辑草稿保护) - 工作流(草稿→风控→管理层)、RBAC、报告导出、校准、客户/费率/红线/最低工资管理 - 专业图标体系替换全部emoji、看板美化 - 生产化:API_BASE可配置(同源反代)、auth密钥惰性读取修复RBAC - 444单测+204前端测试+51 e2e diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8e38239 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# LLM(通义千问 / DashScope 兼容模式)配置模板。 +# 复制为 .env 并填入你自己的 API Key。留空则关闭 LLM,回退到确定性规则引擎。 +DASHSCOPE_API_KEY= +LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 +LLM_MODEL=qwen-plus +LLM_TIMEOUT_MS=15000 + +# PostgreSQL 持久化(留空则用进程内存储) +DATABASE_URL=postgresql://localhost:5432/riskagent + +# RBAC 鉴权密钥(JWT 签名)。留空=演示模式(不强制鉴权); +# 配置后敏感操作(风控审核/管理层审批/override/红线裁定/费率红线最低工资配置)按角色强制校验。 +# 生产环境务必设置为高强度随机值。 +AUTH_SECRET= + +# 目标净利率基准(小数,可被预测准确度校准覆盖)。 +TARGET_NET_MARGIN=0.05 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..de60460 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,117 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + +jobs: + build-test: + name: 类型检查 / 单元测试 / 构建 + runs-on: ubuntu-latest + + services: + postgres: + # 使用含 pgvector 扩展的镜像(相似项目向量搜索迁移依赖) + image: pgvector/pgvector:pg16 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: riskagent + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/riskagent + # CI 不配置 LLM/AUTH:评估自动回退确定性规则引擎,RBAC 为演示模式 + DASHSCOPE_API_KEY: '' + AUTH_SECRET: '' + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: 安装依赖 + run: npm ci + + - name: 运行数据库迁移 + run: npm run migrate:up + + - name: 类型检查(前后端) + run: npm run typecheck + + - name: 单元 / 属性测试 + run: npm test + + - name: 构建 + run: npm run build + + e2e: + name: 端到端(API 全流程) + runs-on: ubuntu-latest + needs: build-test + + services: + postgres: + image: pgvector/pgvector:pg16 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: riskagent + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/riskagent + DASHSCOPE_API_KEY: '' + AUTH_SECRET: '' + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: 安装依赖 + run: npm ci + + - name: 运行数据库迁移 + run: npm run migrate:up + + - name: 构建 + run: npm run build + + - name: 播种 E2E 所需基础数据(客户档案) + run: node scripts/seed-e2e.mjs + + - name: 启动后端 + run: | + node dist/server/index.js & + echo $! > server.pid + for i in $(seq 1 30); do + curl -sf http://localhost:3005/api/health && break + sleep 1 + done + + - name: 运行端到端测试 + run: npm run e2e + + - name: 关闭后端 + if: always() + run: kill "$(cat server.pid)" || true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e31b802 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +coverage/ +*.tsbuildinfo +.DS_Store +*.log +.env +.env.local diff --git a/.kiro/specs/outsourcing-risk-assessment/.config.kiro b/.kiro/specs/outsourcing-risk-assessment/.config.kiro new file mode 100644 index 0000000..ee133a5 --- /dev/null +++ b/.kiro/specs/outsourcing-risk-assessment/.config.kiro @@ -0,0 +1 @@ +{"specId": "d7bb36fd-a687-4004-aea0-177620140635", "workflowType": "requirements-first", "specType": "feature"} diff --git a/.kiro/specs/outsourcing-risk-assessment/design.md b/.kiro/specs/outsourcing-risk-assessment/design.md new file mode 100644 index 0000000..d896a3a --- /dev/null +++ b/.kiro/specs/outsourcing-risk-assessment/design.md @@ -0,0 +1,1262 @@ +# Design Document + +## Overview + +本设计文档描述**外包项目风险评估 AI 智能体**(System)的技术方案,覆盖 25 条需求(含 Req 19-25 的统一设计系统、数据可视化、便捷评估流程、响应式适配、可访问性、操作反馈与角色化默认视图等 UI/前端需求)。System 是一个 Web 应用,由三部分构成:结构化评分表单、对话式自适应追问、风险评估报告生成与导出,并对评估结果进行持久化存档。 + +设计的核心原则是**指标体系与评分引擎解耦**:风险模型(维度 / 指标 / 权重 / 评分规则 / 红线)以结构化配置存储,Scoring_Engine 是一台"通用计算器"——读取任意合法配置即可完成评分、归一化、分级与红线校验,新增维度或指标只是一次配置操作,无需改动代码(Req 4、11、14)。 + +设计目标分解: + +- **可配置**:管理员统一维护风险模型,评估者只用不改,配置变更留痕审计(Req 11、12)。 +- **可量化**:在定性分级之外输出具体金额的费用/定价测算(Req 8)。 +- **可解释**:每个评分项可追溯到维度、指标、评分规则、数据点取值、来源与置信度(Req 18)。 +- **可扩展**:分行业知识库分区、可插拔外部数据适配层、地域参数化合规规则集(Req 14、15、16)。 +- **可验证**:评分 / 归一化 / 分级 / 红线 / 排序等纯算法逻辑以正确性属性约束,支撑属性化测试。 + +### Research Notes + +设计涉及的关键领域均为系统内部确定性逻辑,研究集中在三处: + +1. **线性归一化映射**:需求 4.3 规定当全部纳入计算的指标 Risk_Level 同取 1 时 Risk_Score=0、同取 5 时=100。由于评分项得分 = Risk_Level × 权重,且同级权重归一化后和为 1,加权汇总后的原始分天然落在区间 [1, 5]。因此归一化公式为 `Risk_Score = round((weightedRaw - 1) / (5 - 1) × 100)`。该公式与权重无关地满足端点约束,是设计的基础不变式。 +2. **分级区间互斥与完备**:需求 5 定义四个区间 [0,25]、(25,50]、(50,75]、(75,100],采用"左开右闭 + 首段闭"的边界约定,保证任意 0-100 整数恰好落入一级(互斥且完备)。 +3. **中国大陆合规规则**:劳务派遣用工比例上限 10%、经济补偿 N 与 N+1、社保缴费基数下限、当地最低工资标准(Req 16)。这些以 Compliance_Rule_Set 配置项形式落地,按 Region 加载,跨境通过新增规则集扩展而不改判定引擎。来源为《劳务派遣暂行规定》《劳动合同法》既有规则(内容用于建模规则结构,不作法律意见)。 + +## Architecture + +System 采用分层架构:表现层(角色化视图)、应用编排层(评估流程编排)、领域引擎层(分类 / 追问 / 评分 / 费用 / 策略 / 报告)、配置与知识层(配置中心 / 知识库 / 合规规则集)、适配层(外部数据)、持久化层。 + +```mermaid +graph TB + subgraph Presentation[表现层 - 角色化视图] + SalesView[商务/销售视图] + RiskView[风控视图] + MgmtView[管理层看板/组合看板] + ConfigUI[管理员配置界面] + end + + subgraph App[应用编排层] + Orchestrator[Assessment Orchestrator 评估流程编排] + AuthZ[角色权限控制 RBAC] + end + + subgraph Engines[领域引擎层] + Classifier[Classifier 业务类型/行业识别] + QEngine[Question_Engine 自适应追问] + SEngine[Scoring_Engine 评分/归一化/分级/红线] + CEngine[Cost_Engine 费用测算] + StratEngine[Strategy_Engine 应对策略] + Report[Report_Generator 报告生成/导出] + end + + subgraph ConfigLayer[配置与知识层] + ConfigCenter[Config_Center 模型配置中心] + KB[Knowledge_Base 分行业知识库] + Compliance[Compliance_Rule_Set 地域合规规则集] + end + + subgraph Adapters[适配层] + ExtAdapter[External_Data_Adapter 可插拔外部数据] + end + + subgraph Persistence[持久化层] + AssessmentStore[(Assessment 存储)] + ConfigStore[(配置/模板存储)] + AuditLog[(配置变更审计日志)] + end + + Presentation --> AuthZ --> Orchestrator + Orchestrator --> Classifier + Orchestrator --> QEngine + Orchestrator --> SEngine + Orchestrator --> CEngine + Orchestrator --> StratEngine + Orchestrator --> Report + Classifier --> KB + QEngine --> KB + SEngine --> ConfigCenter + SEngine --> Compliance + CEngine --> Compliance + QEngine --> ExtAdapter + ConfigCenter --> ConfigStore + ConfigCenter --> AuditLog + KB --> ConfigStore + Orchestrator --> AssessmentStore + ConfigUI --> AuthZ +``` + +### 评估流程编排(端到端) + +```mermaid +sequenceDiagram + participant U as 评估者 + participant O as Orchestrator + participant C as Classifier + participant KB as Knowledge_Base + participant Q as Question_Engine + participant E as External_Data_Adapter + participant S as Scoring_Engine + participant Cost as Cost_Engine + participant St as Strategy_Engine + participant R as Report_Generator + participant DB as Assessment Store + + U->>O: 提交项目描述 (+可选 Region) + O->>C: 识别业务类型/行业 + C-->>O: 类型/行业 + Confidence + alt Confidence < 0.6 + O->>U: 展示 Top3 候选请确认 + U-->>O: 确认选择 + end + O->>KB: 按 类型×行业 加载 Template + KB-->>O: Template (含继承解析) + O->>O: 实例化 Risk_Model (校验权重/必填) + loop 信息缺口 (≤ 最大追问轮次) + O->>Q: 识别缺口/生成追问 + Q->>E: 尝试外部数据取值 + E-->>Q: 命中/降级回退 + Q->>U: 追问问题 + U-->>Q: 回答 + end + Q->>Q: 缺口达上限→行业默认值(标注"智能体假设") + O->>S: 评分/归一化/分级/红线 + S-->>O: Risk_Score/Risk_Grade/Redline 结果 + O->>Cost: 费用测算 + O->>St: 可接受性结论 + 应对策略 + O->>R: 生成报告 + O->>DB: 持久化 Assessment +``` + +### 关键架构决策 + +| 决策 | 理由 | 关联需求 | +|------|------|----------| +| 配置驱动的通用评分引擎 | 新增维度/指标/行业零代码改动 | Req 4、11、14 | +| 红线校验独立于分值通道 | 一票否决须优先于分级,且不受 Risk_Score 高低影响 | Req 6 | +| Data_Provenance 三态贯穿全链路 | 可解释性与降级回退的统一载体 | Req 15、18 | +| Region 参数化合规规则集 | 中国先行,跨境靠新增规则集扩展 | Req 16 | +| 适配层接口化 + 超时降级 | 外部源不可用不阻断评估 | Req 15 | +| 前端单一 Design_System 驱动 | 组件/排版/配色/间距/图标全局一致,新增页面零样式漂移 | Req 19 | +| 配色与主题以 Color_Token 抽象 | 风险等级语义化配色单一来源,主题切换仅切换令牌取值 | Req 19、20、23 | +| 视图层与领域引擎严格解耦 | UI 仅消费 Orchestrator/各引擎输出,纯算法属性不受 UI 影响 | Req 13、19-25 | + +### 前端架构与设计系统(表现层细化) + +表现层采用与既有技术栈一致的 **TypeScript** 实现,建议技术选型如下(均为成熟生态、不自研基础设施): + +| 关注点 | 选型建议 | 理由 / 关联需求 | +|--------|----------|------------------| +| 组件化前端框架 | React + TypeScript | 组件化、状态可测、生态成熟;便于组件测试与可视化回归 (Req 19、21) | +| 设计系统载体 | 设计令牌(Design Tokens)+ 组件库(如基于 Radix/Headless 模式封装) | Color_Token / 排版 / 间距 / 图标单一来源 (Req 19) | +| 数据可视化 | 图表库(如 ECharts 或 Recharts/Visx) | 热力图、仪表盘、对比图等专业图表,支持图例/标签/图案编码 (Req 20、23) | +| 主题方案 | CSS 自定义属性(CSS Variables)承载 Color_Token,按 Theme 切换变量集 | Light/Dark 切换不重渲染数据状态 (Req 19.6、19.7) | +| 路由与角色导航 | 客户端路由 + 角色守卫(RBAC route guard) | 角色化默认视图与无角色拦截 (Req 25) | +| 表单与草稿 | 受控表单 + 本地/服务端 Draft 持久化 | 断点续评、切换录入方式不丢数据 (Req 21) | +| 可访问性 | 语义化 HTML + ARIA + 自动化检查(axe) | WCAG 2.1 AA (Req 23) | + +#### 前端分层 + +```mermaid +graph TB + subgraph DS[Design_System 设计系统层] + Tokens[Design Tokens
Color_Token / 排版 / 间距(4px) / 图标] + ThemeProvider[Theme Provider
Light / Dark] + Primitives[基础组件
Button/Input/Table/Card/Dialog/Nav/Toast] + end + subgraph Viz[可视化组件层 Charts] + Heatmap[风险热力图] + Gauge[风险总分仪表盘] + Badge[Risk_Badge] + TopN[Top N 关键风险图] + CostChart[费用拆解/报价对比图] + Portfolio[组合对比图] + end + subgraph Flow[流程与视图层] + WizardC[Wizard 向导] + FormDlg[表单/对话式录入] + GapPanel[缺口提示面板] + RoleViews[角色化视图: 商务/风控/管理层] + end + subgraph State[前端状态层] + UIStore[UI 状态: 操作态/Loading/Empty] + DraftStore[Draft 草稿状态] + ViewportSvc[Viewport/断点服务] + end + Tokens --> Primitives + ThemeProvider --> Tokens + Primitives --> Viz + Primitives --> Flow + Tokens --> Viz + Viz --> RoleViews + Flow --> UIStore + Flow --> DraftStore + ViewportSvc --> Flow + RoleViews -->|消费输出| AppLayer[应用编排层/领域引擎] +``` + +#### 风险等级语义化配色映射 + +Design_System 为四级 Risk_Grade 各定义唯一且稳定的 Color_Token(每个 Theme 下各有取值,但令牌名稳定)。UI 在 Risk_Badge、风险热力图、Top N 关键风险图等所有等级相关呈现中,对同一 Risk_Grade 一律引用同名 Color_Token,禁止内联硬编码颜色(Req 19.3)。 + +| Risk_Grade | Color_Token(令牌名) | 语义 | 非颜色编码(Req 23.6) | +|------------|----------------------|------|------------------------| +| 低 | `color.risk.low` | 安全/可接受 | 文本标签"低" + 实心图案 | +| 中 | `color.risk.medium` | 关注 | 文本标签"中" + 斜纹图案 | +| 高 | `color.risk.high` | 警示/有条件 | 文本标签"高" + 网格图案 | +| 极高 | `color.risk.critical` | 否决/不可接受 | 文本标签"极高" + 实心粗边图案 | + +热力图的 Risk_Level(1-5)严重度同样映射到一组稳定的顺序色令牌(`color.heat.1..5`),并辅以数值标签,确保不依赖颜色即可识别(Req 20、23.6)。 + +## Components and Interfaces + +下列接口以语言无关的伪签名描述,标注其核心契约与关联需求。 + +### Classifier(业务类型与行业识别) + +``` +classify(description: string): ClassificationResult + // 前置:description 有效字符数 ≥ 10 且非纯空白,否则抛 InsufficientInputError (Req 1.6) + // 返回 businessType + industry + 各自 Confidence(0-1, 两位小数) (Req 1.1-1.3) + // businessType 为五类中 Confidence 最高的唯一项 + // industry 无法判定时取 "未识别" + +ClassificationResult { + businessType: BusinessType // 唯一最高置信 + businessTypeCandidates: Candidate[] // 按 Confidence 降序,≤3 项 + businessTypeConfidence: number + industry: string // 或 "未识别" + industryCandidates: Candidate[] + industryConfidence: number + needsBusinessTypeConfirm: boolean // businessTypeConfidence < 0.6 (Req 1.4) + needsIndustryConfirm: boolean // industryConfidence < 0.6 且 industry≠"未识别" (Req 1.5) +} + +confirmClassification(assessmentId, businessType, industry): void + // 评估者修改后以确认值为后续加载模板依据 (Req 1.7) +``` + +### Config_Center(模型配置中心) + +``` +loadTemplate(businessType, industry): Template // 精确匹配;回退默认 (Req 2.1-2.2, 2.6) +resolveInheritance(template): Template // 父→子逐项覆盖;环/层级>5 报错 (Req 2.5, 2.7) +instantiateRiskModel(template): RiskModel // 校验必填+权重和=100% (Req 2.4) +saveConfig(actor, config): SaveResult // 仅 Administrator;校验后保存 (Req 11.6-11.8) +normalizeWeights(siblings): Weight[] // 按比例归一化,和=100%,两位小数 (Req 11.2) +saveAsTemplate(actor, config): Template // 另存/派生 (Req 11.5) +``` + +权限边界:所有写操作前置 `requireRole(Administrator)`;非管理员(含 Assessor、未认证)写操作一律拒绝并审计(Req 12.1、12.3、12.5)。 + +### Question_Engine(自适应追问引擎) + +``` +identifyGaps(riskModel, knownData): Indicator[] // 启用且信息不足判定 Risk_Level 的指标 (Req 3.1) +generateQuestions(gaps): Question[] // 按 Indicator 权重降序,同权按 Dimension 权重降序 (Req 3.2) +answerQuestion(indicatorId, answer): UpdateResult // 空/不满足证据→保留缺口 (Req 3.3-3.4) +applyDefaultsOnExhaust(indicator): void // 达最大轮次(默认3,可配)→行业默认值+标注"智能体假设" (Req 3.5-3.6) +// 不变式:Data_Provenance 一旦为"智能体假设",永久保留 (Req 3.7) + +Question { id, dimensionId, indicatorId, prompt, round } // 关联维度与指标 (Req 3.8) +``` + +### Scoring_Engine(评分引擎) + +``` +scoreIndicator(indicator): number // Risk_Level(1-5) × 权重 (Req 4.1) +scoreDimension(dimension): number // 启用指标评分项加权求和 (Req 4.2) +computeRiskScore(riskModel): number // 线性映射归一化→0-100 整数 (Req 4.3-4.4) + // 仅纳入启用的 Dimension/Indicator;无启用项→ScoringError (Req 4.5) +classifyGrade(riskScore): RiskGrade // 四级互斥完备 (Req 5) +checkRedlines(assessment): RedlineResult[] // 独立于分值;缺数据/假设→"待核实"不计命中 (Req 6) +buildHeatmap(riskModel): HeatmapCell[] // 维度×指标×Risk_Level (Req 7.1) +topKeyRisks(riskModel, n): RiskItem[] // 得分降序 Top N,确定性消歧 (Req 7.2-7.5) +explain(scoringItem): Explanation // 判定依据/影响/建议非空 (Req 18.1-18.3) +``` + +归一化公式(核心): + +``` +weightedRaw = Σ_dim ( dimWeight × Σ_ind ( indWeight × riskLevel ) ) + // dimWeight、indWeight 已归一化(同级和=1),故 weightedRaw ∈ [1,5] +riskScore = round( (weightedRaw - 1) / 4 × 100 ) // ∈ [0,100] +``` + +排序消歧(Top N):主键评分项得分降序 → 次键所属 Dimension 权重降序 → 三键 Indicator 权重降序 → 末键 Indicator 稳定标识升序,保证确定性(Req 7.3、7.5)。 + +### Cost_Engine(费用测算引擎) + +``` +estimate(assessment, costInputs): CostEstimate + // 前置:评分已完成,否则 ScoringNotCompleteError (Req 8.5) + // 缺成本输入→行业默认值+标注"智能体假设" (Req 8.4) + +CostEstimate { + riskPremiumRange: Range // 金额或百分比区间,依 Risk_Grade+评分 (Req 8.1) + advanceInterest: Money // 垫资利息 + insuranceCost: Money // 保险费用 + compensationReserve: Money // 补偿准备金 + badDebtReserve: Money // 坏账准备金 + baselineQuote: Money // 基准报价 + riskAdjustedQuote: Money // 风险调整后报价 + breakdown: CostLineItem[] // 每项标注依据输入项+费率/参数来源 (Req 8.2,8.3,8.6) +} +``` + +### Strategy_Engine(应对策略推荐引擎) + +``` +decide(riskGrade, redlineHit): Acceptability // 可接受/有条件接受/不可接受 + // 决策表见下;红线命中或极高→不可接受 (Req 9.6-9.8) +managementMeasures(...): Measure[] // 合同条款/用工合规整改/退场预案/过程监控 各≥1 (Req 9.3) +costMeasures(...): Measure[] // 风险溢价/预付保证金/保险转移/账期成本/准备金计提 各≥1 (Req 9.4) +acceptanceConditions(...): Condition[] // 有条件接受时,每条件关联≥1 关键风险 + 成本影响 (Req 9.2,9.5) +``` + +可接受性决策表: + +| 红线命中 | Risk_Grade | 可接受性结论 | 关联需求 | +|----------|-----------|--------------|----------| +| 是 | 任意 | 不可接受 | 6.2、9.8 | +| 否 | 低 | 可接受 | 9.6 | +| 否 | 中 | 可接受 | 9.6 | +| 否 | 高 | 有条件接受 | 9.7 | +| 否 | 极高 | 不可接受 | 9.8 | + +### External_Data_Adapter(外部数据适配层) + +``` +interface DataSourceAdapter { // 可插拔,新增源不改 Scoring_Engine (Req 15.5) + fetch(query): DataPoint[] // 单次请求 10 秒超时 (Req 15.1) +} +// 成功:标注 Data_Provenance="外部数据" + Confidence(0-1) (Req 15.2) +// 失败/超时/错误:回退用户输入,标注"用户输入" (Req 15.3) +// 回退后仍缺失:标注"智能体假设",不中断 (Req 15.4) +``` + +### Report_Generator(报告生成与导出) + +``` +generate(assessment): Report + // 前置:评分+红线+费用+可接受性结论均完成,否则 FlowNotCompleteError (Req 10.4) + // 含项目概要/总分分级/热力图/维度明细/Top风险+红线结果/结论/应对/假设缺口 (Req 10.1) + // 未命中红线亦明确标注"无红线命中" (Req 10.1) +export(report, format): DownloadFile + // 请求后 30 秒内导出完整自包含文件 (Req 10.2) + // 失败→中止,保留报告不变,返回错误 (Req 10.5) +``` + +### 角色权限与视图层(RBAC + Views) + +``` +requireRole(role): void // 鉴权 +renderView(role, assessment): View // 角色化视图 (Req 13.1-13.3) + // 商务/销售:结论+接受条件+风险调整后报价 + // 风控:评分明细(Risk_Level/依据/Provenance)+红线+缺口尽调 + // 管理层:Grade+热力图+TopN+利润vs风险看板 +renderPortfolio(assessmentIds): PortfolioView // 跨项目组合看板;空集→空看板提示 (Req 13.4,13.6) +// 无角色→拒绝展示并提示分配角色 (Req 13.5) +``` + +### 评估持久化层 + +``` +save(assessment): SaveResult // 完成即持久化输入/评分/报告/元数据 (Req 17.1) + // 失败→保留会话数据+返回错误 (Req 17.2) +reassess(assessmentId): Assessment // 基于原输入创建新评估,原评估不变 (Req 17.3-17.4) +compare(assessmentIds): ComparisonResult // ≥2 个;否则拒绝 (Req 17.5-17.6) +search(filters): Assessment[] // 按类型/行业/Grade/时间;无匹配→空集 (Req 17.7) +``` + +### Design_System(设计系统与主题) + +``` +interface DesignSystem { + typography: TypographyScale // ≥4 级具名层级,各含固定字号与行高 (Req 19.2) + spacing: SpacingScale // 4px 基数标度,取值均为 4 的整数倍 (Req 19.4) + icons: IconSet // 单一来源图标集 (Req 19.5) + components: ComponentLibrary // Button/Input/Table/Card/Dialog/Nav/Toast 等 (Req 19.1) +} + +resolveColorToken(token: ColorToken, theme: Theme): ColorValue + // 对任意令牌名与主题,确定性返回该主题下取值 (Req 19.6) +riskGradeColorToken(grade: RiskGrade): ColorToken + // 四级各映射唯一稳定令牌名,全 UI 共用 (Req 19.3) +contrastRatio(fg: ColorValue, bg: ColorValue): number + // 计算 WCAG 相对对比度,用于达标校验 (Req 23.3) + +setTheme(theme: Theme): void + // 切换 Light/Dark:仅替换 Color_Token 取值,保留页面已录入数据 (Req 19.6, 19.7) + +TypographyScale { levels: TypographyLevel[≥4] } // 每级: name, fontSize, lineHeight +SpacingScale { base: 4, steps: number[] } // 每个 step % 4 == 0 +Theme = Light | Dark +``` + +### 数据可视化组件(Charts) + +``` +renderChart(spec: ChartSpec): ChartView + // 通用契约: + // - data 为空 → 呈现 Empty_State 并提示无可展示数据 (Req 20.4) + // - data 处于请求/计算中 → 呈现 Loading_State (Req 20.5) + // - 系列/类别 ≥2 → 提供图例,图例标签与数据元素一致 (Req 20.2) + // - 坐标轴/数据点/分区提供文本标签 (Req 20.3) + // - 类别以"文本标签或图案"在颜色之外区分 (Req 23.6) + +ChartView 变体: + Heatmap // Dimension×Indicator×Risk_Level,单元格附数值标签 (Req 20.1) + ScoreGauge // 同时呈现 Risk_Score 数值 + 对应 Risk_Grade (Req 20.6) + RiskBadge // 语义化 Color_Token + 文字标签呈现 Risk_Grade (Req 20.1) + TopNRiskChart // Top N 关键风险条形图 (Req 20.1) + CostBreakdown // 费用拆解图 (Req 20.1) + QuoteCompare // 基准报价/风险调整后报价/差额三值并呈,差额=风险调整后-基准 (Req 20.7) + PortfolioCompare // 跨项目组合对比图 (Req 20.1) + +ChartSpec { type, series: Series[], status: ready|loading|empty, legend, labels } +``` + +### Wizard(向导式评估流程) + +``` +WizardState { + steps: Step[] // 有序步骤 + currentIndex: number // 当前步骤序号 + total: number // 步骤总数 + completed: boolean[] // 已完成步骤进度 (Req 21.1) +} +progress(state): { current, total, completedCount } // 进度指示 (Req 21.1) +advance(state): WizardState // 推进;进度 completedCount 单调非减 (Req 21.1) + +switchInputMode(state, mode: 对话式|表单): WizardState + // 切换录入方式保留切换前已录入数据 (Req 21.2) + +listGaps(assessment): GapItem[] // 信息缺口/待补充项 +renderGaps(gaps): GapPanel + // 以醒目样式呈现全部缺口项 + 提供定位至录入位置入口 (Req 21.3) + +quickActions(): { submit, saveDraft, exportReport } // 各一键入口 (Req 21.4) + +saveDraft(assessment): Draft // 持久化为 Draft,保留全部录入数据 (Req 21.5) +openDraft(draftId): { assessment, wizardStep } + // 恢复全部录入数据并定位至保存时步骤 (Req 21.6) +confirmLeaveIfDirty(state): LeaveDecision + // 存在未保存修改时提示并请求确认 (Req 21.7) +``` + +### 响应式布局服务(Viewport) + +``` +selectLayout(viewportWidth: number): Layout + // 确定性断点映射 (Req 22.1, 22.2, 22.3, 22.4): + // width ≥ 1280 → DesktopLayout(全部功能) + // 768 ≤ width ≤ 1279 → CompactLayout(无横向滚动条,内容完整可见) + // width < 768 → MobileLayout(看板保留 Risk_Score/Risk_Grade/Top N) +onViewportChange(prevState, newWidth): UIState + // 跨断点切换应用匹配布局且不丢失已录入数据 (Req 22.4) + +Layout = DesktopLayout | CompactLayout | MobileLayout +``` + +### 可访问性(Accessibility, WCAG 2.1 AA) + +``` +// 交互控件:可键盘聚焦并触发,聚焦时呈现可见焦点指示 (Req 23.1, 23.2) +// 对比度:正文 ≥ 4.5:1、大号文本 ≥ 3:1,由 contrastRatio 校验 (Req 23.3) +// 表单:每个输入控件关联可被辅助技术识别的文本标签 (Req 23.4) +associateLabel(input): { controlId, labelId, accessibleName } // 非空可达 (Req 23.4) +renderFieldError(input, validation): AccessibleError + // 校验失败为每个未通过控件呈现可被辅助技术识别的错误提示 (Req 23.5) +// 图表:颜色之外以文本标签/图案区分类别 (Req 23.6) +``` + +### 操作反馈与状态(Feedback & States) + +``` +OperationState = idle | loading | success | error +runOperation(op): OperationState + // 进行中 → Loading_State (Req 24.1) + // 成功 → 成功反馈 (Req 24.2) + // 失败 → 可读错误信息(说明原因)+ 指向修正路径的操作入口 (Req 24.3) +exportWithProgress(report): ExportResult + // 导出执行中呈现进度反馈,30 秒内呈现完成或失败结果 (Req 24.4) +``` + +### 角色化默认视图与导航(Role-based Default Views) + +``` +defaultRoute(role: Role): Route + // 确定性映射 (Req 25.1-25.3): + // 商务/销售 → SalesView + // 风控 → RiskView + // 管理层 → ManagementDashboard + // 无角色 → AssignRolePrompt(不呈现评估数据) (Req 25.5) +renderDefaultView(role): View + // 角色匹配的功能入口呈现于无需额外导航即可见的位置 (Req 25.4) +``` + +## Data Models + +```mermaid +erDiagram + RiskModel ||--o{ Dimension : contains + Dimension ||--o{ Indicator : contains + Indicator ||--|{ ScoringRule : defines + RiskModel ||--o{ Redline : has + Template ||--o| Template : inherits + Template ||--|| RiskModel : instantiates + Assessment ||--|| RiskModel : uses + Assessment ||--o{ ScoringItem : produces + Assessment ||--|| Region : adopts + Region ||--|| ComplianceRuleSet : maps + ScoringItem ||--|| DataProvenance : annotated + KnowledgeBase ||--o{ Template : stores +``` + +### 核心实体 + +``` +RiskModel { + id, name, businessType, + dimensions: Dimension[], + redlines: Redline[] +} + +Dimension { + id, name, + weight: number, // 0-100,同级启用项归一化后和=100% + enabled: boolean, // 停用保留配置但不计分 (Req 11.1) + indicators: Indicator[] +} + +Indicator { + id, name, + weight: number, // 0-100,同级启用项归一化后和=100% + enabled: boolean, + scoringRules: ScoringRule[5], // 覆盖 Risk_Level 1-5 (Req 11.3) + evidenceRequired: string, + askPrompt: string +} + +ScoringRule { level: 1..5, label, description } // 判定标准 + +Redline { + id, // 唯一标识 (Req 11.4) + triggerCondition, // 触发条件(独立于分值) + consequence, // 一票否决后果 + enabled: boolean +} + +Template { + id, name, businessType, industry, + parentTemplateId?, // 继承;环或层级>5 非法 (Req 2.5,2.7) + isDefault: boolean, // 业务类型默认模板 (Req 2.2) + riskModelConfig +} +``` + +### 评估与可解释性实体 + +``` +Assessment { + id, projectDescription, + businessType, industry, + region: Region, // 默认 CN,记录所采用 Region (Req 16.4-16.5) + riskModel: RiskModel, // 实例化快照 + scoringItems: ScoringItem[], + riskScore: 0..100, + riskGrade: 低|中|高|极高, + redlineResults: RedlineResult[], + costEstimate: CostEstimate, + acceptability: 可接受|有条件接受|不可接受, + report: Report, + metadata: { businessType, industry, region, riskScore, riskGrade, createdAt, assessorId }, // (Req 17.1) + createdAt, assessorId +} + +ScoringItem { + dimensionId, indicatorId, + riskLevel: 1..5, + score: number, // riskLevel × indicatorWeight + provenance: 用户输入|外部数据|智能体假设, // (Req 18.4) + confidence: 0..1, // 两位小数 + rationale: string, // 非空,引用 Dimension/Indicator/ScoringRule/数据点 (Req 18.1) + riskImpact: string, // 非空 (Req 18.2) + recommendation: string // 非空 (Req 18.3) +} + +RedlineResult { + redlineId, + status: 命中|未命中|待核实, // 缺数据/假设→待核实,不计命中 (Req 6.5) + triggeredCondition, evidenceData +} + +DataProvenance = 用户输入 | 外部数据 | 智能体假设 // 三态 +``` + +### 合规与地域实体 + +``` +Region { code, name } // 首版 CN=中国大陆 + +ComplianceRuleSet { + region, + socialInsuranceBase: { lowerBound }, // 社保缴费基数下限 + economicCompensation: { nRule, nPlusOneRule }, // N / N+1 + dispatchRatioCap: 0.10, // 劳务派遣比例上限 10% + minimumWage: { byLocality } // 当地最低工资标准 +} +// Region 无对应规则集→拒绝合规判定/费用测算+提示暂不支持 (Req 16.6) +``` + +### 知识库实体 + +``` +KnowledgeBase { + partitions: IndustryPartition[] // 按行业分区 (Req 14.1) +} +IndustryPartition { + industryId, + indicators, weightTemplates, redlines, cases, askPrompts // 五类必备 (Req 14.1,14.4) +} +// 缺任一类→拒绝创建分区+指明缺失类别 (Req 14.4) +// 无匹配行业分区→默认分区+标注未匹配 (Req 14.5) +``` + +### 审计日志实体 + +``` +ConfigAuditEntry { + actorId, timestamp, // 精确到秒 + action: 变更提交|变更拒绝, + changedConfigKeys: string[], // 成功时记录变更项 (Req 12.4) + reason // 拒绝时记录 (Req 12.5) +} +``` + +### UI 与设计系统实体 + +``` +ColorToken = string // 具名稳定令牌,如 color.risk.low (Req 19.3) +Theme = Light | Dark + +DesignTokens { + colors: Map, // 每令牌双主题取值 (Req 19.6) + typography: TypographyLevel[], // ≥4 级,各具名 + 固定字号 + 固定行高 (Req 19.2) + spacing: number[], // 均为 4 的整数倍 (Req 19.4) + icons: IconRef[] // 单一来源 (Req 19.5) +} + +TypographyLevel { name, fontSize, lineHeight } + +RiskGradeColorMap { // 四级 → 唯一稳定令牌 (Req 19.3) + 低: color.risk.low, 中: color.risk.medium, 高: color.risk.high, 极高: color.risk.critical +} + +Draft { // 断点续评中间状态 (Req 21.5,21.6) + id, baseAssessmentId?, + enteredData, // 全部已录入数据快照 + wizardStep: number, // 保存时所处步骤 + inputMode: 对话式|表单, + savedAt +} + +GapItem { indicatorId, dimensionId, label, locateAnchor } // 缺口定位 (Req 21.3) + +ChartViewModel { + type, status: ready|loading|empty, // 驱动 Empty_State/Loading_State (Req 20.4,20.5) + series: Series[], legend?: LegendItem[], // ≥2 系列必有图例 (Req 20.2) + labels: Label[], // 轴/点/分区文本标签 (Req 20.3) + categoryEncoding: { colorToken, textLabel, pattern } // 非纯颜色编码 (Req 23.6) +} + +OperationFeedback { state: idle|loading|success|error, message?, correctiveAction? } // (Req 24) + +Role = 商务/销售 | 风控 | 管理层 | 无角色 +RouteMap { 商务/销售: SalesView, 风控: RiskView, 管理层: ManagementDashboard, 无角色: AssignRolePrompt } // (Req 25) + +Breakpoint = Desktop(≥1280) | Compact(768-1279) | Mobile(<768) // (Req 22) +``` + +## Correctness Properties + +*A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* + +下列属性由需求验收标准经可测试性分析(prework)转化而来,并经冗余消解(互斥完备性蕴含、决策表合并、多级排序合并、可解释性三要素合并、来源标注合并等)。每条均为全称量化("对任意…"),用于属性化测试。非属性类标准(语义识别、外部 IO、导出时延、架构解耦等)由示例测试与集成测试覆盖,见 Testing Strategy。 + +### Property 1: 业务类型判定唯一且取最高置信 + +*对任意*有效项目描述产生的业务类型置信分布,Classifier 输出的 businessType *必为*五类业务类型中 Confidence 最高的*唯一*一项。 + +**Validates: Requirements 1.1** + +### Property 2: 置信度恒在有效值域内 + +*对任意*分类结果,业务类型 Confidence 与行业 Confidence *均*落在区间 [0, 1] 内且保留两位小数。 + +**Validates: Requirements 1.3** + +### Property 3: 低置信触发候选确认 + +*对任意*分类结果,当某判定的 Confidence 低于 0.6(行业判定附加条件:行业标记不为"未识别")时,System *必*返回按 Confidence 降序排列、数量至多 3 项的候选列表并置确认标志为真;否则不触发确认。 + +**Validates: Requirements 1.4, 1.5** + +### Property 4: 描述信息不足必被拒绝 + +*对任意*为空、仅含空白字符或有效字符数少于 10 的项目描述,System *必*拒绝执行业务类型与行业判定并返回信息不足错误;而任意有效字符数不少于 10 的非空白描述*不会*因长度被拒。 + +**Validates: Requirements 1.6** + +### Property 5: 确认值驱动后续加载 + +*对任意*评估者确认或修改后的(业务类型, 行业),System 后续加载模板*必*以该确认值为依据。 + +**Validates: Requirements 1.7** + +### Property 6: 模板匹配与回退确定性 + +*对任意*知识库与(业务类型, 行业)组合:若存在精确匹配模板则*必*选中该精确模板;若无精确匹配但存在该业务类型默认模板,则*必*选中默认模板并标注"未匹配行业专用模板";若两者皆无则*必*终止并返回无可用模板提示。 + +**Validates: Requirements 2.1, 2.2, 2.6, 14.3, 14.5** + +### Property 7: 模板实例化结构保持 + +*对任意*合法模板,实例化为 Risk_Model 后*必*完整保留其全部 Dimension、Indicator、权重、Scoring_Rule、Redline、追问话术及各项启用/停用状态,无丢失无篡改。 + +**Validates: Requirements 2.3** + +### Property 8: 非法模板必被拒绝实例化 + +*对任意*缺失必填组成项、或同级 Dimension 权重之和/同级 Indicator 权重之和不等于 100% 的模板,System *必*不实例化、终止评估并返回模板数据错误。 + +**Validates: Requirements 2.4** + +### Property 9: 模板继承逐项覆盖 + +*对任意*父子模板对,继承解析结果*必*等于"应用父模板全部组成项后,以子模板中存在差异的组成项逐项覆盖"。 + +**Validates: Requirements 2.5** + +### Property 10: 继承链环与深度防护 + +*对任意*模板继承图,若存在循环引用或继承层级超过 5 层,System *必*终止并返回继承错误;任意无环且层级不超过 5 的合法链*必*成功解析。 + +**Validates: Requirements 2.7** + +### Property 11: 信息缺口识别准确 + +*对任意*已知信息集与实例化模型,被标记为信息缺口的指标集合*恰为*{启用 ∧ 依据其 Scoring_Rule 无法判定 Risk_Level} 的指标集合。 + +**Validates: Requirements 3.1** + +### Property 12: 追问仅针对缺口且排序确定 + +*对任意*缺口指标集合,Question_Engine 生成的问题*仅*覆盖缺口指标,且按 Indicator 权重降序排列、Indicator 权重相同时按所属 Dimension 权重降序消歧;每个问题*必*关联非空且真实存在的 Dimension 与 Indicator。 + +**Validates: Requirements 3.2, 3.8** + +### Property 13: 有效回答移出缺口 + +*对任意*缺口指标,给出满足证据要求的回答后,该指标*必*从缺口集合移除并更新已知信息。 + +**Validates: Requirements 3.3** + +### Property 14: 无效回答保留缺口 + +*对任意*空回答或不满足证据要求的回答,对应指标的信息缺口标记*必*保留,并返回需补充信息的提示。 + +**Validates: Requirements 3.4** + +### Property 15: 追问轮次不超上限并触发兜底 + +*对任意*可配置的最大追问轮次(正整数,默认 3)与任意追问序列,单个指标的追问轮次*恒*不超过该上限;当轮次达上限仍缺失时,该指标*必*采用行业默认值且 Data_Provenance 被标注为"智能体假设"。 + +**Validates: Requirements 3.5, 3.6** + +### Property 16: 智能体假设标注单调永久 + +*对任意*已被标注为"智能体假设"的数据点,施加任意后续补充、更新或重算操作序列后,其 Data_Provenance *恒*保持"智能体假设",不被改回其他取值。 + +**Validates: Requirements 3.7** + +### Property 17: 评分项得分公式 + +*对任意*启用指标,其评分项得分*恒*等于该指标 Risk_Level(1 至 5)乘以其权重;维度得分*恒*等于其下各启用指标评分项得分的加权求和。 + +**Validates: Requirements 4.1, 4.2** + +### Property 18: 归一化端点与值域 + +*对任意*启用指标集合:当全部纳入计算的指标 Risk_Level 同取最小值 1 时 Risk_Score *必*为 0、同取最大值 5 时 *必*为 100;且*对任意* Risk_Level 组合,Risk_Score *恒*为 [0, 100] 内的整数(四舍五入取整)。 + +**Validates: Requirements 4.3** + +### Property 19: 停用项不影响评分 + +*对任意*风险模型,改动任何停用 Dimension 或停用 Indicator 的 Risk_Level 或权重,*均不*改变 Risk_Score(汇总仅纳入启用项)。 + +**Validates: Requirements 4.4** + +### Property 20: 无启用项必报错 + +*对任意*不存在任何启用 Indicator 或启用 Dimension 的评估,Scoring_Engine *必*终止评分、不产生 Risk_Score 并返回评分数据不足错误。 + +**Validates: Requirements 4.5** + +### Property 21: 评分项来源与置信标注 + +*对任意*评分项,*必*记录 Data_Provenance(取值为 用户输入 / 外部数据 / 智能体假设 之一)与落在 [0, 1] 内的 Confidence。 + +**Validates: Requirements 4.6, 18.4** + +### Property 22: 分级区间互斥且完备 + +*对任意* [0, 100] 内整数 Risk_Score,Scoring_Engine *必*输出且仅输出一个 Risk_Grade,且其取值严格遵循区间 [0,25]→低、(25,50]→中、(50,75]→高、(75,100]→极高。 + +**Validates: Requirements 5.1, 5.2, 5.3, 5.4, 5.5** + +### Property 23: 红线校验正确且独立于分值 + +*对任意*评估与启用红线集合,满足触发条件的红线*恰被*标记为命中、不满足的*不被*标记;且*对任意*改变其他评分项使 Risk_Score/Risk_Grade 变化但不改变红线相关数据点的操作,红线命中结果*恒*不变。 + +**Validates: Requirements 6.1, 6.4** + +### Property 24: 红线数据不足标待核实 + +*对任意*启用红线,若其触发条件所需数据缺失或 Data_Provenance 为"智能体假设"以致无法确定是否命中,该红线*必*被标记为"待核实"且*不*计为命中。 + +**Validates: Requirements 6.5** + +### Property 25: 命中红线列入报告 + +*对任意*命中红线的评估,报告*必*列出每个被命中红线及其被触发的条件与对应判定依据数据。 + +**Validates: Requirements 6.3** + +### Property 26: 可接受性决策表完备且红线最高优先 + +*对任意*(红线是否命中, Risk_Grade)组合,Strategy_Engine 输出的可接受性结论*必*为唯一确定值:命中任一红线时*恒*为"不可接受"(优先于 Grade);未命中时,Grade 为低或中→"可接受"、高→"有条件接受"、极高→"不可接受"。 + +**Validates: Requirements 6.2, 6.6, 9.1, 9.6, 9.7, 9.8** + +### Property 27: 应对措施类别全覆盖 + +*对任意*结论为"可接受"或"有条件接受"的评估,管理层面措施*必*在合同条款、用工合规整改、退场预案、过程监控四类中各输出至少一项,费用层面措施*必*在风险溢价定价、预付/保证金、保险转移、账期成本、准备金计提五类中各输出至少一项。 + +**Validates: Requirements 9.3, 9.4** + +### Property 28: 有条件接受的条件关联与成本影响 + +*对任意*结论为"有条件接受"的评估,接受条件清单中每个条件*必*关联至少一个关键风险清单中的风险项,且*必*输出取值为具体金额或金额区间的成本影响测算。 + +**Validates: Requirements 9.2, 9.5** + +### Property 29: 热力图覆盖全部启用指标 + +*对任意*风险模型,热力图数据*必*为每个启用 Indicator 提供一个以其所属 Dimension 为行、Indicator 为列、Risk_Level(1 至 5)为严重度的单元格。 + +**Validates: Requirements 7.1** + +### Property 30: Top N 确定性排序 + +*对任意*评分项集合与可配置 N(1 至 50,默认 10),关键风险清单*必*为按评分项得分降序、同分时先按所属 Dimension 权重降序再按 Indicator 权重降序消歧的确定性排序结果;当启用评分项数量少于 N 时输出全部启用项,否则输出前 N 项;同一输入*恒*产生同一输出顺序。清单中每项*必*提供其 Dimension、Indicator、得分与判定依据。 + +**Validates: Requirements 7.2, 7.3, 7.4, 7.5** + +### Property 31: 风险溢价区间合法且随分级单调 + +*对任意* Risk_Grade 与风险评分,风险溢价加价区间的下界*恒*不大于上界;且更高 Risk_Grade 对应的溢价*恒*不低于更低 Risk_Grade 对应的溢价。 + +**Validates: Requirements 8.1** + +### Property 32: 费用项非负且标注依据 + +*对任意*成本输入,垫资利息、保险费用、补偿准备金、坏账准备金各项金额*恒*非负,且每一项测算金额*必*标注其所依据的输入项与所采用的费率或参数来源。 + +**Validates: Requirements 8.2, 8.6** + +### Property 33: 风险调整后报价不低于基准且拆解一致 + +*对任意*成本输入,风险调整后报价*恒*不低于基准报价,且各项成本拆解之和*与*报价口径一致。 + +**Validates: Requirements 8.3** + +### Property 34: 缺失成本输入兜底为假设 + +*对任意*缺失的成本输入项,Cost_Engine *必*采用行业默认值并将该输入项的 Data_Provenance 标注为"智能体假设"。 + +**Validates: Requirements 8.4** + +### Property 35: 评分未完成拒绝费用测算 + +*对任意*风险评分尚未完成的状态,Cost_Engine *必*拒绝执行费用测算并返回评分未完成提示。 + +**Validates: Requirements 8.5** + +### Property 36: 报告章节完备 + +*对任意*风险评分、红线校验、费用测算与可接受性结论均已完成的评估,生成的报告*必*包含全部规定章节(项目概要与业务类型判定、风险总分与分级、风险热力图、各维度风险明细、Top 关键风险与红线校验结果、可接受性结论、应对方案、假设与信息缺口说明),且未命中任一红线时红线校验结果*必*明确标注为"无红线命中"。 + +**Validates: Requirements 10.1** + +### Property 37: 维度明细字段齐备 + +*对任意*报告,各维度风险明细中每个评分项*必*展示评分、判定依据、风险影响、Data_Provenance 与 Confidence。 + +**Validates: Requirements 10.3** + +### Property 38: 流程未完成拒绝报告 + +*对任意*评估流程尚未完成的状态,请求生成或导出报告*必*被拒绝并返回流程尚未完成提示。 + +**Validates: Requirements 10.4** + +### Property 39: 停用项保留但不计分 + +*对任意*被管理员停用的 Dimension 或 Indicator,其配置数据*必*保留,且*不*纳入评分计算。 + +**Validates: Requirements 11.1** + +### Property 40: 权重归一化保比例且同级和为 100% + +*对任意*非全零的同级权重向量(取值 0 至 100),保存时归一化后同级启用项权重之和*恒*等于 100%(保留两位小数),且任意两项归一化前后的比例*恒*保持不变。 + +**Validates: Requirements 11.2** + +### Property 41: 评分规则与红线配置校验 + +*对任意*自定义 Scoring_Rule,未覆盖 Risk_Level 1 至 5 全部级别者*必*被拒绝;*对任意* Redline 配置,缺少唯一标识、触发条件或一票否决后果,或标识重复者*必*被拒绝。 + +**Validates: Requirements 11.3, 11.4** + +### Property 42: 配置另存为模板可往返 + +*对任意*合法配置,将其另存为模板再加载实例化所得配置*必*与原配置等价(round-trip)。 + +**Validates: Requirements 11.5** + +### Property 43: 校验失败保留上次有效配置 + +*对任意*未通过校验的配置保存请求,存储中的配置*必*保持为上次有效配置不变,并返回指明失败项的校验错误。 + +**Validates: Requirements 11.6, 11.7** + +### Property 44: 同级权重全零拒绝归一化 + +*对任意*同级启用项权重之和为 0 的向量,Config_Center *必*拒绝保存并返回权重校验错误。 + +**Validates: Requirements 11.8** + +### Property 45: 非管理员配置修改一律拒绝且配置不变 + +*对任意*非 Administrator 角色(含 Assessor、未认证、未授权)发起的 Risk_Model 配置修改请求,System *必*拒绝该请求、保持当前配置不变并返回权限不足错误。 + +**Validates: Requirements 12.1, 12.3** + +### Property 46: 配置变更与拒绝均留痕审计 + +*对任意*成功提交的配置变更,审计日志*必*记录操作者身份、精确到秒的变更时间与发生变更的配置项标识;*对任意*被拒绝的配置修改请求,审计日志*必*记录操作者身份与精确到秒的请求时间。 + +**Validates: Requirements 12.4, 12.5** + +### Property 47: 角色化视图内容映射 + +*对任意*已完成评估:商务/销售视图*必*包含可接受性结论、接受条件清单与风险调整后报价;风控视图*必*包含评分项明细(含 Risk_Level、判定依据、Data_Provenance)、红线校验结果与信息缺口尽调事项;管理层视图*必*包含 Risk_Grade、风险热力图、Top N 关键风险与利润对风险对比。 + +**Validates: Requirements 13.1, 13.2, 13.3** + +### Property 48: 组合看板汇总与空集处理 + +*对任意* Assessment 集合,管理层组合看板*必*汇总集合中全部评估;当集合为空时*必*展示空组合看板并提示无可汇总的评估。 + +**Validates: Requirements 13.4, 13.6** + +### Property 49: 无角色拒绝展示 + +*对任意*未分配商务/销售、风控或管理层任一角色的用户,System *必*拒绝展示评估视图并提示需分配角色。 + +**Validates: Requirements 13.5** + +### Property 50: 行业分区内容完备性校验 + +*对任意*行业分区,合法分区*必*包含 Indicator、权重模板、Redline、典型案例、追问话术全部五类内容;*对任意*缺少其中任一类内容的新增分区请求,System *必*拒绝创建、返回指明缺失内容类别的校验错误,并保持已有分区不变。 + +**Validates: Requirements 14.1, 14.4** + +### Property 51: 外部数据点来源与置信标注 + +*对任意*成功获取的外部数据点,其 Data_Provenance *必*为"外部数据"且 Confidence 落在 [0, 1] 内。 + +**Validates: Requirements 15.2** + +### Property 52: 外部数据失败降级回退 + +*对任意*外部数据源连接失败、超过 10 秒未返回或返回错误响应的情形,受影响数据点*必*回退到用户输入并标注 Data_Provenance 为"用户输入";若回退后用户输入仍不完整,则*必*标注为"智能体假设"并继续执行评估、不中断流程。 + +**Validates: Requirements 15.3, 15.4** + +### Property 53: 合规判定覆盖与计量标注 + +*对任意* Region 为中国大陆(CN)的评估,合规判定*必*覆盖社保缴费基数、经济补偿 N 与 N+1、劳务派遣用工比例上限 10%、当地最低工资标准四项;且经济补偿金额、社保缴费金额与相关合规费用*必*依规则项计算并标注其所依据的规则项与输入项。 + +**Validates: Requirements 16.1, 16.2** + +### Property 54: 合规不满足项标注 + +*对任意*合规判定中存在不满足项(社保缴费基数低于法定下限、劳务派遣用工比例超过 10% 或约定薪酬低于当地最低工资标准)的评估,System *必*将该项标注为合规不通过并在报告中列出对应规则项与判定依据。 + +**Validates: Requirements 16.3** + +### Property 55: Region 记录与默认 + +*对任意*创建的 Assessment,*必*记录其所采用的 Region;当请求未指定 Region 时,*必*采用中国大陆(CN)并标注该 Region 为系统默认值。 + +**Validates: Requirements 16.4, 16.5** + +### Property 56: 无规则集地域拒绝合规处理 + +*对任意*当前无对应 Compliance_Rule_Set 的 Region,System *必*拒绝该 Region 的合规判定与费用测算、不生成合规结论,并返回该地域暂不支持的提示。 + +**Validates: Requirements 16.6** + +### Property 57: 评估持久化往返 + +*对任意*完成的 Assessment,持久化存储后再读取*必*得到等价记录,且其元数据*必*至少包含业务类型、行业、Region、Risk_Score、Risk_Grade、创建时间与评估者身份。 + +**Validates: Requirements 17.1** + +### Property 58: 复评保留原评估 + +*对任意*历史 Assessment,复评*必*基于其输入创建一个新 Assessment(新标识)且原 Assessment 保持不变;*对任意*引用不存在历史 Assessment 的复评请求,*必*被拒绝并返回该评估不存在提示。 + +**Validates: Requirements 17.3, 17.4** + +### Property 59: 跨项目对比的数量约束与内容 + +*对任意*被选中的 Assessment 集合:当数量不少于 2 时*必*返回每个被选中 Assessment 的 Risk_Grade、Risk_Score 与关键风险对比数据;当数量少于 2 时*必*拒绝对比并提示至少需选择 2 个评估。 + +**Validates: Requirements 17.5, 17.6** + +### Property 60: 检索结果满足过滤条件 + +*对任意*历史数据集与按业务类型、行业、Risk_Grade 或创建时间范围的检索条件,返回的全部 Assessment *均*满足该检索条件,且无匹配时返回空结果集。 + +**Validates: Requirements 17.7** + +### Property 61: 评分项可解释三要素非空 + +*对任意*评分项,*必*输出非空的判定依据(引用其所属 Dimension、Indicator、Scoring_Rule 及导致该 Risk_Level 的数据点取值)、非空的风险影响说明与非空的建议。 + +**Validates: Requirements 18.1, 18.2, 18.3** + +### Property 62: 假设项进入缺口说明并附尽调建议 + +*对任意* Data_Provenance 为"智能体假设"的评分项,报告的信息缺口说明中*必*列出该项,并为其输出关联对应 Indicator 的补充尽调建议。 + +**Validates: Requirements 18.5, 18.6** + +> 以下为针对 UI/前端需求(Req 19-25)经 prework 可测试性分析与冗余消解后补充的属性,编号续接。纯视觉/主观、键盘可达性、时延与可达位置等标准不强行属性化,改由组件测试、可视化回归、自动化可访问性检查(axe)与人工审查覆盖,详见 Testing Strategy。 + +### Property 63: 排版层级完备且文本取自层级 + +*对任意* UI 文本元素,其排版样式*必*解析到 Design_System 已定义的某个具名 Typography 层级;且已定义层级*恒*不少于 4 级,每级具有固定字号与固定行高。 + +**Validates: Requirements 19.2** + +### Property 64: Risk_Grade 配色令牌一致且唯一 + +*对任意* Risk_Grade,UI 在 Risk_Badge、风险热力图与 Top N 关键风险图等所有等级相关呈现中取得的 Color_Token *必*为同一稳定令牌名;且四级(低、中、高、极高)对应的 Color_Token *互不相同*。 + +**Validates: Requirements 19.3** + +### Property 65: 间距为 4 像素整数倍 + +*对任意* Design_System 间距标度取值与任意组件间距,其像素值*恒*为 4 的非负整数倍。 + +**Validates: Requirements 19.4** + +### Property 66: 主题配色令牌解析正确 + +*对任意* Color_Token 与任意 Theme(Light 或 Dark),`resolveColorToken` *必*返回该令牌在所选 Theme 下定义的取值;切换 Theme 后,页面所有引用该令牌处*必*一致地反映所选 Theme 的取值。 + +**Validates: Requirements 19.6** + +### Property 67: UI 状态转换保留已录入数据 + +*对任意*已录入数据的页面状态,执行切换 Theme、在对话式追问与表单录入之间切换、或因 Viewport 宽度跨越布局断点而变更布局等任一 UI 状态转换后,已录入数据*恒*保持不变(无丢失、无篡改)。 + +**Validates: Requirements 19.7, 21.2, 22.4** + +### Property 68: 图表图例与数据系列一致 + +*对任意*含两个及以上数据系列或类别的 Chart,其图例*必*存在,且图例标签集合*恒*与该 Chart 中对应数据元素的标签集合相等。 + +**Validates: Requirements 20.2** + +### Property 69: 图表文本标签齐备 + +*对任意* Chart,其每个坐标轴、数据点或分区*必*具有非空的文本标签。 + +**Validates: Requirements 20.3** + +### Property 70: 图表空态与加载态呈现 + +*对任意* Chart:当其对应数据为空时*必*呈现 Empty_State 并提示无可展示数据;当其对应数据正在请求或计算中时*必*呈现 Loading_State。 + +**Validates: Requirements 20.4, 20.5** + +### Property 71: 仪表盘同时呈现总分与分级 + +*对任意* Risk_Score,风险总分仪表盘*必*同时呈现该 Risk_Score 数值与其按分级规则对应的 Risk_Grade,且所呈现 Risk_Grade *恒*与分级函数输出一致。 + +**Validates: Requirements 20.6** + +### Property 72: 费用对比图三值并呈且差额一致 + +*对任意*基准报价与风险调整后报价对,费用对比图*必*同时呈现基准报价金额、风险调整后报价金额与二者差额,且所呈现差额*恒*等于风险调整后报价减基准报价。 + +**Validates: Requirements 20.7** + +### Property 73: Wizard 进度正确且单调 + +*对任意* Wizard 步骤序列,进度指示*必*正确呈现当前步骤序号、步骤总数与已完成步骤数(已完成数不超过总数);且*对任意*推进操作序列,已完成步骤数*恒*单调非减。 + +**Validates: Requirements 21.1** + +### Property 74: 缺口项醒目呈现并可定位 + +*对任意*存在信息缺口或待补充项的 Assessment,缺口面板*必*以区别于常规文本的醒目样式呈现*全部*该类项,且每一项*必*提供定位至其对应录入位置的入口。 + +**Validates: Requirements 21.3** + +### Property 75: Draft 往返保真 + +*对任意* Assessment 录入状态,将其保存为 Draft 后再打开该 Draft,所恢复的已录入数据*必*与保存前等价,且恢复后所处的 Wizard 步骤*必*等于保存时所处步骤。 + +**Validates: Requirements 21.5, 21.6** + +### Property 76: 断点布局映射确定性 + +*对任意* Viewport 宽度,`selectLayout` *必*返回唯一确定的布局:宽度不小于 1280 → 桌面布局;768 至 1279 → 紧凑布局;小于 768 → 移动布局;且移动布局的看板视图*必*保留 Risk_Score、Risk_Grade 与 Top N 关键风险清单的呈现。同一宽度*恒*映射到同一布局。 + +**Validates: Requirements 22.1, 22.2, 22.3** + +### Property 77: 文本对比度达标 + +*对任意* Theme 下任意正文文本与其背景的 Color_Token 对,二者计算所得相对对比度*恒*不低于 4.5:1;*对任意*大号文本与其背景的 Color_Token 对,对比度*恒*不低于 3:1。 + +**Validates: Requirements 23.3** + +### Property 78: 表单可访问标注与错误提示 + +*对任意*表单输入控件,*必*关联一个非空、可被辅助技术识别的文本标签;且*对任意*校验失败的输入控件,*必*为其呈现可被辅助技术识别的非空错误提示文本。 + +**Validates: Requirements 23.4, 23.5** + +### Property 79: 图表非纯颜色编码 + +*对任意* Chart 中的数据类别,*必*存在颜色之外的区分编码(文本标签或图案),使该类别在不依赖颜色的情况下仍可识别。 + +**Validates: Requirements 23.6** + +### Property 80: 操作状态反馈映射 + +*对任意*用户触发的操作及其生命周期,UI 呈现的状态*必*确定性映射为:处理中→Loading_State、成功→成功反馈、失败→可读错误信息;且失败时该错误信息*必*非空地说明失败原因并提供指向修正路径的操作入口。 + +**Validates: Requirements 24.1, 24.2, 24.3** + +### Property 81: 角色默认路由确定性 + +*对任意*登录用户角色,`defaultRoute` *必*返回唯一确定的默认视图:商务/销售→商务/销售视图、风控→风控视图、管理层→管理层看板;*对任意*未分配上述任一角色的用户,*必*返回需分配角色的提示视图且不呈现任何评估数据。 + +**Validates: Requirements 25.1, 25.2, 25.3, 25.5** + +## Error Handling + +错误处理遵循统一原则:**输入校验前置、错误可解释、失败不破坏既有状态、降级不中断主流程**。 + +| 错误场景 | 处理策略 | 关联需求 | +|----------|----------|----------| +| 项目描述信息不足(空/纯空白/<10 有效字符) | 拒绝分类,返回信息不足错误 | 1.6 | +| 无可用模板(无精确且无默认) | 终止评估,返回无可用模板提示 | 2.6 | +| 模板数据非法(缺必填/权重和≠100%) | 不实例化,终止,返回模板数据错误并指明问题项 | 2.4 | +| 模板继承环或层级>5 | 终止,返回继承错误 | 2.7 | +| 追问无效回答 | 保留缺口标记,返回需补充信息提示 | 3.4 | +| 追问轮次耗尽仍缺失 | 降级:行业默认值 + 标注"智能体假设",继续流程 | 3.6 | +| 无任何启用指标/维度 | 终止评分,返回评分数据不足错误 | 4.5 | +| 红线数据缺失/为假设 | 标"待核实",不计命中,报告说明原因 | 6.5 | +| 评分未完成请求费用测算 | 拒绝,返回评分未完成提示 | 8.5 | +| 流程未完成请求生成/导出报告 | 拒绝,返回流程未完成提示 | 10.4 | +| 报告导出失败 | 中止导出,保留已生成报告不变,返回导出失败错误 | 10.5 | +| 配置校验失败 | 拒绝保存,保留上次有效配置,返回失败项 | 11.7 | +| 同级权重和为 0 | 拒绝保存,返回权重校验错误 | 11.8 | +| 非授权角色修改配置 | 拒绝,配置不变,返回权限不足错误,写审计 | 12.1, 12.3, 12.5 | +| 用户无角色 | 拒绝展示视图,提示分配角色 | 13.5 | +| 行业分区内容缺类 | 拒绝创建,指明缺失类别,已有分区不变 | 14.4 | +| 外部数据失败/超时/错误 | 降级回退用户输入→(仍缺)智能体假设,不中断 | 15.3, 15.4 | +| Region 无对应规则集 | 拒绝合规判定/费用测算,提示地域暂不支持 | 16.6 | +| 持久化失败 | 保留会话数据,返回存储失败错误 | 17.2 | +| 复评引用不存在评估 | 拒绝,返回评估不存在提示 | 17.4 | +| 对比选中<2 个评估 | 拒绝,提示至少需 2 个 | 17.6 | +| 存在未保存修改时离开评估流程 | 弹出确认提示,未确认则不离开,保留当前数据 | 21.7 | +| 图表数据为空 | 呈现 Empty_State 并提示无可展示数据,不抛错 | 20.4 | +| 图表数据加载中 | 呈现 Loading_State,不阻塞页面其余部分 | 20.5 | +| 用户操作失败 | 呈现可读错误信息(说明原因)+ 指向修正路径的操作入口 | 24.3 | +| 报告导出执行中 | 呈现进度反馈,30 秒内给出完成或失败终态 | 24.4 | +| 请求的 Region/数据缺失致图表无数据 | 图表降级为 Empty_State,主视图其余部分照常呈现 | 20.4 | +| 登录用户无角色 | 呈现需分配角色提示视图,不呈现评估数据 | 25.5 | + +错误响应统一携带:错误码、面向用户的可读消息、关联的需求/配置项标识(便于定位)。降级类错误(追问耗尽、外部数据失败)不向用户报错而是以 Data_Provenance 标注体现,保证主流程不中断(Req 3.6、15.4)。 + +## Testing Strategy + +采用**单元测试 + 属性化测试 + 集成测试**三层互补策略。属性化测试覆盖纯算法与不变式逻辑(评分、归一化、分级、红线、排序、权重归一化、决策表、来源标注等),单元测试覆盖具体示例与边界,集成测试覆盖外部 IO、导出、持久化失败与架构解耦。 + +### 属性化测试(Property-Based Testing) + +适用范围:本特性的核心评分/分级/红线/排序/费用/配置/合规/持久化逻辑均为确定性纯函数或具备清晰输入输出的状态转移,高度适合 PBT。 + +约束: + +- 选用目标语言的成熟 PBT 库(如 TypeScript 的 fast-check、Python 的 Hypothesis、JVM 的 jqwik),**不自行实现** PBT 框架。 +- 每个属性测试*至少运行 100 次迭代*。 +- 每个属性测试以注释标注其对应设计属性,标签格式: + `Feature: outsourcing-risk-assessment, Property {number}: {property_text}` +- 每条 Correctness Property 由*单个*属性测试实现,共 81 条属性(含 Req 19-25 的 UI 可属性化标准 19 条:Property 63-81)。 + +生成器要点: + +- 随机生成合法/非法风险模型(维度、指标、权重向量、启停状态、评分规则、红线)。 +- 边界覆盖:纯空白与 <10 字符描述、权重全零、Risk_Level 同取 1/5、Risk_Score 区间边界 0/25/50/75/100、继承深度 5/6 与含环图、N=1/50 与启用项少于 N、空 Assessment 集合、<2 项对比集合。 +- 三态 Data_Provenance 与 Confidence 边界(0、1)覆盖。 +- UI 属性生成器:随机 Theme(Light/Dark)、随机 Viewport 宽度(覆盖 <768 / 768-1279 / ≥1280 及断点边界 767/768/1279/1280)、随机已录入数据快照、随机图表数据(空集/单系列/多系列/多类别)、随机 Wizard 步骤序列与推进操作、随机 Color_Token 文本/背景对、随机角色(含无角色)。 + +### 前端 UI 测试(组件测试 + 可视化回归 + 自动化可访问性) + +对纯视觉/主观、键盘可达性、可见位置与时延等不适合属性化的 UI 标准,采用以下互补手段覆盖: + +- **组件测试**:验证 Design_System 同类组件在各页面外观/行为一致(19.1)、单一图标集引用(19.5)、一键操作入口存在(21.4)、默认视图首屏含角色匹配功能入口(25.4)、未保存离开确认(21.7)、规定全套图表组件可渲染(20.1)。 +- **可视化回归测试**:对关键页面与图表(含 Light/Dark 双主题、各断点布局)建立基线快照,验证排版/配色/间距视觉一致与图表标签不被遮挡(19.1、20.3)。 +- **自动化可访问性检查(axe)**:对全部页面运行 axe 规则集,验证键盘可聚焦与可见焦点指示(23.1、23.2)、标签关联与 ARIA 正确性(23.4、23.5),作为属性化对比度校验(Property 77)之外的补充。 +- **性能/时延检查**:主题切换 1 秒内生效(19.7)、报告导出 30 秒内给出终态(24.4)以性能与集成测试度量,不纳入属性化。 +- **人工审查**:"专业一致的视觉体验"等主观目标由设计评审与可用性走查保证。 + +### 单元测试(示例与边界) + +- 行业语义识别(1.2):代表性描述验证可识别行业与"未识别"分支。 +- Assessor 可执行评估(12.2):示例验证允许路径。 +- 具体合规算例:给定社保基数、派遣比例、最低工资的代表性数值,验证 N/N+1 与社保金额计算正确(配合属性 53/54)。 +- 各错误分支的代表性触发与消息内容。 +- UI 示例:图表全套组件渲染(20.1)、一键入口存在(21.4)、单一图标集引用(19.5)、默认视图首屏角色入口可见(25.4)、未保存离开确认弹窗(21.7)的代表性用例。 + +### 集成测试(外部依赖与架构解耦,1-3 例) + +- 报告导出(10.2、10.5):验证导出文件自包含且含全部章节、耗时 < 30 秒;注入导出失败验证报告内容不变。 +- 外部数据适配(15.1、15.5):mock 数据源验证成功取数路径;注册新适配器验证无需改 Scoring_Engine 源码即可使用。 +- 知识库行业分区扩展(14.2):运行时新增分区后验证其 Template 可被加载,无需重编译。 +- 持久化失败(17.2):注入存储失败,验证会话数据保留并返回错误。 + +### 追溯性 + +每条属性、单元用例与集成用例均标注其验证的需求编号,确保 25 条需求的可测试验收标准全部被覆盖;非可测试标准(如 UI 主观体验、纯架构组织约束、键盘可达性、时延与可见位置)以设计审查、可视化回归与自动化可访问性检查保证。 diff --git a/.kiro/specs/outsourcing-risk-assessment/requirements.md b/.kiro/specs/outsourcing-risk-assessment/requirements.md new file mode 100644 index 0000000..ac73083 --- /dev/null +++ b/.kiro/specs/outsourcing-risk-assessment/requirements.md @@ -0,0 +1,378 @@ +# Requirements Document + +## Introduction + +本功能构建一个**外包项目风险评估 AI 智能体**(以下统称 System)。用户输入外包项目描述后,System 基于结构化、可配置的风险指标体系,通过多轮对话式追问补全关键信息,对项目进行多维度加权评分与分级,校验红线,输出可量化的费用/定价测算,并给出"风险是否可接受 + 如何接受 + 如何应对(管理措施与费用措施)"的结论与建议,最终生成可导出的风险评估报告并持久化存档。 + +System 覆盖五类外包业务:岗位外包、劳务派遣、业务/服务外包、BPO、项目制外包;面向商务/销售、风控、管理层三类角色提供差异化交互与视图;首版实现中国大陆劳动法合规体系,并在架构上以地域参数预留跨境扩展。 + +本文档定义可验收的功能需求,遵循 EARS 模式与 INCOSE 质量规则。 + +## Glossary + +- **System**:外包项目风险评估智能体系统整体。 +- **Classifier**:业务类型与行业识别组件,负责从项目描述中判定外包业务类型与所属行业。 +- **Question_Engine**:自适应追问引擎,根据信息缺口动态生成补全问题。 +- **Config_Center**:模型配置中心,供管理员维护维度/指标/权重/模板/红线,并执行配置校验。 +- **Scoring_Engine**:评分引擎,读取配置对项目进行加权评分、归一化、分级、红线校验。 +- **Cost_Engine**:费用测算引擎,计算风险溢价、垫资利息、保险费用、准备金及风险调整后报价。 +- **Strategy_Engine**:应对策略推荐引擎,生成管理措施、费用措施与接受条件清单。 +- **External_Data_Adapter**:外部数据适配层,可插拔接入企业资信/征信/涉诉/失信/工商等外部数据。 +- **Report_Generator**:报告生成与导出组件。 +- **Knowledge_Base**:分行业知识库,按行业分区存储指标、模板、权重、红线、案例与追问话术。 +- **Compliance_Rule_Set**:地域化合规规则集(社保基数、经济补偿 N/N+1、派遣比例上限、最低工资等)。 +- **Risk_Model**:风险模型,三层结构 模型→维度(Dimension)→指标(Indicator)→评分项(Scoring_Rule)。 +- **Dimension**:风险维度,如客户风险、用工与人力风险等,可增删、启停、调权。 +- **Indicator**:维度下的风险指标,可增删、启停、调权,含评分标准、证据要求、追问话术。 +- **Scoring_Rule**:指标的评分项,定义 1-5 级风险等级的含义与判定标准。 +- **Redline**:红线规则,命中即触发一票否决。 +- **Template**:风险模型模板,按业务类型与行业组织,支持自定义与继承。 +- **Risk_Level**:单项风险等级,取值 1 至 5 的整数。 +- **Risk_Score**:归一化后的风险总分,取值范围 0 至 100。 +- **Risk_Grade**:风险分级,取值为 低 / 中 / 高 / 极高 四级之一。 +- **Region**:地域参数,标识适用的合规规则集,首版取值为中国大陆(CN)。 +- **Assessment**:一次完整的项目风险评估记录,含输入、评分、报告与元数据。 +- **Administrator**:管理员角色,唯一可修改 Risk_Model 配置的角色。 +- **Assessor**:评估者角色,包含商务/销售、风控、管理层,可使用但不可修改 Risk_Model。 +- **Data_Provenance**:数据来源标注,取值为 用户输入 / 外部数据 / 智能体假设 之一。 +- **Confidence**:置信度,标注某一数据点可信程度的量化指标。 +- **UI**:System 的 Web 用户界面整体,承载评估流程交互、数据可视化与结果呈现。 +- **Design_System**:统一设计系统,定义可复用组件库、排版层级、配色规范、间距标度与图标集,供 UI 全局一致引用。 +- **Color_Token**:配色令牌,Design_System 中具名且取值稳定的颜色变量,含风险等级语义化配色(低/中/高/极高)。 +- **Theme**:界面主题,取值为 明亮(Light)或 暗黑(Dark)之一,决定 UI 的整体配色方案。 +- **Viewport**:浏览器视口,以 CSS 像素表示的可视区域宽度,用于响应式适配判定。 +- **Chart**:UI 中的数据可视化图表组件,含风险热力图、风险总分仪表盘、关键风险图、费用对比图与组合对比图。 +- **Risk_Badge**:风险分级徽章,以语义化配色与文字标签同时呈现 Risk_Grade 的可视化元素。 +- **Wizard**:向导式分步评估流程组件,将评估过程拆分为带进度指示的有序步骤。 +- **Draft**:评估草稿,尚未完成的 Assessment 的可保存中间状态,用于断点续评。 +- **WCAG**:Web 内容无障碍指南(Web Content Accessibility Guidelines)2.1 版本 AA 级,作为 UI 可访问性达标基准。 +- **Loading_State**:加载态,UI 在数据请求或处理进行中向用户呈现的等待反馈。 +- **Empty_State**:空数据态,UI 在无可呈现数据时向用户呈现的占位提示。 + +## Requirements + +### Requirement 1: 业务类型与行业识别 + +**User Story:** 作为评估者,我希望 System 从项目描述中识别外包业务类型与所属行业,以便加载匹配的风险模型模板。 + +#### Acceptance Criteria + +1. WHEN 评估者提交有效项目描述文本, THE Classifier SHALL 将该项目判定为下列业务类型中 Confidence 最高的唯一一项:岗位外包、劳务派遣、业务/服务外包、BPO、项目制外包。 +2. WHEN 评估者提交有效项目描述文本, THE Classifier SHALL 判定该项目所属行业,并在行业无法判定时输出取值为"未识别"的行业标记。 +3. THE Classifier SHALL 为业务类型判定结果与行业判定结果分别输出取值范围 0 至 1、保留两位小数的 Confidence。 +4. IF 业务类型判定的 Confidence 低于 0.6, THEN THE System SHALL 向评估者展示按 Confidence 由高到低排序、至多 3 项的候选业务类型列表并请求评估者确认。 +5. IF 行业判定的 Confidence 低于 0.6 且行业标记不为"未识别", THEN THE System SHALL 向评估者展示按 Confidence 由高到低排序、至多 3 项的候选行业列表并请求评估者确认。 +6. IF 评估者提交的项目描述文本为空、仅含空白字符或有效字符数少于 10, THEN THE System SHALL 拒绝执行业务类型与行业判定并返回提示项目描述信息不足的错误。 +7. WHEN 评估者修改 System 判定的业务类型或行业, THE System SHALL 采用评估者确认的业务类型与行业作为后续加载模板的依据。 + +### Requirement 2: 模板加载与风险模型实例化 + +**User Story:** 作为评估者,我希望 System 根据业务类型与行业自动加载对应的风险模型模板,以便在标准化基线上开展评估。 + +#### Acceptance Criteria + +1. WHEN 评估者确认的业务类型与行业确定, THE System SHALL 从 Knowledge_Base 加载与该业务类型和行业组合精确匹配的 Template,并将其实例化为本次 Assessment 的 Risk_Model。 +2. IF 不存在与业务类型和行业组合精确匹配的 Template, THEN THE System SHALL 加载该业务类型的默认 Template 实例化为本次 Assessment 的 Risk_Model,并在本次 Assessment 中输出取值为"未匹配行业专用模板"的标记。 +3. WHEN 加载 Template, THE System SHALL 在实例化的 Risk_Model 中保留该 Template 定义的全部 Dimension、Indicator、权重、Scoring_Rule、Redline、追问话术,以及各 Dimension 与 Indicator 的启用/停用状态。 +4. IF 待加载的 Template 缺少 Dimension、Indicator、权重或 Scoring_Rule 中任一必填组成项,或其同级 Dimension 权重之和或同级 Indicator 权重之和不等于 100%, THEN THE System SHALL 不实例化 Risk_Model、终止本次 Assessment,并向评估者返回指明模板数据错误的提示。 +5. WHERE Template 通过模板继承派生, THE System SHALL 应用父模板的全部组成项配置,并以子模板中存在差异的组成项逐项覆盖父模板的对应组成项。 +6. IF 既不存在与业务类型和行业组合精确匹配的 Template、也不存在该业务类型的默认 Template, THEN THE System SHALL 不实例化 Risk_Model、终止本次 Assessment,并向评估者返回指明无可用模板的提示。 +7. IF 模板继承链中存在循环引用或继承层级超过 5 层, THEN THE System SHALL 不实例化 Risk_Model、终止本次 Assessment,并向评估者返回指明模板继承错误的提示。 + +### Requirement 3: 自适应信息追问 + +**User Story:** 作为评估者,我希望 System 针对信息缺口动态追问,以便补全评分所需关键信息。 + +#### Acceptance Criteria + +1. WHEN Risk_Model 实例化完成, THE Question_Engine SHALL 将每个处于启用状态且已知信息不足以依据其 Scoring_Rule 判定 Risk_Level 的 Indicator 标记为信息缺口。 +2. THE Question_Engine SHALL 仅为标记为信息缺口的 Indicator 生成追问问题,并按 Indicator 权重由高到低排序;当 Indicator 权重相同时,按其所属 Dimension 权重由高到低排序消歧。 +3. WHEN 评估者回答一个追问问题, THE Question_Engine SHALL 更新对应 Indicator 的已知信息并重新识别剩余信息缺口。 +4. IF 评估者对追问问题给出空回答或不满足证据要求的回答, THEN THE Question_Engine SHALL 保持该 Indicator 的信息缺口标记并返回需补充信息的提示。 +5. THE Question_Engine SHALL 将单个 Indicator 的追问轮次限制在最大追问轮次以内,最大追问轮次为可配置正整数且默认值为 3。 +6. WHERE 某 Indicator 的信息在追问轮次达到上限后仍缺失, THE System SHALL 对该 Indicator 采用行业默认值并将其 Data_Provenance 标注为"智能体假设"。 +7. WHERE 某 Indicator 的 Data_Provenance 已被标注为"智能体假设", THE System SHALL 在该标注被应用后永久保留该标注,即使评估者后续补充了对应信息。 +8. THE Question_Engine SHALL 为每个追问问题关联其对应的 Dimension 与 Indicator。 + +### Requirement 4: 风险评分与归一化 + +**User Story:** 作为风控,我希望 System 基于加权模型计算风险得分,以便获得可量化的风险结果。 + +#### Acceptance Criteria + +1. THE Scoring_Engine SHALL 为每个启用的 Indicator 计算评分项得分,评分项得分等于该 Indicator 的 Risk_Level(取值 1 至 5)乘以该 Indicator 的权重。 +2. THE Scoring_Engine SHALL 计算每个启用的 Dimension 的得分为其下各启用 Indicator 评分项得分的加权求和。 +3. THE Scoring_Engine SHALL 将各启用 Dimension 的得分加权汇总后线性映射归一化为 Risk_Score,使全部纳入计算的 Indicator 的 Risk_Level 同取最小值 1 时 Risk_Score 映射为 0、同取最大值 5 时 Risk_Score 映射为 100,并将 Risk_Score 四舍五入取整为 0 至 100 的整数。 +4. THE Scoring_Engine SHALL 在汇总计算中仅纳入处于启用状态的 Dimension 与 Indicator。 +5. IF 本次 Assessment 不存在任何启用的 Indicator 或 Dimension, THEN THE Scoring_Engine SHALL 终止评分、不产生 Risk_Score 并返回评分数据不足的错误。 +6. THE Scoring_Engine SHALL 为每个评分项记录其 Data_Provenance(取值为 用户输入 / 外部数据 / 智能体假设 之一)与取值范围 0 至 1 的 Confidence。 + +### Requirement 5: 风险分级 + +**User Story:** 作为评估者,我希望 System 将风险总分映射为风险等级,以便快速判断风险高低。 + +#### Acceptance Criteria + +1. WHEN Scoring_Engine 完成 Risk_Score 归一化计算, THE Scoring_Engine SHALL 在 0 ≤ Risk_Score ≤ 25 时将 Risk_Grade 判定为"低"。 +2. WHEN Scoring_Engine 完成 Risk_Score 归一化计算, THE Scoring_Engine SHALL 在 25 < Risk_Score ≤ 50 时将 Risk_Grade 判定为"中"。 +3. WHEN Scoring_Engine 完成 Risk_Score 归一化计算, THE Scoring_Engine SHALL 在 50 < Risk_Score ≤ 75 时将 Risk_Grade 判定为"高"。 +4. WHEN Scoring_Engine 完成 Risk_Score 归一化计算, THE Scoring_Engine SHALL 在 75 < Risk_Score ≤ 100 时将 Risk_Grade 判定为"极高"。 +5. THE Scoring_Engine SHALL 为每次 Assessment 输出且仅输出一个 Risk_Grade。 + +### Requirement 6: 红线一票否决 + +**User Story:** 作为风控,我希望命中红线的项目被强制判定为不可接受,以便规避不可承受的风险。 + +#### Acceptance Criteria + +1. WHEN Scoring_Engine 完成评分, THE Scoring_Engine SHALL 逐条校验本次 Assessment 是否满足每一个启用的 Redline 的触发条件,并将满足触发条件的 Redline 标记为命中。 +2. IF 本次 Assessment 命中任一启用的 Redline, THEN THE System SHALL 将可接受性结论判定为"不可接受",且该判定优先于基于 Risk_Grade 得出的可接受性判定。 +3. IF 本次 Assessment 命中任一启用的 Redline, THEN THE System SHALL 在报告中列出每个被命中的 Redline 及其被触发的条件与对应的判定依据数据。 +4. THE Scoring_Engine SHALL 独立于 Risk_Score 与 Risk_Grade 的高低执行全部启用 Redline 的校验。 +5. IF 某个启用的 Redline 的触发条件所需数据缺失,或其 Data_Provenance 为"智能体假设"以致无法确定是否命中, THEN THE System SHALL 将该 Redline 标记为"待核实"、在报告中列出该 Redline 并说明无法判定的原因,且不将其计为命中。 +6. WHEN 本次 Assessment 未命中任何启用的 Redline, THE System SHALL 不因红线校验而改变基于 Risk_Grade 得出的可接受性结论。 + +### Requirement 7: 风险热力图与关键风险清单 + +**User Story:** 作为管理层,我希望看到风险的可视化分布与关键风险排序,以便聚焦最重要的风险点。 + +#### Acceptance Criteria + +1. THE System SHALL 输出以 Dimension 为行、以 Indicator 为列、以 Risk_Level(取值 1 至 5)度量严重度的风险热力图数据。 +2. THE System SHALL 输出按评分项得分由高到低排序的 Top N 关键风险清单,N 为可配置正整数,取值范围 1 至 50,默认值为 10。 +3. WHEN 评分项得分相同, THE System SHALL 先按所属 Dimension 权重由高到低、再按 Indicator 权重由高到低确定其在关键风险清单中的排序。 +4. WHERE 启用的评分项数量少于 N, THE System SHALL 在关键风险清单中输出全部启用评分项。 +5. THE System SHALL 为关键风险清单中的每个风险项提供其所属 Dimension、Indicator、得分与判定依据。 + +### Requirement 8: 费用与定价量化测算 + +**User Story:** 作为商务/销售,我希望 System 输出具体金额的风险溢价与成本测算,以便基于风险定价。 + +#### Acceptance Criteria + +1. WHEN 风险评分完成且成本输入可用, THE Cost_Engine SHALL 依据 Risk_Grade 与风险评分计算取值为具体金额区间或百分比区间的风险溢价加价区间。 +2. THE Cost_Engine SHALL 计算垫资利息、保险费用、补偿准备金与坏账准备金各项的具体金额,并为每项标注计算所依据的输入项与所采用的费率或参数来源。 +3. THE Cost_Engine SHALL 输出基准报价与风险调整后报价的对比及各项成本拆解。 +4. WHERE 测算所需的成本输入项缺失, THE Cost_Engine SHALL 采用行业默认值并将该输入项的 Data_Provenance 标注为"智能体假设"。 +5. IF 风险评分尚未完成, THEN THE Cost_Engine SHALL 拒绝执行费用测算并返回评分未完成的提示。 +6. THE Cost_Engine SHALL 为每一项测算金额标注其计算所依据的输入项。 + +### Requirement 9: 可接受性结论与应对策略 + +**User Story:** 作为评估者,我希望 System 给出明确的可接受性结论与应对方案,以便据此决策与执行。 + +#### Acceptance Criteria + +1. WHEN Scoring_Engine 完成评分与红线校验, THE Strategy_Engine SHALL 输出取值为 可接受 / 有条件接受 / 不可接受 之一的可接受性结论。 +2. WHEN 可接受性结论为"有条件接受", THE Strategy_Engine SHALL 输出接受条件清单,且清单中每个条件关联至少一个关键风险清单中的风险项。 +3. WHEN 可接受性结论为"可接受"或"有条件接受", THE Strategy_Engine SHALL 输出管理层面应对措施,且为合同条款、用工合规整改、退场预案、过程监控四类中的每一类各输出至少一项措施。 +4. WHEN 可接受性结论为"可接受"或"有条件接受", THE Strategy_Engine SHALL 输出费用层面应对措施,且为风险溢价定价、预付/保证金、保险转移、账期成本、准备金计提五类中的每一类各输出至少一项措施。 +5. WHEN 可接受性结论为"有条件接受", THE Strategy_Engine SHALL 为接受条件清单中每个条件输出取值为具体金额或金额区间的成本影响测算。 +6. WHERE 本次 Assessment 未命中任何启用的 Redline 且 Risk_Grade 为"低"或"中", THE Strategy_Engine SHALL 将可接受性结论判定为"可接受"。 +7. WHERE 本次 Assessment 未命中任何启用的 Redline 且 Risk_Grade 为"高", THE Strategy_Engine SHALL 将可接受性结论判定为"有条件接受"。 +8. IF 本次 Assessment 命中任一启用的 Redline 或 Risk_Grade 为"极高", THEN THE Strategy_Engine SHALL 将可接受性结论判定为"不可接受"。 + +### Requirement 10: 评估报告生成与导出 + +**User Story:** 作为评估者,我希望 System 生成结构化报告并支持导出,以便归档与共享。 + +#### Acceptance Criteria + +1. WHEN 本次 Assessment 的风险评分、红线校验、费用测算与可接受性结论均已生成完成, THE Report_Generator SHALL 生成包含以下内容的报告:项目概要与业务类型判定、风险总分与分级、风险热力图、各维度风险明细、Top 关键风险清单与红线校验结果、可接受性结论、应对方案、假设与信息缺口说明;其中红线校验结果在未命中任一 Redline 时亦明确标注为"无红线命中"。 +2. WHEN 评估者在报告已生成后请求导出报告, THE Report_Generator SHALL 在评估者请求后 30 秒内将报告导出为包含第 1 条全部报告内容的完整且自包含的可下载文件。 +3. THE Report_Generator SHALL 在各维度风险明细中为每个评分项展示评分、判定依据、风险影响、Data_Provenance 与 Confidence。 +4. IF 评估者在第 1 条所述评估流程完成前请求生成或导出报告, THEN THE Report_Generator SHALL 拒绝该请求并返回指示评估流程尚未完成的提示。 +5. IF 报告导出过程失败, THEN THE Report_Generator SHALL 中止本次导出、保留已生成的报告内容不变,并向评估者返回指示导出失败的错误。 + +### Requirement 11: 管理员配置风险模型 + +**User Story:** 作为管理员,我希望统一配置维度、指标、权重、模板与红线,以便维护风险模型而无需改动代码。 + +#### Acceptance Criteria + +1. WHERE 操作者角色为 Administrator, THE Config_Center SHALL 允许对 Dimension 与 Indicator 执行新增、删除、启用与停用操作,且对停用项保留其配置数据但不纳入评分。 +2. WHERE 操作者角色为 Administrator, THE Config_Center SHALL 允许将 Dimension 权重与 Indicator 权重调整为取值 0 至 100 的值,并在保存时自动按比例归一化使同级启用项权重之和等于 100% 且保留两位小数。 +3. WHERE 操作者角色为 Administrator, THE Config_Center SHALL 允许自定义 Indicator 的 Scoring_Rule、证据要求与追问话术,且自定义的 Scoring_Rule 须覆盖 Risk_Level 1 至 5 各级的判定标准。 +4. WHERE 操作者角色为 Administrator, THE Config_Center SHALL 允许配置 Redline 规则,且每条 Redline 含唯一标识、触发条件与一票否决后果。 +5. WHERE 操作者角色为 Administrator, THE Config_Center SHALL 允许将当前配置另存为自定义 Template 并支持基于已有 Template 派生新 Template。 +6. WHEN Config_Center 保存配置, THE Config_Center SHALL 校验权重合法性、必填项完整性与红线唯一性。 +7. IF 配置校验失败, THEN THE Config_Center SHALL 拒绝保存、保留上次有效配置不变并返回指明失败项的校验错误。 +8. IF 某同级启用项权重之和为 0 以致无法归一化, THEN THE Config_Center SHALL 拒绝保存并返回权重校验错误。 + +### Requirement 12: 配置访问权限控制 + +**User Story:** 作为系统所有者,我希望仅管理员能修改风险模型,以便保证评估模型的一致性与可信度。 + +#### Acceptance Criteria + +1. IF 操作者角色为 Assessor 且请求修改 Risk_Model 配置, THEN THE System SHALL 拒绝该修改请求、保持当前 Risk_Model 配置不变,并向操作者返回指示权限不足的错误。 +2. WHERE 操作者角色为 Assessor, THE System SHALL 允许使用当前 Risk_Model 执行评估。 +3. IF 请求修改 Risk_Model 配置的操作者角色既非 Administrator 也非 Assessor(含未认证或未授权操作者), THEN THE System SHALL 拒绝该修改请求、保持当前 Risk_Model 配置不变,并返回指示权限不足的错误。 +4. WHEN System 成功提交一次 Risk_Model 配置变更, THE System SHALL 记录该变更的操作者身份、变更时间(精确到秒)与发生变更的配置项标识。 +5. WHEN System 拒绝一次 Risk_Model 配置修改请求, THE System SHALL 记录该被拒绝请求的操作者身份与请求时间(精确到秒)。 + +### Requirement 13: 角色化交互与视图 + +**User Story:** 作为不同角色的用户,我希望获得与我职责匹配的交互深度与视图,以便高效完成本职工作。 + +#### Acceptance Criteria + +1. WHEN 商务/销售角色用户打开一个已完成的 Assessment, THE System SHALL 展示包含可接受性结论、接受条件清单与风险调整后报价的视图。 +2. WHEN 风控角色用户打开一个已完成的 Assessment, THE System SHALL 展示包含评分项明细(含 Risk_Level、判定依据、Data_Provenance)、红线校验结果与信息缺口尽调事项的视图。 +3. WHEN 管理层角色用户打开一个已完成的 Assessment, THE System SHALL 展示包含 Risk_Grade、风险热力图、Top N 关键风险与利润对风险对比的高层看板视图。 +4. WHEN 管理层角色用户请求组合看板, THE System SHALL 展示跨项目汇总的组合看板视图。 +5. IF 用户未分配商务/销售、风控或管理层任一角色, THEN THE System SHALL 拒绝展示评估视图并提示需分配角色。 +6. WHERE 管理层请求的组合看板对应的 Assessment 集合为空, THE System SHALL 展示空组合看板并提示无可汇总的评估。 + +### Requirement 14: 分行业知识库管理 + +**User Story:** 作为管理员,我希望按行业分区管理知识库,以便针对不同行业提供专属模型与话术。 + +#### Acceptance Criteria + +1. THE Knowledge_Base SHALL 按行业标识分区存储内容,且每个行业分区包含该行业的 Indicator、权重模板、Redline、典型案例与追问话术全部五类内容。 +2. WHERE 操作者角色为 Administrator, WHEN 新增一个行业分区, THE System SHALL 在不修改 Scoring_Engine 源代码的前提下使该行业分区的 Template 可被 System 加载。 +3. WHEN 业务类型与行业确定, THE System SHALL 依据业务类型与行业的组合从 Knowledge_Base 选择对应行业分区中匹配的 Template。 +4. IF 新增的行业分区缺少 Indicator、权重模板、Redline、典型案例或追问话术中的任一类内容, THEN THE System SHALL 拒绝创建该行业分区、返回指明缺失内容类别的校验错误,并保持 Knowledge_Base 中已有分区不变。 +5. IF Knowledge_Base 中不存在与已确定行业匹配的行业分区, THEN THE System SHALL 选择默认行业分区的 Template 并标注未匹配到行业专用分区。 + +### Requirement 15: 外部数据接入与来源标注 + +**User Story:** 作为风控,我希望可插拔接入外部可信数据并标注来源,以便提升客户风险评估的客观性。 + +#### Acceptance Criteria + +1. WHERE 外部数据源已配置, WHEN External_Data_Adapter 在单次请求 10 秒内收到成功响应, THE External_Data_Adapter SHALL 获取企业资信、征信、涉诉、失信或工商数据用于客户风险相关 Indicator 取值。 +2. THE External_Data_Adapter SHALL 为每个数据点标注取值为"外部数据"的 Data_Provenance 与取值范围 0 至 1 的 Confidence。 +3. IF 外部数据源连接失败、超过 10 秒未返回或返回错误响应, THEN THE External_Data_Adapter SHALL 回退到用户输入并将受影响数据点的 Data_Provenance 标注为"用户输入"。 +4. IF 外部数据回退后用户输入仍不完整, THEN THE System SHALL 将缺失数据点的 Data_Provenance 标注为"智能体假设"并继续执行评估、不中断流程。 +5. THE System SHALL 在不修改 Scoring_Engine 源代码的前提下支持新增外部数据源适配器。 + +### Requirement 16: 地域化合规规则 + +**User Story:** 作为评估者,我希望 System 依据中国大陆劳动法规则进行合规相关测算,并在架构上预留跨境扩展,以便当前合规且未来可扩展。 + +#### Acceptance Criteria + +1. WHERE Region 取值为中国大陆(CN), THE System SHALL 加载并应用中国大陆 Compliance_Rule_Set 对本次 Assessment 执行合规判定,判定项覆盖社保缴费基数、经济补偿 N 与 N+1、劳务派遣用工比例上限 10% 与当地最低工资标准。 +2. WHERE Region 取值为中国大陆(CN), THE System SHALL 依据中国大陆 Compliance_Rule_Set 测算经济补偿(N 与 N+1)金额、社保缴费金额与相关合规费用,并为每项金额标注其所依据的规则项与输入项。 +3. IF 本次 Assessment 的合规判定中存在任一不满足项(社保缴费基数低于法定下限、劳务派遣用工比例超过 10% 或约定薪酬低于当地最低工资标准), THEN THE System SHALL 将该项标注为合规不通过,并在报告中列出对应规则项与判定依据。 +4. WHEN 创建一次 Assessment, THE System SHALL 记录该 Assessment 所采用的 Region。 +5. IF 创建 Assessment 的请求未指定 Region, THEN THE System SHALL 采用中国大陆(CN)作为该 Assessment 的默认 Region 并标注该 Region 为系统默认值。 +6. IF 请求的 Region 当前无对应 Compliance_Rule_Set, THEN THE System SHALL 拒绝该 Region 的合规判定与费用测算、不为该 Assessment 生成合规结论,并向评估者返回提示该地域暂不支持的消息。 + +### Requirement 17: 评估持久化与历史管理 + +**User Story:** 作为评估者,我希望评估结果被持久化并可检索复用,以便查询、复评与跨项目对比。 + +#### Acceptance Criteria + +1. WHEN 一次 Assessment 完成, THE System SHALL 持久化存储该 Assessment 的输入、评分结果、报告与元数据,且元数据至少包含业务类型、行业、Region、Risk_Score、Risk_Grade、创建时间与评估者身份。 +2. IF 持久化存储失败, THEN THE System SHALL 保留本次 Assessment 数据于当前会话并返回存储失败的错误。 +3. WHEN 评估者发起对历史 Assessment 的复评, THE System SHALL 基于原 Assessment 的输入创建新的 Assessment 并保留原 Assessment 不变。 +4. IF 复评引用的历史 Assessment 不存在, THEN THE System SHALL 拒绝复评并返回该评估不存在的提示。 +5. WHEN 评估者请求跨项目对比且选中 2 个及以上 Assessment, THE System SHALL 返回被选中 Assessment 的 Risk_Grade、Risk_Score 与关键风险的对比数据。 +6. IF 跨项目对比选中的 Assessment 少于 2 个, THEN THE System SHALL 拒绝对比并提示至少需选择 2 个评估。 +7. WHEN 评估者按业务类型、行业、Risk_Grade 或创建时间范围检索历史 Assessment, THE System SHALL 返回匹配的 Assessment,并在无匹配时返回空结果集。 + +### Requirement 18: 可解释性与证据标注 + +**User Story:** 作为风控,我希望每个风险项都有可追溯的依据,以便避免黑箱打分并支撑决策。 + +#### Acceptance Criteria + +1. THE Scoring_Engine SHALL 为每个评分项输出非空的判定依据,且该判定依据引用其所属 Dimension、Indicator、Scoring_Rule 及导致该 Risk_Level 的数据点取值。 +2. THE Scoring_Engine SHALL 为每个评分项输出非空的风险影响说明。 +3. THE Scoring_Engine SHALL 为每个评分项输出非空的建议。 +4. THE System SHALL 为每个评分项展示其 Data_Provenance(取值为 用户输入 / 外部数据 / 智能体假设 之一)与取值范围 0 至 1 的 Confidence。 +5. WHERE 评分项的 Data_Provenance 为"智能体假设", THE System SHALL 在报告的信息缺口说明中列出该项。 +6. WHERE 评分项的 Data_Provenance 为"智能体假设", THE System SHALL 为该项输出关联对应 Indicator 的建议补充尽调事项。 + +### Requirement 19: 统一设计系统与视觉规范 + +**User Story:** 作为评估者,我希望 UI 在所有页面采用统一的组件、排版与配色规范,以便获得专业一致的使用体验。 + +#### Acceptance Criteria + +1. THE UI SHALL 在全部页面从同一 Design_System 引用按钮、表单控件、表格、卡片、对话框、导航与提示组件,且同类组件在不同页面的外观与交互行为一致。 +2. THE Design_System SHALL 定义不少于 4 级的文本排版层级,每级具有具名标识、固定字号与固定行高,且 UI 的全部文本从该排版层级取值。 +3. THE Design_System SHALL 为 Risk_Grade 的 低、中、高、极高 四级各定义一个唯一且稳定的 Color_Token,且 UI 在 Risk_Badge、风险热力图与关键风险图中对同一 Risk_Grade 一致使用其对应 Color_Token。 +4. THE Design_System SHALL 定义以 4 像素为基数的间距标度,且 UI 组件间距取值为该基数的整数倍。 +5. THE Design_System SHALL 提供单一来源的图标集,且 UI 的全部图标从该图标集引用。 +6. THE UI SHALL 提供 明亮(Light)与 暗黑(Dark)两种 Theme,并在切换 Theme 时对全部页面应用所选 Theme 对应的 Color_Token。 +7. WHEN 用户在一次会话内切换 Theme, THE UI SHALL 在 1 秒内对当前页面应用所选 Theme 且不丢失当前页面已录入的数据。 + +### Requirement 20: 数据可视化的专业呈现 + +**User Story:** 作为管理层,我希望风险与费用数据以专业、易读的图表呈现,以便快速理解评估结论。 + +#### Acceptance Criteria + +1. THE UI SHALL 提供风险热力图、风险总分仪表盘、Risk_Badge、Top N 关键风险图、费用拆解图、基准报价与风险调整后报价对比图,以及跨项目组合对比图。 +2. THE UI SHALL 为每个含两个及以上数据系列或类别的 Chart 提供图例,且图例标签与 Chart 中对应数据元素一致。 +3. THE UI SHALL 为每个 Chart 的坐标轴、数据点或分区提供文本标签,且标签文本不被相邻元素遮挡。 +4. WHILE 某个 Chart 对应的数据为空, THE UI SHALL 为该 Chart 呈现 Empty_State 并提示无可展示数据。 +5. WHILE 某个 Chart 对应的数据正在请求或计算中, THE UI SHALL 为该 Chart 呈现 Loading_State。 +6. THE UI SHALL 在风险总分仪表盘中同时呈现 Risk_Score 数值与其对应的 Risk_Grade。 +7. THE UI SHALL 在费用对比图中同时呈现基准报价金额、风险调整后报价金额及二者差额。 + +### Requirement 21: 便捷易用的评估流程 + +**User Story:** 作为评估者,我希望评估流程有清晰引导与顺畅交互,以便高效完成评估而不遗漏关键信息。 + +#### Acceptance Criteria + +1. THE UI SHALL 以 Wizard 形式将评估流程拆分为有序步骤,并在每一步呈现当前步骤序号、步骤总数与已完成步骤的进度指示。 +2. WHEN 评估者在 Wizard 中从对话式追问切换至表单录入或从表单录入切换至对话式追问, THE UI SHALL 保留切换前已录入的数据。 +3. WHILE 本次 Assessment 存在标记为信息缺口或待补充的项, THE UI SHALL 以区别于常规文本的醒目样式呈现全部该类项并提供定位至对应录入位置的入口。 +4. THE UI SHALL 为提交评估、保存草稿与导出报告各提供一个一键触发的操作入口。 +5. WHEN 评估者请求保存草稿, THE UI SHALL 将当前 Assessment 持久化为 Draft 并保留全部已录入数据。 +6. WHEN 评估者打开一个已保存的 Draft, THE UI SHALL 恢复该 Draft 的全部已录入数据并定位至保存时所处的 Wizard 步骤。 +7. IF 评估者在存在未保存修改时请求离开当前评估流程, THEN THE UI SHALL 提示存在未保存修改并请求确认。 + +### Requirement 22: 响应式与多视口适配 + +**User Story:** 作为评估者,我希望 UI 在不同分辨率下都能正常使用,以便在多种设备上查看评估结果。 + +#### Acceptance Criteria + +1. THE UI SHALL 在 Viewport 宽度大于等于 1280 CSS 像素时以桌面布局呈现全部功能。 +2. WHILE Viewport 宽度处于 768 至 1279 CSS 像素, THE UI SHALL 调整布局使全部内容在不产生横向滚动条的前提下完整可见。 +3. WHILE Viewport 宽度小于 768 CSS 像素, THE UI SHALL 在看板视图中保留 Risk_Score、Risk_Grade 与 Top N 关键风险清单的呈现。 +4. WHEN Viewport 宽度变化跨越布局断点, THE UI SHALL 应用与当前 Viewport 宽度匹配的布局且不丢失当前页面已录入的数据。 + +### Requirement 23: 可访问性达标 + +**User Story:** 作为依赖辅助技术的用户,我希望 UI 满足无障碍标准,以便我能够无障碍地完成评估操作。 + +#### Acceptance Criteria + +1. THE UI SHALL 使全部交互控件可通过键盘获得焦点并触发其操作。 +2. WHILE 某交互控件获得键盘焦点, THE UI SHALL 为该控件呈现可见的焦点指示。 +3. THE UI SHALL 使正文文本与其背景的对比度不低于 4.5:1,使大号文本与其背景的对比度不低于 3:1。 +4. THE UI SHALL 为每个表单输入控件关联可被辅助技术识别的文本标签。 +5. IF 表单校验失败, THEN THE UI SHALL 为每个校验未通过的输入控件呈现可被辅助技术识别的错误提示文本。 +6. THE UI SHALL 在 Chart 中以颜色之外的文本标签或图案区分各数据类别,使数据类别不依赖颜色即可识别。 + +### Requirement 24: 操作反馈与状态提示 + +**User Story:** 作为评估者,我希望每次操作都能得到清晰反馈,以便了解系统状态并在出错时知道如何修正。 + +#### Acceptance Criteria + +1. WHILE 一次用户触发的操作正在处理中, THE UI SHALL 呈现 Loading_State 指示该操作进行中。 +2. WHEN 一次用户触发的操作成功完成, THE UI SHALL 呈现指示操作成功的反馈。 +3. IF 一次用户触发的操作失败, THEN THE UI SHALL 呈现可读的错误信息,且该错误信息说明失败原因并提供指向修正路径的操作入口。 +4. WHILE 报告导出操作正在执行, THE UI SHALL 呈现进度反馈,且在评估者请求导出后 30 秒内呈现导出完成或导出失败的结果。 + +### Requirement 25: 角色化默认视图 + +**User Story:** 作为不同角色的用户,我希望登录后直接进入与我职责匹配的视图,以便减少导航成本快速开展工作。 + +#### Acceptance Criteria + +1. WHEN 商务/销售角色用户登录, THE UI SHALL 将该用户默认导航至面向商务/销售的视图。 +2. WHEN 风控角色用户登录, THE UI SHALL 将该用户默认导航至面向风控的视图。 +3. WHEN 管理层角色用户登录, THE UI SHALL 将该用户默认导航至面向管理层的高层看板视图。 +4. THE UI SHALL 在默认视图中将与当前用户角色匹配的功能入口呈现于不需额外导航即可见的位置。 +5. IF 登录用户未分配商务/销售、风控或管理层任一角色, THEN THE UI SHALL 呈现提示需分配角色的视图且不呈现评估数据。 diff --git a/.kiro/specs/outsourcing-risk-assessment/tasks.md b/.kiro/specs/outsourcing-risk-assessment/tasks.md new file mode 100644 index 0000000..e331592 --- /dev/null +++ b/.kiro/specs/outsourcing-risk-assessment/tasks.md @@ -0,0 +1,747 @@ +# Implementation Plan: 外包项目风险评估 AI 智能体 + +## Overview + +实现语言为 **TypeScript**,属性化测试库采用 **fast-check**,单元/集成测试运行器采用 **Vitest**。 + +实现策略自底向上、增量推进:先建立项目骨架与核心领域类型 + Data_Provenance 三态工具,再实现配置中心(模板加载/继承/实例化/校验),随后是评分引擎(评分/归一化/分级/红线/热力图/Top N),费用测算引擎、策略引擎、追问引擎、分类器、合规规则集、知识库、外部数据适配层、报告生成、持久化、RBAC 与角色视图,由 Orchestrator 端到端串联;最后实现前端表现层(Design_System 设计系统、数据可视化组件、Wizard 评估流程、响应式适配、可访问性、操作反馈与角色化默认视图),复用领域引擎输出。 + +每条 Correctness Property(共 81 条:领域逻辑 62 条 + Req 19-25 UI 可属性化 19 条,即 Property 63-81)由单个属性化测试实现,紧邻其实现任务以尽早发现错误。属性测试约束:使用 fast-check、每条属性至少运行 100 次迭代、以注释标注标签 `Feature: outsourcing-risk-assessment, Property {n}: {property_text}`。带 `*` 的子任务为可选测试任务。 + +## Tasks + +- [x] 1. 搭建项目骨架与核心领域类型 + - [x] 1.1 初始化 TypeScript 工程与测试框架 + - 初始化 `package.json`、`tsconfig.json`、目录结构(`src/`、`src/__tests__/`) + - 安装并配置 Vitest 与 fast-check(≥100 次迭代默认配置) + - 添加 build / test / lint 脚本 + - _Requirements: 基础设施(无直接验收标准)_ + + - [x] 1.2 定义核心领域类型与接口 + - 定义 `RiskModel`、`Dimension`、`Indicator`、`ScoringRule`、`Redline`、`Template` + - 定义 `Assessment`、`ScoringItem`、`RedlineResult`、`HeatmapCell`、`RiskItem` + - 定义 `DataProvenance`(用户输入/外部数据/智能体假设)、`Region`、`ComplianceRuleSet` + - 定义 `KnowledgeBase`、`IndustryPartition`、`ConfigAuditEntry`、`CostEstimate`、`Acceptability` + - _Requirements: 2.3, 4.6, 16.4, 17.1, 18.4_ + + - [x] 1.3 实现 Data_Provenance 三态工具 + - 实现 provenance 标注/转移函数,强制"智能体假设"单调永久(一旦标注不可被改回其他取值) + - 实现 Confidence 值域约束([0,1] 两位小数) + - _Requirements: 3.7, 4.6, 18.4_ + + - [x]* 1.4 编写 Data_Provenance 单调性属性测试 + - **Property 16: 智能体假设标注单调永久** + - **Validates: Requirements 3.7** + +- [x] 2. 实现 Config_Center 模型配置中心 + - [x] 2.1 实现权重归一化 `normalizeWeights` + - 按比例归一化使同级启用项权重之和=100%(两位小数);全零向量拒绝 + - _Requirements: 11.2, 11.8_ + + - [x]* 2.2 编写权重归一化保比例属性测试 + - **Property 40: 权重归一化保比例且同级和为 100%** + - **Validates: Requirements 11.2** + + - [x]* 2.3 编写权重全零拒绝属性测试 + - **Property 44: 同级权重全零拒绝归一化** + - **Validates: Requirements 11.8** + + - [x] 2.4 实现模板继承解析 `resolveInheritance` + - 应用父模板全部组成项后以子模板差异项逐项覆盖;检测循环引用与层级>5 报错 + - _Requirements: 2.5, 2.7_ + + - [x]* 2.5 编写模板继承逐项覆盖属性测试 + - **Property 9: 模板继承逐项覆盖** + - **Validates: Requirements 2.5** + + - [x]* 2.6 编写继承链环与深度防护属性测试 + - **Property 10: 继承链环与深度防护** + - **Validates: Requirements 2.7** + + - [x] 2.7 实现模板匹配与回退 `loadTemplate` + - 精确匹配优先;无精确匹配回退业务类型默认模板并标注"未匹配行业专用模板";皆无则终止返回无可用模板提示 + - _Requirements: 2.1, 2.2, 2.6, 14.3, 14.5_ + + - [x]* 2.8 编写模板匹配与回退确定性属性测试 + - **Property 6: 模板匹配与回退确定性** + - **Validates: Requirements 2.1, 2.2, 2.6, 14.3, 14.5** + + - [x] 2.9 实现风险模型实例化与校验 `instantiateRiskModel` + - 保留全部组成项与启停状态;校验必填项完整性与同级权重和=100%,非法则不实例化并终止 + - _Requirements: 2.3, 2.4_ + + - [x]* 2.10 编写实例化结构保持属性测试 + - **Property 7: 模板实例化结构保持** + - **Validates: Requirements 2.3** + + - [x]* 2.11 编写非法模板拒绝实例化属性测试 + - **Property 8: 非法模板必被拒绝实例化** + - **Validates: Requirements 2.4** + + - [x] 2.12 实现配置保存、另存模板与启停 + - 实现 `saveConfig`(校验 Scoring_Rule 覆盖 1-5、Redline 唯一/必填、权重合法性后保存;失败保留上次有效配置) + - 实现 `saveAsTemplate`/派生;实现 Dimension/Indicator 启用/停用(停用保留配置但不计分) + - _Requirements: 11.1, 11.3, 11.4, 11.5, 11.6, 11.7_ + + - [x]* 2.13 编写评分规则与红线配置校验属性测试 + - **Property 41: 评分规则与红线配置校验** + - **Validates: Requirements 11.3, 11.4** + + - [x]* 2.14 编写配置另存为模板可往返属性测试 + - **Property 42: 配置另存为模板可往返** + - **Validates: Requirements 11.5** + + - [x]* 2.15 编写校验失败保留上次有效配置属性测试 + - **Property 43: 校验失败保留上次有效配置** + - **Validates: Requirements 11.6, 11.7** + + - [x]* 2.16 编写停用项保留但不计分属性测试 + - **Property 39: 停用项保留但不计分** + - **Validates: Requirements 11.1** + +- [x] 3. Checkpoint - 确保配置中心测试通过 + - Ensure all tests pass, ask the user if questions arise. + +- [x] 4. 实现 Scoring_Engine 评分引擎 + - [x] 4.1 实现 `scoreIndicator` 与 `scoreDimension` + - 评分项得分 = Risk_Level(1-5) × 权重;维度得分 = 启用指标评分项加权求和 + - _Requirements: 4.1, 4.2_ + + - [x]* 4.2 编写评分项得分公式属性测试 + - **Property 17: 评分项得分公式** + - **Validates: Requirements 4.1, 4.2** + + - [x] 4.3 实现 `computeRiskScore` 归一化 + - 线性映射 `round((weightedRaw-1)/4×100)`;仅纳入启用项;无启用项返回评分数据不足错误 + - _Requirements: 4.3, 4.4, 4.5_ + + - [x]* 4.4 编写归一化端点与值域属性测试 + - **Property 18: 归一化端点与值域** + - **Validates: Requirements 4.3** + + - [x]* 4.5 编写停用项不影响评分属性测试 + - **Property 19: 停用项不影响评分** + - **Validates: Requirements 4.4** + + - [x]* 4.6 编写无启用项必报错属性测试 + - **Property 20: 无启用项必报错** + - **Validates: Requirements 4.5** + + - [x] 4.7 实现 `classifyGrade` 风险分级 + - 区间 [0,25]→低、(25,50]→中、(50,75]→高、(75,100]→极高,输出且仅输出一个 Risk_Grade + - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_ + + - [x]* 4.8 编写分级区间互斥且完备属性测试 + - **Property 22: 分级区间互斥且完备** + - **Validates: Requirements 5.1, 5.2, 5.3, 5.4, 5.5** + + - [x] 4.9 实现 `explain` 与评分项来源/置信标注 + - 为每个评分项输出非空判定依据(引用 Dimension/Indicator/Scoring_Rule/数据点取值)、风险影响、建议 + - 记录 Data_Provenance 与 [0,1] Confidence + - _Requirements: 4.6, 18.1, 18.2, 18.3, 18.4_ + + - [x]* 4.10 编写评分项来源与置信标注属性测试 + - **Property 21: 评分项来源与置信标注** + - **Validates: Requirements 4.6, 18.4** + + - [x]* 4.11 编写可解释三要素非空属性测试 + - **Property 61: 评分项可解释三要素非空** + - **Validates: Requirements 18.1, 18.2, 18.3** + + - [x] 4.12 实现 `checkRedlines` 红线校验 + - 独立于 Risk_Score/Risk_Grade 校验全部启用红线;满足触发条件标记命中 + - 数据缺失或为"智能体假设"标"待核实"且不计命中 + - _Requirements: 6.1, 6.4, 6.5_ + + - [x]* 4.13 编写红线校验正确且独立于分值属性测试 + - **Property 23: 红线校验正确且独立于分值** + - **Validates: Requirements 6.1, 6.4** + + - [x]* 4.14 编写红线数据不足标待核实属性测试 + - **Property 24: 红线数据不足标待核实** + - **Validates: Requirements 6.5** + + - [x] 4.15 实现 `buildHeatmap` 风险热力图 + - 为每个启用 Indicator 输出 Dimension 行 × Indicator 列 × Risk_Level(1-5) 单元格 + - _Requirements: 7.1_ + + - [x]* 4.16 编写热力图覆盖全部启用指标属性测试 + - **Property 29: 热力图覆盖全部启用指标** + - **Validates: Requirements 7.1** + + - [x] 4.17 实现 `topKeyRisks` 关键风险清单 + - 得分降序 Top N(N 可配 1-50 默认 10);同分按 Dimension 权重→Indicator 权重→稳定标识消歧;少于 N 输出全部;每项含 Dimension/Indicator/得分/判定依据 + - _Requirements: 7.2, 7.3, 7.4, 7.5_ + + - [x]* 4.18 编写 Top N 确定性排序属性测试 + - **Property 30: Top N 确定性排序** + - **Validates: Requirements 7.2, 7.3, 7.4, 7.5** + +- [x] 5. Checkpoint - 确保评分引擎测试通过 + - Ensure all tests pass, ask the user if questions arise. + +- [x] 6. 实现 Cost_Engine 费用测算引擎 + - [x] 6.1 实现 `estimate` 费用测算 + - 依 Risk_Grade+评分计算风险溢价区间(下界≤上界、随分级单调) + - 计算垫资利息/保险费用/补偿准备金/坏账准备金(非负、标注依据输入项与费率来源) + - 输出基准报价与风险调整后报价(后者≥基准)及拆解一致;缺成本输入兜底默认值标"智能体假设";评分未完成拒绝并提示 + - _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6_ + + - [x]* 6.2 编写风险溢价区间合法且单调属性测试 + - **Property 31: 风险溢价区间合法且随分级单调** + - **Validates: Requirements 8.1** + + - [x]* 6.3 编写费用项非负且标注依据属性测试 + - **Property 32: 费用项非负且标注依据** + - **Validates: Requirements 8.2, 8.6** + + - [x]* 6.4 编写报价不低于基准且拆解一致属性测试 + - **Property 33: 风险调整后报价不低于基准且拆解一致** + - **Validates: Requirements 8.3** + + - [x]* 6.5 编写缺失成本输入兜底为假设属性测试 + - **Property 34: 缺失成本输入兜底为假设** + - **Validates: Requirements 8.4** + + - [x]* 6.6 编写评分未完成拒绝费用测算属性测试 + - **Property 35: 评分未完成拒绝费用测算** + - **Validates: Requirements 8.5** + +- [x] 7. 实现 Strategy_Engine 应对策略引擎 + - [x] 7.1 实现 `decide` 可接受性决策表 + - 红线命中→不可接受(优先于 Grade);未命中时 低/中→可接受、高→有条件接受、极高→不可接受 + - _Requirements: 6.2, 6.6, 9.1, 9.6, 9.7, 9.8_ + + - [x]* 7.2 编写可接受性决策表完备且红线最高优先属性测试 + - **Property 26: 可接受性决策表完备且红线最高优先** + - **Validates: Requirements 6.2, 6.6, 9.1, 9.6, 9.7, 9.8** + + - [x] 7.3 实现管理层面与费用层面应对措施 + - 管理措施覆盖合同条款/用工合规整改/退场预案/过程监控四类各≥1 + - 费用措施覆盖风险溢价定价/预付保证金/保险转移/账期成本/准备金计提五类各≥1 + - _Requirements: 9.3, 9.4_ + + - [x]* 7.4 编写应对措施类别全覆盖属性测试 + - **Property 27: 应对措施类别全覆盖** + - **Validates: Requirements 9.3, 9.4** + + - [x] 7.5 实现接受条件清单 + - 有条件接受时每个条件关联≥1 关键风险项并输出具体金额/区间的成本影响测算 + - _Requirements: 9.2, 9.5_ + + - [x]* 7.6 编写有条件接受的条件关联与成本影响属性测试 + - **Property 28: 有条件接受的条件关联与成本影响** + - **Validates: Requirements 9.2, 9.5** + +- [x] 8. 实现 Question_Engine 自适应追问引擎 + - [x] 8.1 实现 `identifyGaps` 信息缺口识别 + - 缺口集合恰为 {启用 ∧ 依 Scoring_Rule 无法判定 Risk_Level} 的指标 + - _Requirements: 3.1_ + + - [x]* 8.2 编写信息缺口识别准确属性测试 + - **Property 11: 信息缺口识别准确** + - **Validates: Requirements 3.1** + + - [x] 8.3 实现 `generateQuestions` 追问问题生成 + - 仅覆盖缺口指标;按 Indicator 权重降序、同权按 Dimension 权重降序;每问关联真实 Dimension 与 Indicator + - _Requirements: 3.2, 3.8_ + + - [x]* 8.4 编写追问仅针对缺口且排序确定属性测试 + - **Property 12: 追问仅针对缺口且排序确定** + - **Validates: Requirements 3.2, 3.8** + + - [x] 8.5 实现 `answerQuestion` 回答处理 + - 满足证据要求→移出缺口并更新已知信息;空/不满足证据→保留缺口并返回需补充信息提示 + - _Requirements: 3.3, 3.4_ + + - [x]* 8.6 编写有效回答移出缺口属性测试 + - **Property 13: 有效回答移出缺口** + - **Validates: Requirements 3.3** + + - [x]* 8.7 编写无效回答保留缺口属性测试 + - **Property 14: 无效回答保留缺口** + - **Validates: Requirements 3.4** + + - [x] 8.8 实现 `applyDefaultsOnExhaust` 兜底 + - 单指标追问轮次不超过可配上限(默认 3);达上限仍缺失采用行业默认值并标注"智能体假设" + - _Requirements: 3.5, 3.6_ + + - [x]* 8.9 编写追问轮次不超上限并触发兜底属性测试 + - **Property 15: 追问轮次不超上限并触发兜底** + - **Validates: Requirements 3.5, 3.6** + +- [x] 9. 实现 Classifier 业务类型与行业识别 + - [x] 9.1 实现 `classify` 识别与输入校验 + - 输入<10 有效字符/空白拒绝;业务类型取五类中 Confidence 最高唯一项;行业不可判定取"未识别";Confidence∈[0,1] 两位小数 + - _Requirements: 1.1, 1.2, 1.3, 1.6_ + + - [x]* 9.2 编写业务类型判定唯一且取最高置信属性测试 + - **Property 1: 业务类型判定唯一且取最高置信** + - **Validates: Requirements 1.1** + + - [x]* 9.3 编写置信度恒在有效值域内属性测试 + - **Property 2: 置信度恒在有效值域内** + - **Validates: Requirements 1.3** + + - [x]* 9.4 编写描述信息不足必被拒绝属性测试 + - **Property 4: 描述信息不足必被拒绝** + - **Validates: Requirements 1.6** + + - [x] 9.5 实现候选列表与确认标志 + - Confidence<0.6(行业附加:行业≠"未识别")时返回按 Confidence 降序≤3 项候选并置确认标志 + - _Requirements: 1.4, 1.5_ + + - [x]* 9.6 编写低置信触发候选确认属性测试 + - **Property 3: 低置信触发候选确认** + - **Validates: Requirements 1.4, 1.5** + + - [x] 9.7 实现 `confirmClassification` 确认值驱动 + - 采用评估者确认/修改后的业务类型与行业作为后续加载模板依据 + - _Requirements: 1.7_ + + - [x]* 9.8 编写确认值驱动后续加载属性测试 + - **Property 5: 确认值驱动后续加载** + - **Validates: Requirements 1.7** + + - [x]* 9.9 编写行业语义识别单元测试 + - 代表性描述验证可识别行业与"未识别"分支 + - _Requirements: 1.2_ + +- [x] 10. 实现 Compliance_Rule_Set 地域合规规则集 + - [x] 10.1 实现按 Region 加载规则集与 Region 记录/默认 + - 创建 Assessment 记录所采用 Region;未指定时默认 CN 并标注系统默认值;无对应规则集拒绝合规处理并提示暂不支持 + - _Requirements: 16.4, 16.5, 16.6_ + + - [x]* 10.2 编写 Region 记录与默认属性测试 + - **Property 55: Region 记录与默认** + - **Validates: Requirements 16.4, 16.5** + + - [x]* 10.3 编写无规则集地域拒绝合规处理属性测试 + - **Property 56: 无规则集地域拒绝合规处理** + - **Validates: Requirements 16.6** + + - [x] 10.4 实现 CN 合规判定与费用测算 + - 覆盖社保基数、经济补偿 N/N+1、派遣比例上限 10%、当地最低工资;计算经济补偿/社保金额/合规费用并标注规则项与输入项;不满足项标合规不通过 + - _Requirements: 16.1, 16.2, 16.3_ + + - [x]* 10.5 编写合规判定覆盖与计量标注属性测试 + - **Property 53: 合规判定覆盖与计量标注** + - **Validates: Requirements 16.1, 16.2** + + - [x]* 10.6 编写合规不满足项标注属性测试 + - **Property 54: 合规不满足项标注** + - **Validates: Requirements 16.3** + + - [x]* 10.7 编写合规算例单元测试 + - 给定社保基数/派遣比例/最低工资代表性数值验证 N/N+1 与社保金额计算正确 + - _Requirements: 16.2_ + +- [x] 11. 实现 Knowledge_Base 分行业知识库 + - [x] 11.1 实现行业分区存储与完备性校验 + - 按行业分区存储 Indicator/权重模板/Redline/典型案例/追问话术五类;缺任一类拒绝创建并指明缺失类别且保持已有分区不变 + - _Requirements: 14.1, 14.4_ + + - [x]* 11.2 编写行业分区内容完备性校验属性测试 + - **Property 50: 行业分区内容完备性校验** + - **Validates: Requirements 14.1, 14.4** + + - [x]* 11.3 编写知识库行业分区运行时扩展集成测试 + - 运行时新增分区后验证其 Template 可被加载、无需修改 Scoring_Engine 源码或重编译 + - _Requirements: 14.2_ + +- [x] 12. 实现 External_Data_Adapter 外部数据适配层 + - [x] 12.1 实现适配器接口与来源标注 + - 定义可插拔 `DataSourceAdapter` 接口;成功取数标注 Data_Provenance="外部数据" 与 [0,1] Confidence + - _Requirements: 15.1, 15.2, 15.5_ + + - [x]* 12.2 编写外部数据点来源与置信标注属性测试 + - **Property 51: 外部数据点来源与置信标注** + - **Validates: Requirements 15.2** + + - [x] 12.3 实现超时/失败降级回退 + - 连接失败/超 10 秒/错误响应回退用户输入标"用户输入";回退后仍缺失标"智能体假设"并继续评估不中断 + - _Requirements: 15.3, 15.4_ + + - [x]* 12.4 编写外部数据失败降级回退属性测试 + - **Property 52: 外部数据失败降级回退** + - **Validates: Requirements 15.3, 15.4** + + - [x]* 12.5 编写外部数据适配集成测试 + - mock 数据源验证成功取数路径;注册新适配器验证无需改 Scoring_Engine 源码即可使用 + - _Requirements: 15.1, 15.5_ + +- [x] 13. 实现 Report_Generator 报告生成与导出 + - [x] 13.1 实现 `generate` 报告生成与流程门控 + - 评分/红线/费用/可接受性结论均完成方可生成;含全部规定章节;未命中红线明确标注"无红线命中";流程未完成拒绝并提示 + - _Requirements: 10.1, 10.4_ + + - [x]* 13.2 编写报告章节完备属性测试 + - **Property 36: 报告章节完备** + - **Validates: Requirements 10.1** + + - [x]* 13.3 编写流程未完成拒绝报告属性测试 + - **Property 38: 流程未完成拒绝报告** + - **Validates: Requirements 10.4** + + - [x] 13.4 实现维度明细字段与信息缺口尽调说明 + - 各维度明细每个评分项展示评分/判定依据/风险影响/Data_Provenance/Confidence + - "智能体假设"项列入信息缺口说明并附关联 Indicator 的补充尽调建议 + - _Requirements: 10.3, 18.5, 18.6_ + + - [x]* 13.5 编写维度明细字段齐备属性测试 + - **Property 37: 维度明细字段齐备** + - **Validates: Requirements 10.3** + + - [x]* 13.6 编写假设项进入缺口说明并附尽调建议属性测试 + - **Property 62: 假设项进入缺口说明并附尽调建议** + - **Validates: Requirements 18.5, 18.6** + + - [x] 13.7 实现命中红线列入报告 + - 命中红线评估报告列出每个被命中红线及其被触发条件与判定依据数据 + - _Requirements: 6.3_ + + - [x]* 13.8 编写命中红线列入报告属性测试 + - **Property 25: 命中红线列入报告** + - **Validates: Requirements 6.3** + + - [x] 13.9 实现 `export` 报告导出 + - 30 秒内导出完整自包含可下载文件;失败中止导出、保留已生成报告不变并返回错误 + - _Requirements: 10.2, 10.5_ + + - [x]* 13.10 编写报告导出集成测试 + - 验证导出文件自包含且含全部章节、耗时<30 秒;注入导出失败验证报告内容不变 + - _Requirements: 10.2, 10.5_ + +- [x] 14. 实现评估持久化与历史管理 + - [x] 14.1 实现 `save` 持久化与元数据 + - 完成即持久化输入/评分结果/报告/元数据(含业务类型/行业/Region/Risk_Score/Risk_Grade/创建时间/评估者身份);失败保留会话数据并返回错误 + - _Requirements: 17.1, 17.2_ + + - [x]* 14.2 编写评估持久化往返属性测试 + - **Property 57: 评估持久化往返** + - **Validates: Requirements 17.1** + + - [x] 14.3 实现 `reassess` 复评 + - 基于原输入创建新 Assessment(新标识)且原评估不变;引用不存在评估拒绝并提示 + - _Requirements: 17.3, 17.4_ + + - [x]* 14.4 编写复评保留原评估属性测试 + - **Property 58: 复评保留原评估** + - **Validates: Requirements 17.3, 17.4** + + - [x] 14.5 实现 `compare` 跨项目对比 + - ≥2 个返回各 Risk_Grade/Risk_Score/关键风险对比数据;<2 个拒绝并提示至少需选 2 个 + - _Requirements: 17.5, 17.6_ + + - [x]* 14.6 编写跨项目对比数量约束与内容属性测试 + - **Property 59: 跨项目对比的数量约束与内容** + - **Validates: Requirements 17.5, 17.6** + + - [x] 14.7 实现 `search` 历史检索 + - 按业务类型/行业/Risk_Grade/创建时间范围检索;返回项均满足条件,无匹配返回空集 + - _Requirements: 17.7_ + + - [x]* 14.8 编写检索结果满足过滤条件属性测试 + - **Property 60: 检索结果满足过滤条件** + - **Validates: Requirements 17.7** + + - [x]* 14.9 编写持久化失败集成测试 + - 注入存储失败验证会话数据保留并返回存储失败错误 + - _Requirements: 17.2_ + +- [x] 15. 实现 RBAC 角色权限与角色视图 + - [x] 15.1 实现 `requireRole`、非管理员配置拒绝与审计 + - 非 Administrator(含 Assessor/未认证/未授权)配置修改一律拒绝、配置不变、返回权限不足 + - 成功变更记录操作者/精确到秒变更时间/变更项标识;拒绝记录操作者与请求时间 + - _Requirements: 12.1, 12.3, 12.4, 12.5_ + + - [x]* 15.2 编写非管理员配置修改一律拒绝属性测试 + - **Property 45: 非管理员配置修改一律拒绝且配置不变** + - **Validates: Requirements 12.1, 12.3** + + - [x]* 15.3 编写配置变更与拒绝均留痕审计属性测试 + - **Property 46: 配置变更与拒绝均留痕审计** + - **Validates: Requirements 12.4, 12.5** + + - [x] 15.4 实现 `renderView` 角色化视图 + - 商务/销售:结论+接受条件+风险调整后报价;风控:评分明细(Risk_Level/依据/Provenance)+红线+缺口尽调;管理层:Grade+热力图+TopN+利润对风险对比 + - _Requirements: 13.1, 13.2, 13.3_ + + - [x]* 15.5 编写角色化视图内容映射属性测试 + - **Property 47: 角色化视图内容映射** + - **Validates: Requirements 13.1, 13.2, 13.3** + + - [x] 15.6 实现 `renderPortfolio` 组合看板与无角色拒绝 + - 管理层组合看板汇总全部评估,空集展示空看板并提示;无角色用户拒绝展示并提示分配角色 + - _Requirements: 13.4, 13.5, 13.6_ + + - [x]* 15.7 编写组合看板汇总与空集处理属性测试 + - **Property 48: 组合看板汇总与空集处理** + - **Validates: Requirements 13.4, 13.6** + + - [x]* 15.8 编写无角色拒绝展示属性测试 + - **Property 49: 无角色拒绝展示** + - **Validates: Requirements 13.5** + + - [x]* 15.9 编写 Assessor 可执行评估单元测试 + - 示例验证 Assessor 可使用当前 Risk_Model 执行评估的允许路径 + - _Requirements: 12.2_ + +- [x] 16. 集成与端到端串联 + - [x] 16.1 实现 Assessment Orchestrator 端到端编排 + - 串联分类→模板加载/实例化→追问(含外部数据降级)→评分/红线→费用→策略→报告→持久化,连接全部组件 + - _Requirements: 1.7, 2.1, 3.1, 4.3, 8.1, 9.1, 10.1, 16.4, 17.1_ + + - [x]* 16.2 编写端到端评估流程集成测试 + - 自动化测试覆盖从项目描述输入到报告生成与持久化的完整流程 + - _Requirements: 10.1, 17.1_ + +- [x] 17. Checkpoint - 确保领域引擎与持久化全部测试通过 + - Ensure all tests pass, ask the user if questions arise. + +- [x] 18. 搭建前端工程脚手架与 Design_System 设计系统 + - [x] 18.1 初始化前端工程脚手架与 UI 测试工具链 + - 初始化 React + TypeScript 前端工程(与既有 TS 工程共用或独立 `web/` 目录),配置打包/路由依赖 + - 安装并配置 UI 测试工具:组件测试(Vitest + React Testing Library)、可视化回归快照、自动化可访问性检查(axe,如 `@axe-core/react`/`jest-axe`)、属性化测试 fast-check + - 引入图表库(如 ECharts 或 Recharts/Visx)依赖 + - _Requirements: 基础设施(无直接验收标准)_ + + - [x] 18.2 实现 Design Tokens(排版/间距/图标/Color_Token) + - 定义 ≥4 级具名 Typography 层级(各含固定字号与固定行高) + - 定义以 4 像素为基数的间距标度(取值均为 4 的整数倍) + - 定义单一来源图标集 IconSet + - 定义稳定 Color_Token,含 Risk_Grade 四级语义化配色(`color.risk.low/medium/high/critical`)与热力图顺序色 `color.heat.1..5`,每令牌含 Light/Dark 双主题取值 + - _Requirements: 19.2, 19.3, 19.4, 19.5_ + + - [x]* 18.3 编写排版层级完备且文本取自层级属性测试 + - **Property 63: 排版层级完备且文本取自层级** + - **Validates: Requirements 19.2** + + - [x]* 18.4 编写 Risk_Grade 配色令牌一致且唯一属性测试 + - **Property 64: Risk_Grade 配色令牌一致且唯一** + - **Validates: Requirements 19.3** + + - [x]* 18.5 编写间距为 4 像素整数倍属性测试 + - **Property 65: 间距为 4 像素整数倍** + - **Validates: Requirements 19.4** + + - [x] 18.6 实现 Theme Provider 与 `resolveColorToken`(CSS Variables) + - 以 CSS 自定义属性承载 Color_Token,按 Theme(Light/Dark)切换变量集 + - 实现 `resolveColorToken(token, theme)` 与 `riskGradeColorToken(grade)`;`setTheme` 仅替换令牌取值且保留页面已录入数据 + - _Requirements: 19.6, 19.7_ + + - [x]* 18.7 编写主题配色令牌解析正确属性测试 + - **Property 66: 主题配色令牌解析正确** + - **Validates: Requirements 19.6** + + - [x] 18.8 实现基础组件库(Button/Input/Table/Card/Dialog/Nav/Toast) + - 基于 Design Tokens 封装可复用基础组件,同类组件全局外观与交互行为一致,图标统一引用单一图标集 + - _Requirements: 19.1, 19.5_ + + - [x]* 18.9 编写组件库一致性与单一图标集组件测试 + - 验证同类组件在不同页面外观/行为一致、全部图标取自单一图标集 + - _Requirements: 19.1, 19.5_ + +- [x] 19. 实现数据可视化组件库(Charts) + - [x] 19.1 实现通用 Chart 容器与状态/图例/标签/非颜色编码 + - 实现 `renderChart(spec)` 通用契约:data 为空→Empty_State 提示无可展示数据;data 请求/计算中→Loading_State + - 系列/类别 ≥2 提供图例(标签与数据元素一致);坐标轴/数据点/分区提供非空文本标签 + - 类别以颜色之外的文本标签或图案区分(`categoryEncoding`) + - 消费 Scoring_Engine 输出(热力图、Top N 数据,见任务 4.15/4.17) + - _Requirements: 20.2, 20.3, 20.4, 20.5, 23.6_ + + - [x] 19.2 实现风险热力图、Risk_Badge 与 Top N 关键风险图 + - 热力图按 Dimension×Indicator×Risk_Level 渲染并附数值标签;Risk_Badge 以 Color_Token + 文字标签呈现 Risk_Grade;Top N 条形图 + - _Requirements: 20.1_ + + - [x] 19.3 实现风险总分仪表盘(Risk_Score 数值 + Risk_Grade) + - 仪表盘同时呈现 Risk_Score 数值与按分级规则对应的 Risk_Grade(与 `classifyGrade` 输出一致) + - _Requirements: 20.1, 20.6_ + + - [x] 19.4 实现费用拆解图与基准 vs 风险调整后报价对比图 + - 费用拆解图;报价对比图同时呈现基准报价、风险调整后报价与二者差额(差额=风险调整后-基准) + - _Requirements: 20.1, 20.7_ + + - [x] 19.5 实现跨项目组合对比图 + - 跨项目组合对比图,消费持久化层 `compare`/组合看板数据 + - _Requirements: 20.1_ + + - [x]* 19.6 编写图表图例与数据系列一致属性测试 + - **Property 68: 图表图例与数据系列一致** + - **Validates: Requirements 20.2** + + - [x]* 19.7 编写图表文本标签齐备属性测试 + - **Property 69: 图表文本标签齐备** + - **Validates: Requirements 20.3** + + - [x]* 19.8 编写图表空态与加载态呈现属性测试 + - **Property 70: 图表空态与加载态呈现** + - **Validates: Requirements 20.4, 20.5** + + - [x]* 19.9 编写仪表盘同时呈现总分与分级属性测试 + - **Property 71: 仪表盘同时呈现总分与分级** + - **Validates: Requirements 20.6** + + - [x]* 19.10 编写费用对比图三值并呈且差额一致属性测试 + - **Property 72: 费用对比图三值并呈且差额一致** + - **Validates: Requirements 20.7** + + - [x]* 19.11 编写图表非纯颜色编码属性测试 + - **Property 79: 图表非纯颜色编码** + - **Validates: Requirements 23.6** + + - [x]* 19.12 编写全套图表组件渲染单元测试 + - 验证 Req 20.1 规定的全套图表组件(热力图/仪表盘/Risk_Badge/Top N/费用拆解/报价对比/组合对比)均可渲染 + - _Requirements: 20.1_ + +- [x] 20. 实现 Wizard 向导与评估流程 UI + - [x] 20.1 实现 Wizard 步骤与进度指示 + - 将评估流程拆分为有序步骤,呈现当前步骤序号、步骤总数与已完成步骤进度;`advance` 使已完成数单调非减 + - _Requirements: 21.1_ + + - [x] 20.2 实现录入方式切换与一键操作入口 + - 对话式追问↔表单录入切换保留切换前已录入数据;为提交评估/保存草稿/导出报告各提供一键触发入口 + - _Requirements: 21.2, 21.4_ + + - [x] 20.3 实现信息缺口提示面板 + - 以区别于常规文本的醒目样式呈现全部信息缺口/待补充项,并为每项提供定位至对应录入位置的入口 + - _Requirements: 21.3_ + + - [x] 20.4 实现 Draft 草稿保存、断点续评与未保存离开确认 + - 保存草稿持久化为 Draft 保留全部录入数据;打开 Draft 恢复全部数据并定位至保存时步骤;存在未保存修改离开时弹出确认 + - _Requirements: 21.5, 21.6, 21.7_ + + - [x]* 20.5 编写 Wizard 进度正确且单调属性测试 + - **Property 73: Wizard 进度正确且单调** + - **Validates: Requirements 21.1** + + - [x]* 20.6 编写缺口项醒目呈现并可定位属性测试 + - **Property 74: 缺口项醒目呈现并可定位** + - **Validates: Requirements 21.3** + + - [x]* 20.7 编写 Draft 往返保真属性测试 + - **Property 75: Draft 往返保真** + - **Validates: Requirements 21.5, 21.6** + + - [x]* 20.8 编写一键入口与未保存离开确认组件测试 + - 验证提交/保存草稿/导出一键入口存在,且未保存修改离开时呈现确认提示 + - _Requirements: 21.4, 21.7_ + +- [x] 21. 实现响应式布局服务与断点适配 + - [x] 21.1 实现 `selectLayout` 断点映射与 `onViewportChange` + - 确定性断点映射:≥1280→桌面布局(全部功能)、768-1279→紧凑布局(无横向滚动、内容完整可见)、<768→移动布局(看板保留 Risk_Score/Risk_Grade/Top N) + - 跨断点切换应用匹配布局且不丢失已录入数据 + - _Requirements: 22.1, 22.2, 22.3, 22.4_ + + - [x]* 21.2 编写断点布局映射确定性属性测试 + - **Property 76: 断点布局映射确定性** + - **Validates: Requirements 22.1, 22.2, 22.3** + + - [x]* 21.3 编写 UI 状态转换保留已录入数据属性测试 + - **Property 67: UI 状态转换保留已录入数据** + - 覆盖切换 Theme、对话↔表单录入切换、跨断点布局变更后数据不丢失不篡改 + - **Validates: Requirements 19.7, 21.2, 22.4** + +- [x] 22. 实现可访问性(WCAG 2.1 AA) + - [x] 22.1 实现键盘可达、可见焦点、表单标签与无障碍错误 + - 全部交互控件可键盘聚焦并触发、聚焦呈现可见焦点指示;每个表单输入控件关联可被辅助技术识别的非空文本标签;校验失败为每个未通过控件呈现可被辅助技术识别的错误提示 + - _Requirements: 23.1, 23.2, 23.4, 23.5_ + + - [x] 22.2 实现 `contrastRatio` 对比度计算 + - 计算 WCAG 相对对比度,供正文 ≥4.5:1、大号文本 ≥3:1 达标校验 + - _Requirements: 23.3_ + + - [x]* 22.3 编写文本对比度达标属性测试 + - **Property 77: 文本对比度达标** + - **Validates: Requirements 23.3** + + - [x]* 22.4 编写表单可访问标注与错误提示属性测试 + - **Property 78: 表单可访问标注与错误提示** + - **Validates: Requirements 23.4, 23.5** + + - [x]* 22.5 编写 axe 自动化可访问性检查 + - 对全部页面运行 axe 规则集,验证键盘可聚焦与可见焦点指示、标签关联与 ARIA 正确性 + - _Requirements: 23.1, 23.2_ + +- [x] 23. 实现操作反馈与角色化默认视图/路由守卫 + - [x] 23.1 实现操作状态反馈与导出进度反馈 + - `runOperation` 状态映射:处理中→Loading_State、成功→成功反馈、失败→可读错误信息(说明原因+指向修正路径的操作入口) + - 报告导出执行中呈现进度反馈,30 秒内呈现完成或失败终态 + - _Requirements: 24.1, 24.2, 24.3, 24.4_ + + - [x]* 23.2 编写操作状态反馈映射属性测试 + - **Property 80: 操作状态反馈映射** + - **Validates: Requirements 24.1, 24.2, 24.3** + + - [x] 23.3 实现 `defaultRoute` 角色化默认视图与 RBAC 路由守卫 + - 角色默认导航:商务/销售→SalesView、风控→RiskView、管理层→ManagementDashboard;无角色→需分配角色提示视图且不呈现评估数据 + - 默认视图首屏将与角色匹配的功能入口呈现于无需额外导航即可见的位置;复用任务 15 的角色化视图(`renderView`/`renderPortfolio`)作为目标视图 + - _Requirements: 25.1, 25.2, 25.3, 25.4, 25.5_ + + - [x]* 23.4 编写角色默认路由确定性属性测试 + - **Property 81: 角色默认路由确定性** + - **Validates: Requirements 25.1, 25.2, 25.3, 25.5** + + - [x]* 23.5 编写默认视图首屏角色入口可见组件测试 + - 验证各角色默认视图首屏呈现与其角色匹配的功能入口 + - _Requirements: 25.4_ + +- [x] 24. 前端 UI 可视化回归与性能检查(非属性化补充) + - [x]* 24.1 编写可视化回归基线快照测试 + - 对关键页面与全套图表建立基线快照,覆盖 Light/Dark 双主题与各断点(≥1280/768-1279/<768)布局,验证排版/配色/间距视觉一致与图表标签不被遮挡 + - _Requirements: 19.1, 20.3_ + + - [x]* 24.2 编写主题切换与导出性能检查 + - 验证主题切换在 1 秒内对当前页面生效、报告导出在 30 秒内给出完成或失败终态 + - _Requirements: 19.7, 24.4_ + +- [x] 25. 最终 Checkpoint - 确保领域引擎与前端 UI 全部测试通过 + - Ensure all tests pass, ask the user if questions arise. + +## Notes + +- 标记 `*` 的子任务为可选测试任务,可为更快的 MVP 跳过;核心实现任务不可跳过。 +- 每条 Correctness Property 由单个属性化测试实现,共 81 条(领域逻辑 62 条 + Req 19-25 UI 可属性化 19 条,即 Property 63-81),全部映射到对应实现任务并紧邻放置。 +- 属性测试使用 fast-check、每条至少运行 100 次迭代、标注标签 `Feature: outsourcing-risk-assessment, Property {n}: {property_text}`。 +- 非属性类标准由单元测试与集成测试覆盖:领域侧(语义识别、外部 IO、导出时延、知识库分区扩展、持久化失败、架构解耦);前端侧(组件一致性、单一图标集、全套图表渲染、一键入口、未保存离开确认、默认视图首屏角色入口)。 +- 前端 UI 不适合属性化的标准由组件测试、可视化回归(Light/Dark + 各断点基线快照)、axe 自动化可访问性检查与性能检查(主题切换 1 秒内、导出 30 秒内)覆盖。 +- 前端任务(18-24)自底向上:先 Design_System(令牌/主题/组件库)→ 可视化组件 → Wizard 流程 → 响应式 → 可访问性 → 操作反馈与角色路由,复用领域引擎(任务 4 评分/热力图/Top N、任务 13 报告导出、任务 15 角色化视图)输出,避免重复。 +- 每个任务引用其对应需求与设计属性编号以保证可追溯性;Checkpoint 用于增量验证。 + +## Task Dependency Graph + +```json +{ + "waves": [ + { "id": 0, "tasks": ["1.1"] }, + { "id": 1, "tasks": ["1.2"] }, + { "id": 2, "tasks": ["1.3", "2.1", "4.1", "9.1", "10.1", "11.1", "12.1", "14.1"] }, + { "id": 3, "tasks": ["1.4", "2.2", "2.3", "2.4", "4.2", "4.3", "9.2", "9.3", "9.4", "9.5", "9.9", "10.2", "10.3", "10.4", "11.2", "11.3", "12.2", "12.3", "14.2", "14.3", "15.1"] }, + { "id": 4, "tasks": ["2.5", "2.6", "2.7", "4.4", "4.5", "4.6", "4.7", "9.6", "9.7", "10.5", "10.6", "10.7", "12.4", "12.5", "14.4", "14.5", "15.2", "15.3", "15.9"] }, + { "id": 5, "tasks": ["2.8", "2.9", "4.8", "4.9", "9.8", "14.6", "14.7"] }, + { "id": 6, "tasks": ["2.10", "2.11", "2.12", "4.10", "4.11", "4.12", "8.1", "14.8", "14.9"] }, + { "id": 7, "tasks": ["2.13", "2.14", "2.15", "2.16", "4.13", "4.14", "4.15", "8.2", "8.3"] }, + { "id": 8, "tasks": ["4.16", "4.17", "8.4", "8.5"] }, + { "id": 9, "tasks": ["4.18", "6.1", "7.1", "8.6", "8.7", "8.8"] }, + { "id": 10, "tasks": ["6.2", "6.3", "6.4", "6.5", "6.6", "7.2", "7.3", "8.9"] }, + { "id": 11, "tasks": ["7.4", "7.5"] }, + { "id": 12, "tasks": ["7.6"] }, + { "id": 13, "tasks": ["13.1"] }, + { "id": 14, "tasks": ["13.2", "13.3", "13.4", "15.4"] }, + { "id": 15, "tasks": ["13.5", "13.6", "13.7", "15.5", "15.6"] }, + { "id": 16, "tasks": ["13.8", "13.9", "15.7", "15.8"] }, + { "id": 17, "tasks": ["13.10"] }, + { "id": 18, "tasks": ["16.1"] }, + { "id": 19, "tasks": ["16.2"] }, + { "id": 20, "tasks": ["18.1"] }, + { "id": 21, "tasks": ["18.2"] }, + { "id": 22, "tasks": ["18.3", "18.4", "18.5", "18.6", "18.8"] }, + { "id": 23, "tasks": ["18.7", "18.9", "19.1"] }, + { "id": 24, "tasks": ["19.2", "19.3", "19.4", "19.5"] }, + { "id": 25, "tasks": ["19.6", "19.7", "19.8", "19.9", "19.10", "19.11", "19.12", "20.1"] }, + { "id": 26, "tasks": ["20.2", "20.3", "20.4", "21.1", "22.1", "22.2", "23.1", "23.3"] }, + { "id": 27, "tasks": ["20.5", "20.6", "20.7", "20.8", "21.2", "21.3", "22.3", "22.4", "22.5", "23.2", "23.4", "23.5"] }, + { "id": 28, "tasks": ["24.1", "24.2"] } + ] +} +``` diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/baidu-backup.sh b/baidu-backup.sh new file mode 100755 index 0000000..1ed33a6 --- /dev/null +++ b/baidu-backup.sh @@ -0,0 +1,349 @@ +#!/bin/bash +# ============================================================ +# 百度网盘备份脚本 +# 用法: ./baidu-backup.sh [目标目录] +# 示例: ./baidu-backup.sh /2026/0517 +# ./baidu-backup.sh # 自动使用 /年份/月日 +# ============================================================ + +set -e + +# ---- 配置 ---- +APP_KEY="z3gemBZfg7KYj6U3eHNfIzTs7uYS9OMh" +SECRET_KEY="ptCKj2DfxL0KtGR1pM08c9KO2t2UC7SR" +TOKEN_FILE="$HOME/.baidu_pan_token.json" +BLOCK_SIZE=$((4 * 1024 * 1024)) # 4MB + +# ---- 自动识别项目 ---- +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_NAME="$(basename "$SCRIPT_DIR")" +PARENT_DIR="$(dirname "$SCRIPT_DIR")" +ZIP_FILE="/tmp/${PROJECT_NAME}.zip" +MD5_FILE="/tmp/baidu_md5_list.txt" + +# ---- 目标目录 ---- +if [ -n "$1" ]; then + REMOTE_DIR="$1" +else + REMOTE_DIR="/$(date +%Y)/$(date +%m%d)" +fi +REMOTE_PATH="${REMOTE_DIR}/${PROJECT_NAME}.zip" + +echo "" +echo "╔══════════════════════════════════════════╗" +echo "║ 📦 百度网盘备份工具 ║" +echo "╚══════════════════════════════════════════╝" +echo "" +echo " 项目: $PROJECT_NAME" +echo " 目标: $REMOTE_PATH" +echo "" + +# ============================================================ +# 步骤1: 打包 +# ============================================================ +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "[1/5] 📦 打包项目文件..." +rm -f "$ZIP_FILE" +cd "$PARENT_DIR" + +# 先统计文件总数 +TOTAL_FILES=$(find "$PROJECT_NAME" -not -path "${PROJECT_NAME}/.git/*" -type f | wc -l | tr -d ' ') +echo " 📊 共 ${TOTAL_FILES} 个文件" + +# 打包并实时显示百分比 +COUNTER=0 +zip -r "$ZIP_FILE" "$PROJECT_NAME" -x "${PROJECT_NAME}/.git/*" 2>&1 | while IFS= read -r line; do + COUNTER=$((COUNTER + 1)) + PCT=$((COUNTER * 100 / TOTAL_FILES)) + if [ $PCT -gt 100 ]; then PCT=100; fi + # 进度条 + BAR_FILLED=$((PCT * 30 / 100)) + BAR_EMPTY=$((30 - BAR_FILLED)) + BAR="" + for ((b=0; b/dev/null || stat -c%s "$ZIP_FILE" 2>/dev/null) +FILE_SIZE_MB=$((FILE_SIZE / 1024 / 1024)) +echo " ✅ 打包完成: ${FILE_SIZE_MB}MB" + +# ============================================================ +# 步骤2: 获取 Token(缓存 / 刷新 / 设备授权) +# ============================================================ +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "[2/5] 🔑 获取百度网盘授权..." + +ACCESS_TOKEN="" + +# 尝试读取缓存 +if [ -f "$TOKEN_FILE" ]; then + CACHED=$(python3 -c " +import json, time, sys +with open('$TOKEN_FILE') as f: + d = json.load(f) +if time.time() < d.get('expires_at', 0): + print('VALID|' + d['access_token']) +else: + print('EXPIRED|' + d.get('refresh_token', '')) +" 2>/dev/null || echo "FAIL|") + + STATUS="${CACHED%%|*}" + VALUE="${CACHED#*|}" + + if [ "$STATUS" = "VALID" ]; then + ACCESS_TOKEN="$VALUE" + echo " ✅ 使用缓存 Token(有效)" + elif [ "$STATUS" = "EXPIRED" ] && [ -n "$VALUE" ]; then + echo " ⏳ Token 已过期,尝试刷新..." + RESP=$(curl -s -X POST "https://openapi.baidu.com/oauth/2.0/token" \ + -d "grant_type=refresh_token&refresh_token=${VALUE}&client_id=${APP_KEY}&client_secret=${SECRET_KEY}") + NEW_TOKEN=$(echo "$RESP" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('access_token',''))" 2>/dev/null) + if [ -n "$NEW_TOKEN" ]; then + ACCESS_TOKEN="$NEW_TOKEN" + # 更新缓存 + python3 -c " +import json, time +d = json.loads('$RESP'.replace(\"'\", '\"')) +token = {'access_token': d['access_token'], 'refresh_token': d['refresh_token'], 'expires_at': int(time.time()) + d['expires_in']} +with open('$TOKEN_FILE', 'w') as f: json.dump(token, f) +" 2>/dev/null + echo " ✅ Token 刷新成功" + else + echo " ⚠️ 刷新失败,需要重新授权" + fi + fi +fi + +# 如果没有有效 Token,走设备授权流程 +if [ -z "$ACCESS_TOKEN" ]; then + echo " 🔐 需要设备授权..." + DEVICE_RESP=$(curl -s -X POST "https://openapi.baidu.com/oauth/2.0/device/code" \ + -d "response_type=device_code&client_id=${APP_KEY}&scope=basic,netdisk") + + DEVICE_CODE=$(echo "$DEVICE_RESP" | python3 -c "import sys,json;print(json.load(sys.stdin)['device_code'])") + USER_CODE=$(echo "$DEVICE_RESP" | python3 -c "import sys,json;print(json.load(sys.stdin)['user_code'])") + QRCODE_URL=$(echo "$DEVICE_RESP" | python3 -c "import sys,json;print(json.load(sys.stdin)['qrcode_url'])") + + echo "" + echo " ┌─────────────────────────────────────┐" + echo " │ 请打开: https://openapi.baidu.com/device" + echo " │ 输入码: $USER_CODE" + echo " │ 或扫码: $QRCODE_URL" + echo " └─────────────────────────────────────┘" + echo "" + read -p " 授权完成后按回车继续..." _ + + TOKEN_RESP=$(curl -s -X POST "https://openapi.baidu.com/oauth/2.0/token" \ + -d "grant_type=device_token&code=${DEVICE_CODE}&client_id=${APP_KEY}&client_secret=${SECRET_KEY}") + ACCESS_TOKEN=$(echo "$TOKEN_RESP" | python3 -c "import sys,json;print(json.load(sys.stdin).get('access_token',''))" 2>/dev/null) + + if [ -z "$ACCESS_TOKEN" ]; then + echo " ❌ 授权失败: $TOKEN_RESP" + rm -f "$ZIP_FILE" + exit 1 + fi + + # 保存缓存 + echo "$TOKEN_RESP" | python3 -c " +import sys, json, time +d = json.load(sys.stdin) +token = {'access_token': d['access_token'], 'refresh_token': d['refresh_token'], 'expires_at': int(time.time()) + d['expires_in']} +with open('$TOKEN_FILE', 'w') as f: json.dump(token, f) +" + echo " ✅ 授权成功,Token 已缓存" +fi + +# ============================================================ +# 步骤3: 预创建文件 +# ============================================================ +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +BLOCKS=$(( (FILE_SIZE + BLOCK_SIZE - 1) / BLOCK_SIZE )) +echo "[3/5] 📋 预创建文件(${BLOCKS}个分片)..." +BLOCK_LIST=$(python3 -c "import json; print(json.dumps(['0'*32]*$BLOCKS))") +PRE_RESP=$(curl -s "https://pan.baidu.com/rest/2.0/xpan/file?method=precreate&access_token=${ACCESS_TOKEN}" \ + -d "path=${REMOTE_PATH}&size=${FILE_SIZE}&isdir=0&autoinit=1&block_list=${BLOCK_LIST}") +UPLOAD_ID=$(echo "$PRE_RESP" | python3 -c "import sys,json;print(json.load(sys.stdin).get('uploadid',''))" 2>/dev/null) +if [ -z "$UPLOAD_ID" ]; then + echo " ❌ 预创建失败: $PRE_RESP" + rm -f "$ZIP_FILE" + exit 1 +fi +echo " ✅ 预创建成功" + +# ============================================================ +# 步骤4: 分片上传(并发) +# ============================================================ +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +PARALLEL="${BAIDU_PARALLEL:-8}" +if [ "$PARALLEL" -gt "$BLOCKS" ]; then PARALLEL=$BLOCKS; fi +echo "[4/5] 🚀 上传中(${PARALLEL} 路并发)..." +echo "" + +UNSORTED_MD5="/tmp/baidu_md5_unsorted_${UPLOAD_ID}.txt" +PROGRESS_FILE="/tmp/baidu_progress_${UPLOAD_ID}.txt" +ERR_FILE="/tmp/baidu_err_${UPLOAD_ID}.txt" +DONE_FLAG="/tmp/baidu_done_${UPLOAD_ID}" +: > "$UNSORTED_MD5" +: > "$PROGRESS_FILE" +: > "$ERR_FILE" +rm -f "$DONE_FLAG" +: > "$MD5_FILE" +START_TIME=$(date +%s) + +# 单分片上传 worker(并发安全:单行 echo < PIPE_BUF 是原子写) +upload_part() { + local i=$1 + local chunk="/tmp/baidu_chunk_${UPLOAD_ID}_${i}" + dd if="$ZIP_FILE" bs="$BLOCK_SIZE" skip="$i" count=1 2>/dev/null > "$chunk" + local resp md5 attempt + md5="" + for attempt in 1 2 3; do + resp=$(curl -s --max-time 600 \ + "https://d.pcs.baidu.com/rest/2.0/pcs/superfile2?method=upload&access_token=${ACCESS_TOKEN}&type=tmpfile&path=${REMOTE_PATH}&uploadid=${UPLOAD_ID}&partseq=${i}" \ + -F "file=@${chunk}") + md5=$(echo "$resp" | python3 -c "import sys,json;print(json.load(sys.stdin).get('md5',''))" 2>/dev/null) + [ -n "$md5" ] && break + sleep $((attempt * 2)) + done + rm -f "$chunk" + if [ -z "$md5" ]; then + echo "part ${i} failed after 3 attempts: ${resp}" >> "$ERR_FILE" + return 1 + fi + echo "${i}|${md5}" >> "$UNSORTED_MD5" + echo "x" >> "$PROGRESS_FILE" +} +export -f upload_part +export ZIP_FILE BLOCK_SIZE ACCESS_TOKEN REMOTE_PATH UPLOAD_ID +export UNSORTED_MD5 PROGRESS_FILE ERR_FILE + +# 后台进度刷新进程(每秒刷新一次) +( + while :; do + DONE=$(wc -l < "$PROGRESS_FILE" 2>/dev/null | tr -d ' ') + DONE=${DONE:-0} + if [ "$DONE" -gt "$BLOCKS" ]; then DONE=$BLOCKS; fi + PCT=$((DONE * 100 / BLOCKS)) + UPLOADED_MB=$((DONE * BLOCK_SIZE / 1024 / 1024)) + if [ $UPLOADED_MB -gt $FILE_SIZE_MB ]; then UPLOADED_MB=$FILE_SIZE_MB; fi + NOW=$(date +%s) + ELAPSED=$((NOW - START_TIME)) + if [ $ELAPSED -gt 0 ] && [ $DONE -gt 0 ]; then + SPEED_KB=$((DONE * BLOCK_SIZE / 1024 / ELAPSED)) + SPEED_INT=$((SPEED_KB / 1024)) + SPEED_DEC=$(( (SPEED_KB % 1024) * 10 / 1024 )) + if [ "$SPEED_KB" -gt 0 ]; then + REMAINING_KB=$(( (FILE_SIZE - DONE * BLOCK_SIZE) / 1024 )) + if [ $REMAINING_KB -lt 0 ]; then REMAINING_KB=0; fi + ETA=$(( REMAINING_KB / SPEED_KB )) + ETA_MIN=$((ETA / 60)) + ETA_SEC=$((ETA % 60)) + ETA_STR="${ETA_MIN}m${ETA_SEC}s" + else + ETA_STR="计算中" + fi + else + SPEED_INT=0 + SPEED_DEC=0 + ETA_STR="计算中" + fi + BAR_FILLED=$((PCT * 30 / 100)) + BAR_EMPTY=$((30 - BAR_FILLED)) + BAR="" + for ((b=0; b/dev/null || true +echo "" + +if [ "$XARGS_RC" -ne 0 ]; then + echo "" + echo " ❌ 分片上传失败:" + if [ -s "$ERR_FILE" ]; then + head -n 5 "$ERR_FILE" + fi + rm -f "$ZIP_FILE" "$MD5_FILE" "$UNSORTED_MD5" "$PROGRESS_FILE" "$ERR_FILE" "$DONE_FLAG" + rm -f /tmp/baidu_chunk_${UPLOAD_ID}_* + exit 1 +fi + +# 校验分片数 +DONE_COUNT=$(wc -l < "$UNSORTED_MD5" | tr -d ' ') +if [ "$DONE_COUNT" != "$BLOCKS" ]; then + echo "" + echo " ❌ 分片数不匹配: ${DONE_COUNT}/${BLOCKS}" + if [ -s "$ERR_FILE" ]; then + head -n 5 "$ERR_FILE" + fi + rm -f "$ZIP_FILE" "$MD5_FILE" "$UNSORTED_MD5" "$PROGRESS_FILE" "$ERR_FILE" "$DONE_FLAG" + rm -f /tmp/baidu_chunk_${UPLOAD_ID}_* + exit 1 +fi + +# 按 partseq 升序生成最终 MD5_FILE +sort -t'|' -k1n "$UNSORTED_MD5" | cut -d'|' -f2 > "$MD5_FILE" +rm -f "$UNSORTED_MD5" "$PROGRESS_FILE" "$ERR_FILE" "$DONE_FLAG" + +echo "" +echo " ✅ 所有分片上传完成" + +# ============================================================ +# 步骤5: 合并文件 +# ============================================================ +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "[5/5] 🔗 合并文件..." +MD5_LIST=$(awk '{printf "\"%s\",", $0}' "$MD5_FILE" | sed 's/,$//') +CREATE_RESP=$(curl -s "https://pan.baidu.com/rest/2.0/xpan/file?method=create&access_token=${ACCESS_TOKEN}" \ + -d "path=${REMOTE_PATH}&size=${FILE_SIZE}&isdir=0&uploadid=${UPLOAD_ID}&block_list=[${MD5_LIST}]") +CREATE_ERRNO=$(echo "$CREATE_RESP" | python3 -c "import sys,json;print(json.load(sys.stdin).get('errno',99))" 2>/dev/null) +FINAL_PATH=$(echo "$CREATE_RESP" | python3 -c "import sys,json;print(json.load(sys.stdin).get('path',''))" 2>/dev/null) + +# 清理 +rm -f "$ZIP_FILE" "$MD5_FILE" +rm -f /tmp/baidu_chunk_${UPLOAD_ID}_* 2>/dev/null || true + +# 计算耗时 +END_TIME=$(date +%s) +TOTAL=$((END_TIME - START_TIME)) +T_MIN=$((TOTAL / 60)) +T_SEC=$((TOTAL % 60)) + +# 输出结果 +echo "" +echo "╔══════════════════════════════════════════════╗" +if [ "$CREATE_ERRNO" = "0" ]; then + echo "║ 📦 百度网盘备份完成! ║" + echo "╠══════════════════════════════════════════════╣" + echo " 📁 路径: $FINAL_PATH" + echo " 📊 大小: ${FILE_SIZE_MB} MB" + echo " ⏱️ 耗时: ${T_MIN}分${T_SEC}秒" + echo " ✅ 状态: 上传成功" +else + echo "║ ❌ 合并失败 ║" + echo "╠══════════════════════════════════════════════╣" + echo " errno: $CREATE_ERRNO" + echo " 响应: $CREATE_RESP" +fi +echo "╚══════════════════════════════════════════════╝" +echo "" diff --git a/docs/demo-complete/01-01-sales-首页.png b/docs/demo-complete/01-01-sales-首页.png new file mode 100644 index 0000000..f07b642 Binary files /dev/null and b/docs/demo-complete/01-01-sales-首页.png differ diff --git a/docs/demo-complete/02-02-step1-立项.png b/docs/demo-complete/02-02-step1-立项.png new file mode 100644 index 0000000..b81189c Binary files /dev/null and b/docs/demo-complete/02-02-step1-立项.png differ diff --git a/docs/demo-complete/03-03-step2-描述提交.png b/docs/demo-complete/03-03-step2-描述提交.png new file mode 100644 index 0000000..c7353af Binary files /dev/null and b/docs/demo-complete/03-03-step2-描述提交.png differ diff --git a/docs/demo-complete/04-04-step3-LLM分类+相似项目.png b/docs/demo-complete/04-04-step3-LLM分类+相似项目.png new file mode 100644 index 0000000..46f8c71 Binary files /dev/null and b/docs/demo-complete/04-04-step3-LLM分类+相似项目.png differ diff --git a/docs/demo-complete/05-05-step4-指标LLM预填.png b/docs/demo-complete/05-05-step4-指标LLM预填.png new file mode 100644 index 0000000..d4589dd Binary files /dev/null and b/docs/demo-complete/05-05-step4-指标LLM预填.png differ diff --git a/docs/demo-complete/06-06-step5-报价填写.png b/docs/demo-complete/06-06-step5-报价填写.png new file mode 100644 index 0000000..dfdbd00 Binary files /dev/null and b/docs/demo-complete/06-06-step5-报价填写.png differ diff --git a/docs/demo-complete/07-07-评估完成-详情顶部.png b/docs/demo-complete/07-07-评估完成-详情顶部.png new file mode 100644 index 0000000..9e0fc23 Binary files /dev/null and b/docs/demo-complete/07-07-评估完成-详情顶部.png differ diff --git a/docs/demo-complete/08-08-盈利分析+曲线.png b/docs/demo-complete/08-08-盈利分析+曲线.png new file mode 100644 index 0000000..b25fdb4 Binary files /dev/null and b/docs/demo-complete/08-08-盈利分析+曲线.png differ diff --git a/docs/demo-complete/09-09-附件+评分明细.png b/docs/demo-complete/09-09-附件+评分明细.png new file mode 100644 index 0000000..063bb25 Binary files /dev/null and b/docs/demo-complete/09-09-附件+评分明细.png differ diff --git a/docs/demo-complete/10-10-risk-首页待办.png b/docs/demo-complete/10-10-risk-首页待办.png new file mode 100644 index 0000000..6839a27 Binary files /dev/null and b/docs/demo-complete/10-10-risk-首页待办.png differ diff --git a/docs/demo-complete/11-11-risk-详情页底部审核区.png b/docs/demo-complete/11-11-risk-详情页底部审核区.png new file mode 100644 index 0000000..582c337 Binary files /dev/null and b/docs/demo-complete/11-11-risk-详情页底部审核区.png differ diff --git a/docs/demo-complete/12-12-risk-审核通过.png b/docs/demo-complete/12-12-risk-审核通过.png new file mode 100644 index 0000000..3e5368d Binary files /dev/null and b/docs/demo-complete/12-12-risk-审核通过.png differ diff --git a/docs/demo-complete/13-13-mgmt-首页看板.png b/docs/demo-complete/13-13-mgmt-首页看板.png new file mode 100644 index 0000000..7c37ab3 Binary files /dev/null and b/docs/demo-complete/13-13-mgmt-首页看板.png differ diff --git a/docs/demo-complete/14-14-mgmt-详情页审批区.png b/docs/demo-complete/14-14-mgmt-详情页审批区.png new file mode 100644 index 0000000..f36ed3c Binary files /dev/null and b/docs/demo-complete/14-14-mgmt-详情页审批区.png differ diff --git a/docs/demo-complete/15-15-mgmt-审批通过.png b/docs/demo-complete/15-15-mgmt-审批通过.png new file mode 100644 index 0000000..e5af6e1 Binary files /dev/null and b/docs/demo-complete/15-15-mgmt-审批通过.png differ diff --git a/docs/demo-complete/16-16-完整审计时间线.png b/docs/demo-complete/16-16-完整审计时间线.png new file mode 100644 index 0000000..02212c4 Binary files /dev/null and b/docs/demo-complete/16-16-完整审计时间线.png differ diff --git a/docs/demo-complete/17-17-mgmt-首页列表-归档按钮.png b/docs/demo-complete/17-17-mgmt-首页列表-归档按钮.png new file mode 100644 index 0000000..9005f61 Binary files /dev/null and b/docs/demo-complete/17-17-mgmt-首页列表-归档按钮.png differ diff --git a/docs/demo-complete/18-18-费率管理页.png b/docs/demo-complete/18-18-费率管理页.png new file mode 100644 index 0000000..bd6f584 Binary files /dev/null and b/docs/demo-complete/18-18-费率管理页.png differ diff --git a/docs/demo-complete/19-19-红线管理页.png b/docs/demo-complete/19-19-红线管理页.png new file mode 100644 index 0000000..b4f97c8 Binary files /dev/null and b/docs/demo-complete/19-19-红线管理页.png differ diff --git a/docs/demo-complete/20-20-客户档案页.png b/docs/demo-complete/20-20-客户档案页.png new file mode 100644 index 0000000..4e508f7 Binary files /dev/null and b/docs/demo-complete/20-20-客户档案页.png differ diff --git a/docs/demo-complete/complete-flow.mp4 b/docs/demo-complete/complete-flow.mp4 new file mode 100644 index 0000000..5ea31ea Binary files /dev/null and b/docs/demo-complete/complete-flow.mp4 differ diff --git a/docs/demo-complete/complete-flow.webm b/docs/demo-complete/complete-flow.webm new file mode 100644 index 0000000..af9b169 Binary files /dev/null and b/docs/demo-complete/complete-flow.webm differ diff --git a/docs/demo-full/01-sales-dashboard.png b/docs/demo-full/01-sales-dashboard.png new file mode 100644 index 0000000..a95ad34 Binary files /dev/null and b/docs/demo-full/01-sales-dashboard.png differ diff --git a/docs/demo-full/02-step1-立项.png b/docs/demo-full/02-step1-立项.png new file mode 100644 index 0000000..a7fd872 Binary files /dev/null and b/docs/demo-full/02-step1-立项.png differ diff --git a/docs/demo-full/03-step2-描述.png b/docs/demo-full/03-step2-描述.png new file mode 100644 index 0000000..c99baca Binary files /dev/null and b/docs/demo-full/03-step2-描述.png differ diff --git a/docs/demo-full/04-step3-分类-相似项目.png b/docs/demo-full/04-step3-分类-相似项目.png new file mode 100644 index 0000000..ff69dba Binary files /dev/null and b/docs/demo-full/04-step3-分类-相似项目.png differ diff --git a/docs/demo-full/05-step4-指标预填.png b/docs/demo-full/05-step4-指标预填.png new file mode 100644 index 0000000..e632e13 Binary files /dev/null and b/docs/demo-full/05-step4-指标预填.png differ diff --git a/docs/demo-full/06-step5-报价.png b/docs/demo-full/06-step5-报价.png new file mode 100644 index 0000000..d926018 Binary files /dev/null and b/docs/demo-full/06-step5-报价.png differ diff --git a/docs/demo-full/07-detail-top.png b/docs/demo-full/07-detail-top.png new file mode 100644 index 0000000..63414c1 Binary files /dev/null and b/docs/demo-full/07-detail-top.png differ diff --git a/docs/demo-full/08-detail-盈利.png b/docs/demo-full/08-detail-盈利.png new file mode 100644 index 0000000..cf7f2b7 Binary files /dev/null and b/docs/demo-full/08-detail-盈利.png differ diff --git a/docs/demo-full/09-detail-附件区.png b/docs/demo-full/09-detail-附件区.png new file mode 100644 index 0000000..dd7d1e6 Binary files /dev/null and b/docs/demo-full/09-detail-附件区.png differ diff --git a/docs/demo-full/10-mgmt-dashboard-看板.png b/docs/demo-full/10-mgmt-dashboard-看板.png new file mode 100644 index 0000000..c6dbbac Binary files /dev/null and b/docs/demo-full/10-mgmt-dashboard-看板.png differ diff --git a/docs/demo-full/11-mgmt-费率管理.png b/docs/demo-full/11-mgmt-费率管理.png new file mode 100644 index 0000000..bd6f584 Binary files /dev/null and b/docs/demo-full/11-mgmt-费率管理.png differ diff --git a/docs/demo-full/12-mgmt-红线管理.png b/docs/demo-full/12-mgmt-红线管理.png new file mode 100644 index 0000000..b4f97c8 Binary files /dev/null and b/docs/demo-full/12-mgmt-红线管理.png differ diff --git a/docs/demo-full/13-mgmt-客户档案.png b/docs/demo-full/13-mgmt-客户档案.png new file mode 100644 index 0000000..4e508f7 Binary files /dev/null and b/docs/demo-full/13-mgmt-客户档案.png differ diff --git a/docs/demo-full/14-risk-驳回弹窗.png b/docs/demo-full/14-risk-驳回弹窗.png new file mode 100644 index 0000000..97a8cdd Binary files /dev/null and b/docs/demo-full/14-risk-驳回弹窗.png differ diff --git a/docs/demo-full/full-walkthrough.mp4 b/docs/demo-full/full-walkthrough.mp4 new file mode 100644 index 0000000..2fccbb5 Binary files /dev/null and b/docs/demo-full/full-walkthrough.mp4 differ diff --git a/docs/demo-full/full-walkthrough.webm b/docs/demo-full/full-walkthrough.webm new file mode 100644 index 0000000..31092a8 Binary files /dev/null and b/docs/demo-full/full-walkthrough.webm differ diff --git a/docs/demo/01-login.png b/docs/demo/01-login.png new file mode 100644 index 0000000..52b45ac Binary files /dev/null and b/docs/demo/01-login.png differ diff --git a/docs/demo/02-dashboard.png b/docs/demo/02-dashboard.png new file mode 100644 index 0000000..fd4a1fe Binary files /dev/null and b/docs/demo/02-dashboard.png differ diff --git a/docs/demo/03-step1-立项信息.png b/docs/demo/03-step1-立项信息.png new file mode 100644 index 0000000..9c96d93 Binary files /dev/null and b/docs/demo/03-step1-立项信息.png differ diff --git a/docs/demo/04-step2-项目描述.png b/docs/demo/04-step2-项目描述.png new file mode 100644 index 0000000..b289a36 Binary files /dev/null and b/docs/demo/04-step2-项目描述.png differ diff --git a/docs/demo/05-step3-业务确认-LLM分类.png b/docs/demo/05-step3-业务确认-LLM分类.png new file mode 100644 index 0000000..5cb44a0 Binary files /dev/null and b/docs/demo/05-step3-业务确认-LLM分类.png differ diff --git a/docs/demo/06-step4-指标补全-LLM预填.png b/docs/demo/06-step4-指标补全-LLM预填.png new file mode 100644 index 0000000..50e3e68 Binary files /dev/null and b/docs/demo/06-step4-指标补全-LLM预填.png differ diff --git a/docs/demo/07-step5-报价与成本.png b/docs/demo/07-step5-报价与成本.png new file mode 100644 index 0000000..6876f19 Binary files /dev/null and b/docs/demo/07-step5-报价与成本.png differ diff --git a/docs/demo/08-detail-顶部结论.png b/docs/demo/08-detail-顶部结论.png new file mode 100644 index 0000000..455a69c Binary files /dev/null and b/docs/demo/08-detail-顶部结论.png differ diff --git a/docs/demo/09-detail-分区.png b/docs/demo/09-detail-分区.png new file mode 100644 index 0000000..a6a727c Binary files /dev/null and b/docs/demo/09-detail-分区.png differ diff --git a/docs/demo/10-detail-分区.png b/docs/demo/10-detail-分区.png new file mode 100644 index 0000000..693948c Binary files /dev/null and b/docs/demo/10-detail-分区.png differ diff --git a/docs/demo/11-detail-分区.png b/docs/demo/11-detail-分区.png new file mode 100644 index 0000000..49e6d6f Binary files /dev/null and b/docs/demo/11-detail-分区.png differ diff --git a/docs/demo/12-detail-分区.png b/docs/demo/12-detail-分区.png new file mode 100644 index 0000000..2a1471f Binary files /dev/null and b/docs/demo/12-detail-分区.png differ diff --git a/docs/demo/13-detail-分区.png b/docs/demo/13-detail-分区.png new file mode 100644 index 0000000..124f684 Binary files /dev/null and b/docs/demo/13-detail-分区.png differ diff --git a/docs/demo/14-detail-分区.png b/docs/demo/14-detail-分区.png new file mode 100644 index 0000000..e13ffa8 Binary files /dev/null and b/docs/demo/14-detail-分区.png differ diff --git a/docs/demo/15-detail-分区.png b/docs/demo/15-detail-分区.png new file mode 100644 index 0000000..645917f Binary files /dev/null and b/docs/demo/15-detail-分区.png differ diff --git a/docs/demo/walkthrough.webm b/docs/demo/walkthrough.webm new file mode 100644 index 0000000..8b045e0 Binary files /dev/null and b/docs/demo/walkthrough.webm differ diff --git a/docs/demo/外包项目评估.mp4 b/docs/demo/外包项目评估.mp4 new file mode 100644 index 0000000..10358dc Binary files /dev/null and b/docs/demo/外包项目评估.mp4 differ diff --git a/docs/demo/外包项目评估Agent.mp4 b/docs/demo/外包项目评估Agent.mp4 new file mode 100644 index 0000000..b677834 Binary files /dev/null and b/docs/demo/外包项目评估Agent.mp4 differ diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..275fa33 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,47 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import reactPlugin from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; + +export default [ + { + ignores: ['dist/**', 'coverage/**', 'node_modules/**', 'web/dist/**'], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['src/**/*.ts'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + rules: { + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + }, + }, + { + files: ['web/**/*.{ts,tsx}'], + plugins: { + react: reactPlugin, + 'react-hooks': reactHooks, + }, + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + parserOptions: { + ecmaFeatures: { jsx: true }, + }, + }, + settings: { + react: { version: 'detect' }, + }, + rules: { + ...reactPlugin.configs.recommended.rules, + ...reactHooks.configs.recommended.rules, + // The automatic JSX runtime (jsx: "react-jsx") makes the React import + // unnecessary. + 'react/react-in-jsx-scope': 'off', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + }, + }, +]; diff --git a/migrations/1730000000000_init.cjs b/migrations/1730000000000_init.cjs new file mode 100644 index 0000000..3d6afde --- /dev/null +++ b/migrations/1730000000000_init.cjs @@ -0,0 +1,48 @@ +/* eslint-disable */ +/** + * 初始 schema:评估记录 / 工作流状态 / 操作记录 / 盈利分析。 + * 使用 IF NOT EXISTS 以与早期 initSchema 已建表的环境幂等兼容。 + */ + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS assessments ( + id TEXT PRIMARY KEY, + assessment JSONB NOT NULL, + report JSONB, + saved_at TIMESTAMPTZ NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_assessments_saved_at ON assessments(saved_at DESC); + + CREATE TABLE IF NOT EXISTS workflow_status ( + assessment_id TEXT PRIMARY KEY, + status TEXT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + + CREATE TABLE IF NOT EXISTS audit_logs ( + id BIGSERIAL PRIMARY KEY, + assessment_id TEXT NOT NULL, + role TEXT NOT NULL, + username TEXT NOT NULL, + action TEXT NOT NULL, + comment TEXT, + ts TIMESTAMPTZ NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_audit_assessment ON audit_logs(assessment_id); + + CREATE TABLE IF NOT EXISTS profitability ( + assessment_id TEXT PRIMARY KEY, + result JSONB NOT NULL + ); + `); +}; + +exports.down = (pgm) => { + pgm.sql(` + DROP TABLE IF EXISTS profitability; + DROP TABLE IF EXISTS audit_logs; + DROP TABLE IF EXISTS workflow_status; + DROP TABLE IF EXISTS assessments; + `); +}; diff --git a/migrations/1730000001000_recommendation.cjs b/migrations/1730000001000_recommendation.cjs new file mode 100644 index 0000000..6d65867 --- /dev/null +++ b/migrations/1730000001000_recommendation.cjs @@ -0,0 +1,20 @@ +/* eslint-disable */ +/** 综合承接建议持久化表。 */ + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS recommendations ( + assessment_id TEXT PRIMARY KEY, + level TEXT NOT NULL, + title TEXT NOT NULL, + note TEXT NOT NULL, + target_margin DOUBLE PRECISION NOT NULL, + net_margin DOUBLE PRECISION, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + `); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP TABLE IF EXISTS recommendations;`); +}; diff --git a/migrations/1730000002000_archived.cjs b/migrations/1730000002000_archived.cjs new file mode 100644 index 0000000..3fe6614 --- /dev/null +++ b/migrations/1730000002000_archived.cjs @@ -0,0 +1,12 @@ +/* eslint-disable */ +/** 评估归档标记:归档项不在主列表显示,可单独查看。 */ + +exports.up = (pgm) => { + pgm.sql(`ALTER TABLE assessments ADD COLUMN IF NOT EXISTS archived BOOLEAN NOT NULL DEFAULT false;`); + pgm.sql(`CREATE INDEX IF NOT EXISTS idx_assessments_archived ON assessments(archived);`); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP INDEX IF EXISTS idx_assessments_archived;`); + pgm.sql(`ALTER TABLE assessments DROP COLUMN IF EXISTS archived;`); +}; diff --git a/migrations/1730000003000_scenarios.cjs b/migrations/1730000003000_scenarios.cjs new file mode 100644 index 0000000..e0387a1 --- /dev/null +++ b/migrations/1730000003000_scenarios.cjs @@ -0,0 +1,21 @@ +/* eslint-disable */ +/** 多报价方案对比:每个评估最多 5 套报价方案。 */ + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS scenarios ( + id TEXT NOT NULL, + assessment_id TEXT NOT NULL REFERENCES assessments(id) ON DELETE CASCADE, + label TEXT NOT NULL, + inputs JSONB NOT NULL, + result JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (assessment_id, id) + ); + CREATE INDEX IF NOT EXISTS idx_scenarios_assessment ON scenarios(assessment_id); + `); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP TABLE IF EXISTS scenarios;`); +}; diff --git a/migrations/1730000004000_rate_tables.cjs b/migrations/1730000004000_rate_tables.cjs new file mode 100644 index 0000000..8df173a --- /dev/null +++ b/migrations/1730000004000_rate_tables.cjs @@ -0,0 +1,24 @@ +/* eslint-disable */ +/** 费率表后台化:消除硬编码,支持按地域/分类管理费率,版本化发布。 */ + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS rate_tables ( + id BIGSERIAL PRIMARY KEY, + region TEXT NOT NULL, + category TEXT NOT NULL, + key TEXT NOT NULL, + value DOUBLE PRECISION NOT NULL, + effective_date DATE NOT NULL DEFAULT CURRENT_DATE, + version INT NOT NULL DEFAULT 1, + reviewed BOOLEAN NOT NULL DEFAULT false, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE(region, category, key, version) + ); + CREATE INDEX IF NOT EXISTS idx_rate_tables_region ON rate_tables(region, category); + `); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP TABLE IF EXISTS rate_tables;`); +}; diff --git a/migrations/1730000005000_actuals.cjs b/migrations/1730000005000_actuals.cjs new file mode 100644 index 0000000..bcdd089 --- /dev/null +++ b/migrations/1730000005000_actuals.cjs @@ -0,0 +1,21 @@ +/* eslint-disable */ +/** 运营指标实际值回填(评估闭环)。 */ + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS actuals ( + id BIGSERIAL PRIMARY KEY, + assessment_id TEXT NOT NULL REFERENCES assessments(id) ON DELETE CASCADE, + month INT NOT NULL, + metric_name TEXT NOT NULL, + actual_value DOUBLE PRECISION NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE(assessment_id, month, metric_name) + ); + CREATE INDEX IF NOT EXISTS idx_actuals_assessment ON actuals(assessment_id); + `); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP TABLE IF EXISTS actuals;`); +}; diff --git a/migrations/1730000006000_redline_rules.cjs b/migrations/1730000006000_redline_rules.cjs new file mode 100644 index 0000000..5d632b4 --- /dev/null +++ b/migrations/1730000006000_redline_rules.cjs @@ -0,0 +1,26 @@ +/* eslint-disable */ +/** 合规红线库(可配置):按地域/业务类型条件启用,版本化管理。 */ + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS redline_rules ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + trigger_condition TEXT NOT NULL, + consequence TEXT NOT NULL, + region TEXT, + business_type TEXT, + enabled BOOLEAN NOT NULL DEFAULT true, + version INT NOT NULL DEFAULT 1, + regulation_ref TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + CREATE INDEX IF NOT EXISTS idx_redline_rules_region ON redline_rules(region); + CREATE INDEX IF NOT EXISTS idx_redline_rules_biz ON redline_rules(business_type); + `); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP TABLE IF EXISTS redline_rules;`); +}; diff --git a/migrations/1730000007000_customers.cjs b/migrations/1730000007000_customers.cjs new file mode 100644 index 0000000..98e650e --- /dev/null +++ b/migrations/1730000007000_customers.cjs @@ -0,0 +1,23 @@ +/* eslint-disable */ +/** 客户信用与集中度风险:客户档案表。 */ + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS customers ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + credit_rating TEXT NOT NULL DEFAULT '未评级', + avg_overdue_days DOUBLE PRECISION NOT NULL DEFAULT 0, + total_contract_amount DOUBLE PRECISION NOT NULL DEFAULT 0, + assessment_count INT NOT NULL DEFAULT 0, + notes TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + CREATE INDEX IF NOT EXISTS idx_customers_name ON customers(name); + `); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP TABLE IF EXISTS customers;`); +}; diff --git a/migrations/1730000008000_expires_at.cjs b/migrations/1730000008000_expires_at.cjs new file mode 100644 index 0000000..8e45b66 --- /dev/null +++ b/migrations/1730000008000_expires_at.cjs @@ -0,0 +1,13 @@ +/* eslint-disable */ +/** 评估有效期:默认 6 个月,到期可一键复评。 */ + +exports.up = (pgm) => { + pgm.sql(`ALTER TABLE assessments ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ;`); + pgm.sql(`UPDATE assessments SET expires_at = saved_at + INTERVAL '6 months' WHERE expires_at IS NULL;`); + pgm.sql(`CREATE INDEX IF NOT EXISTS idx_assessments_expires ON assessments(expires_at);`); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP INDEX IF EXISTS idx_assessments_expires;`); + pgm.sql(`ALTER TABLE assessments DROP COLUMN IF EXISTS expires_at;`); +}; diff --git a/migrations/1730000009000_sla.cjs b/migrations/1730000009000_sla.cjs new file mode 100644 index 0000000..4e56acb --- /dev/null +++ b/migrations/1730000009000_sla.cjs @@ -0,0 +1,10 @@ +/* eslint-disable */ +/** 审批SLA:记录进入当前状态的时间,供超时计算。 */ + +exports.up = (pgm) => { + pgm.sql(`ALTER TABLE workflow_status ADD COLUMN IF NOT EXISTS entered_at TIMESTAMPTZ NOT NULL DEFAULT now();`); +}; + +exports.down = (pgm) => { + pgm.sql(`ALTER TABLE workflow_status DROP COLUMN IF EXISTS entered_at;`); +}; diff --git a/migrations/1730000010000_reject_reasons.cjs b/migrations/1730000010000_reject_reasons.cjs new file mode 100644 index 0000000..b9ab5a5 --- /dev/null +++ b/migrations/1730000010000_reject_reasons.cjs @@ -0,0 +1,20 @@ +/* eslint-disable */ +/** 驳回原因结构化:枚举表 + 审计关联。 */ + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS reject_reasons ( + id BIGSERIAL PRIMARY KEY, + assessment_id TEXT NOT NULL, + reason_type TEXT NOT NULL, + detail TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + CREATE INDEX IF NOT EXISTS idx_reject_reasons_assessment ON reject_reasons(assessment_id); + CREATE INDEX IF NOT EXISTS idx_reject_reasons_type ON reject_reasons(reason_type); + `); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP TABLE IF EXISTS reject_reasons;`); +}; diff --git a/migrations/1730000011000_fts.cjs b/migrations/1730000011000_fts.cjs new file mode 100644 index 0000000..6e36f92 --- /dev/null +++ b/migrations/1730000011000_fts.cjs @@ -0,0 +1,13 @@ +/* eslint-disable */ +/** 全文检索:用 PG tsvector 做相似历史项目检索。 */ + +exports.up = (pgm) => { + pgm.sql(`ALTER TABLE assessments ADD COLUMN IF NOT EXISTS tsv tsvector;`); + pgm.sql(`UPDATE assessments SET tsv = to_tsvector('simple', COALESCE(assessment->>'projectDescription','') || ' ' || COALESCE(assessment->>'businessType','') || ' ' || COALESCE(assessment->>'industry',''));`); + pgm.sql(`CREATE INDEX IF NOT EXISTS idx_assessments_tsv ON assessments USING gin(tsv);`); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP INDEX IF EXISTS idx_assessments_tsv;`); + pgm.sql(`ALTER TABLE assessments DROP COLUMN IF EXISTS tsv;`); +}; diff --git a/migrations/1730000012000_attachments.cjs b/migrations/1730000012000_attachments.cjs new file mode 100644 index 0000000..0c79a5c --- /dev/null +++ b/migrations/1730000012000_attachments.cjs @@ -0,0 +1,23 @@ +/* eslint-disable */ +/** 附件管理:挂到评估/风险项,支持上传与审批时查证。 */ + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS attachments ( + id TEXT PRIMARY KEY, + assessment_id TEXT NOT NULL REFERENCES assessments(id) ON DELETE CASCADE, + risk_item_id TEXT, + filename TEXT NOT NULL, + mime_type TEXT NOT NULL DEFAULT 'application/octet-stream', + size_bytes BIGINT NOT NULL DEFAULT 0, + storage_path TEXT NOT NULL, + uploaded_by TEXT, + uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + CREATE INDEX IF NOT EXISTS idx_attachments_assessment ON attachments(assessment_id); + `); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP TABLE IF EXISTS attachments;`); +}; diff --git a/migrations/1730000013000_experience.cjs b/migrations/1730000013000_experience.cjs new file mode 100644 index 0000000..a70580c --- /dev/null +++ b/migrations/1730000013000_experience.cjs @@ -0,0 +1,26 @@ +/* eslint-disable */ +/** LLM 经验库:沉淀综合研判 + 人工修正后的最终结论,供后续 few-shot。 */ + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS experience_library ( + id BIGSERIAL PRIMARY KEY, + assessment_id TEXT NOT NULL, + business_type TEXT NOT NULL, + industry TEXT NOT NULL, + project_summary TEXT NOT NULL, + risk_grade TEXT, + acceptability TEXT, + recommendation_level TEXT, + lesson TEXT NOT NULL, + tags TEXT[] NOT NULL DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + CREATE INDEX IF NOT EXISTS idx_experience_biz ON experience_library(business_type); + CREATE INDEX IF NOT EXISTS idx_experience_tags ON experience_library USING gin(tags); + `); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP TABLE IF EXISTS experience_library;`); +}; diff --git a/migrations/1730000014000_embedding.cjs b/migrations/1730000014000_embedding.cjs new file mode 100644 index 0000000..db8b80b --- /dev/null +++ b/migrations/1730000014000_embedding.cjs @@ -0,0 +1,13 @@ +/* eslint-disable */ +/** 向量搜索:用 pgvector 存储项目描述的 embedding,做语义相似检索。 */ + +exports.up = (pgm) => { + pgm.sql(`CREATE EXTENSION IF NOT EXISTS vector;`); + pgm.sql(`ALTER TABLE assessments ADD COLUMN IF NOT EXISTS embedding vector(1024);`); + pgm.sql(`CREATE INDEX IF NOT EXISTS idx_assessments_embedding ON assessments USING ivfflat (embedding vector_cosine_ops) WITH (lists = 10);`); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP INDEX IF EXISTS idx_assessments_embedding;`); + pgm.sql(`ALTER TABLE assessments DROP COLUMN IF EXISTS embedding;`); +}; diff --git a/migrations/1730000015000_region_rates.cjs b/migrations/1730000015000_region_rates.cjs new file mode 100644 index 0000000..d460599 --- /dev/null +++ b/migrations/1730000015000_region_rates.cjs @@ -0,0 +1,18 @@ +/* eslint-disable */ +/** 地域费率套:与引擎 RegionRates 结构对齐,复核后驱动评估盈利测算。 */ + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS region_rates ( + region TEXT PRIMARY KEY, + rates JSONB NOT NULL, + reviewed BOOLEAN NOT NULL DEFAULT false, + updated_by TEXT, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + `); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP TABLE IF EXISTS region_rates;`); +}; diff --git a/migrations/1730000016000_redline_computable.cjs b/migrations/1730000016000_redline_computable.cjs new file mode 100644 index 0000000..4a2e8d7 --- /dev/null +++ b/migrations/1730000016000_redline_computable.cjs @@ -0,0 +1,37 @@ +/* eslint-disable */ +/** + * 红线可计算触发条件:将红线绑定到某项度量(月净利率/月毛利率/客户平均逾期天数/单客户集中度), + * 配合比较运算符与阈值,使红线可在评估时自动判定命中(而非一律"待核实")。 + * + * - linked_metric: 关联度量键(netMargin / grossMargin / avgOverdueDays / concentration), + * 为空表示该红线仍需人工核实(合规/资质类红线)。 + * - compare_op: 比较运算符(>=, <=, >, <)。 + * - threshold: 阈值;百分比类度量(净利率/毛利率/集中度)按百分数填写(如 0 表示 0%、5 表示 5%), + * 逾期天数按天填写。 + */ + +exports.up = (pgm) => { + pgm.sql(` + ALTER TABLE redline_rules ADD COLUMN IF NOT EXISTS linked_metric TEXT; + ALTER TABLE redline_rules ADD COLUMN IF NOT EXISTS compare_op TEXT; + ALTER TABLE redline_rules ADD COLUMN IF NOT EXISTS threshold DOUBLE PRECISION; + `); + + // 为内置数值型红线预置可计算条件(仅当字段为空时设置,避免覆盖人工配置)。 + pgm.sql(` + UPDATE redline_rules SET linked_metric='netMargin', compare_op='<', threshold=0 + WHERE id='negative-margin-3m' AND linked_metric IS NULL; + UPDATE redline_rules SET linked_metric='avgOverdueDays', compare_op='>', threshold=90 + WHERE id='overdue-90days' AND linked_metric IS NULL; + UPDATE redline_rules SET linked_metric='concentration', compare_op='>', threshold=50 + WHERE id='concentration-50pct' AND linked_metric IS NULL; + `); +}; + +exports.down = (pgm) => { + pgm.sql(` + ALTER TABLE redline_rules DROP COLUMN IF EXISTS linked_metric; + ALTER TABLE redline_rules DROP COLUMN IF EXISTS compare_op; + ALTER TABLE redline_rules DROP COLUMN IF EXISTS threshold; + `); +}; diff --git a/migrations/1730000017000_profitability_inputs.cjs b/migrations/1730000017000_profitability_inputs.cjs new file mode 100644 index 0000000..486ba71 --- /dev/null +++ b/migrations/1730000017000_profitability_inputs.cjs @@ -0,0 +1,13 @@ +/* eslint-disable */ +/** + * 保存盈利测算的原始输入(报价模式/合同周期/成本加成率/各项成本参数/岗位明细等), + * 以便编辑评估时完整带入原值(此前仅保存计算结果,导致加成率等输入项丢失)。 + */ + +exports.up = (pgm) => { + pgm.sql(`ALTER TABLE profitability ADD COLUMN IF NOT EXISTS inputs JSONB;`); +}; + +exports.down = (pgm) => { + pgm.sql(`ALTER TABLE profitability DROP COLUMN IF EXISTS inputs;`); +}; diff --git a/migrations/1730000018000_redline_compound.cjs b/migrations/1730000018000_redline_compound.cjs new file mode 100644 index 0000000..df9db09 --- /dev/null +++ b/migrations/1730000018000_redline_compound.cjs @@ -0,0 +1,21 @@ +/* eslint-disable */ +/** + * 红线复合条件:在主条件之外,支持一个可选的第二条件(与主条件 AND 组合)。 + * 例如「月毛利率 < 0 且 月净利率 < 0」同时满足才命中。 + */ + +exports.up = (pgm) => { + pgm.sql(` + ALTER TABLE redline_rules ADD COLUMN IF NOT EXISTS linked_metric2 TEXT; + ALTER TABLE redline_rules ADD COLUMN IF NOT EXISTS compare_op2 TEXT; + ALTER TABLE redline_rules ADD COLUMN IF NOT EXISTS threshold2 DOUBLE PRECISION; + `); +}; + +exports.down = (pgm) => { + pgm.sql(` + ALTER TABLE redline_rules DROP COLUMN IF EXISTS linked_metric2; + ALTER TABLE redline_rules DROP COLUMN IF EXISTS compare_op2; + ALTER TABLE redline_rules DROP COLUMN IF EXISTS threshold2; + `); +}; diff --git a/migrations/1730000019000_app_settings.cjs b/migrations/1730000019000_app_settings.cjs new file mode 100644 index 0000000..9980cb7 --- /dev/null +++ b/migrations/1730000019000_app_settings.cjs @@ -0,0 +1,18 @@ +/* eslint-disable */ +/** + * 应用级设置(键值):用于持久化可调参数,如目标净利率基准(由预测准确度校准)。 + */ + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS app_settings ( + key TEXT PRIMARY KEY, + value DOUBLE PRECISION NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + `); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP TABLE IF EXISTS app_settings;`); +}; diff --git a/migrations/1730000020000_customer_payments.cjs b/migrations/1730000020000_customer_payments.cjs new file mode 100644 index 0000000..89cfef6 --- /dev/null +++ b/migrations/1730000020000_customer_payments.cjs @@ -0,0 +1,24 @@ +/* eslint-disable */ +/** + * 客户回款记录:记录应收发票的到期日与实际回款日,用于自动计算客户平均逾期天数, + * 替代人工维护的 avg_overdue_days,并驱动"客户逾期超N天"红线。 + */ + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS customer_payments ( + id SERIAL PRIMARY KEY, + customer_id TEXT NOT NULL REFERENCES customers(id) ON DELETE CASCADE, + invoice_amount DOUBLE PRECISION NOT NULL DEFAULT 0, + due_date DATE NOT NULL, + paid_date DATE, + note TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + CREATE INDEX IF NOT EXISTS idx_customer_payments_cust ON customer_payments(customer_id); + `); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP TABLE IF EXISTS customer_payments;`); +}; diff --git a/migrations/1730000021000_min_wages.cjs b/migrations/1730000021000_min_wages.cjs new file mode 100644 index 0000000..9d89022 --- /dev/null +++ b/migrations/1730000021000_min_wages.cjs @@ -0,0 +1,24 @@ +/* eslint-disable */ +/** + * 各地域月最低工资标准(可后台维护):驱动"低于最低工资"红线自动比对。 + * 预置近似默认值,须经 HR/财务按当地官方标准复核更新。 + */ + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS min_wages ( + region TEXT PRIMARY KEY, + monthly_wage DOUBLE PRECISION NOT NULL, + updated_by TEXT, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + INSERT INTO min_wages(region, monthly_wage) VALUES + ('北京', 2420), ('上海', 2690), ('广东', 1900), ('深圳', 2360), + ('江苏', 2490), ('浙江', 2490), ('四川', 2100), ('河北', 2200), ('中国大陆', 2000) + ON CONFLICT(region) DO NOTHING; + `); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP TABLE IF EXISTS min_wages;`); +}; diff --git a/migrations/1730000022000_wizard_drafts.cjs b/migrations/1730000022000_wizard_drafts.cjs new file mode 100644 index 0000000..9c25d06 --- /dev/null +++ b/migrations/1730000022000_wizard_drafts.cjs @@ -0,0 +1,25 @@ +/* eslint-disable */ +/** + * 评估向导草稿(服务端持久化,跨设备):保存未运行/未提交的向导填写进度。 + * - source_assessment_id 为空:全新建评估的草稿 + * - source_assessment_id 非空:编辑某既有评估时的草稿(中断不丢失) + * form 存完整向导快照(步骤/立项/描述/业务类型/指标作答/岗位/成本参数等)。 + */ + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS wizard_drafts ( + id TEXT PRIMARY KEY, + assessor_id TEXT, + source_assessment_id TEXT, + project_name TEXT, + form JSONB NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + CREATE INDEX IF NOT EXISTS idx_wizard_drafts_assessor ON wizard_drafts(assessor_id); + `); +}; + +exports.down = (pgm) => { + pgm.sql(`DROP TABLE IF EXISTS wizard_drafts;`); +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..aead558 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7929 @@ +{ + "name": "outsourcing-risk-assessment", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "outsourcing-risk-assessment", + "version": "0.1.0", + "dependencies": { + "@hono/node-server": "^2.0.4", + "@types/pg": "^8.20.0", + "hono": "^4.12.25", + "node-pg-migrate": "^8.0.4", + "nodejieba": "^3.5.8", + "pg": "^8.21.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0", + "recharts": "^2.13.3", + "zustand": "^5.0.14" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", + "@types/node": "^25.9.2", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "axe-core": "^4.10.2", + "eslint": "^9.17.0", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.1.0", + "fast-check": "^3.23.1", + "jsdom": "^25.0.1", + "playwright": "^1.58.2", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0", + "vitest": "^2.1.8", + "vitest-axe": "^0.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/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.npmjs.org/@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.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "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.npmjs.org/@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.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "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.npmjs.org/@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.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hono/node-server": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.4.tgz", + "integrity": "sha512-Ut3y0dMMPWy6bZ2kVfx25EOVbZlm15dhF4mOsezMlhpNHy+4MkU1qN9Y6lnruYi4wPmFzimGX2X7LF/FwHli4A==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/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/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/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/@remix-run/router": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", + "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "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.61.0", + "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.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "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.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", + "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.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.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/tsconfig-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", + "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.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", + "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "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.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", + "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.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "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.npmjs.org/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.npmjs.org/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.npmjs.org/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.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", + "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.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/visitor-keys": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "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.npmjs.org/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/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/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-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/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.12.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.0.tgz", + "integrity": "sha512-FTavr/7Ba0IptwGOPxnQvdyW2tAsdLBMTBXz7rKH6xJ2skpyxpBxyHkDdBs4lf69yRqYpkqCdfhnwS8YULGOmg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.35", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz", + "integrity": "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/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.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "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.npmjs.org/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.npmjs.org/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.30001797", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", + "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", + "dev": true, + "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/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/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/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/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.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/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/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/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-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/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/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/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/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "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/electron-to-chromium": { + "version": "1.5.371", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", + "integrity": "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.3.tgz", + "integrity": "sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==", + "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-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "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.npmjs.org/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.npmjs.org/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/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/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.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.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.5", + "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-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/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/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/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/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/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/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/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/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "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/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "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-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "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.npmjs.org/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/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/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/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "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/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/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/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/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-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/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.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/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/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/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/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/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/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/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.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-pg-migrate": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/node-pg-migrate/-/node-pg-migrate-8.0.4.tgz", + "integrity": "sha512-HTlJ6fOT/2xHhAUtsqSN85PGMAqSbfGJNRwQF8+ZwQ1+sVGNUTl/ZGEshPsOI3yV22tPIyHXrKXr3S0JxeYLrg==", + "license": "MIT", + "dependencies": { + "glob": "~11.1.0", + "yargs": "~17.7.0" + }, + "bin": { + "node-pg-migrate": "bin/node-pg-migrate.js" + }, + "engines": { + "node": ">=20.11.0" + }, + "peerDependencies": { + "@types/pg": ">=6.0.0 <9.0.0", + "pg": ">=4.3.0 <9.0.0" + }, + "peerDependenciesMeta": { + "@types/pg": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nodejieba": { + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/nodejieba/-/nodejieba-3.5.8.tgz", + "integrity": "sha512-TFiDYgEVmBgQwAr1sVj7err3Cn21qxMxTLA+LTEVqKcGqgwJuonJEW0g6oH2+7FhMKJfkWsvlj1SaTrWXe1Q9w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.10", + "node-addon-api": "^5.1.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/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/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/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/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pg": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", + "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.13.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.14.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", + "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", + "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/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/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/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.5.15", + "resolved": "https://registry.npmjs.org/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/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/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.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/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-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "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.npmjs.org/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.npmjs.org/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.npmjs.org/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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/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_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/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-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/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/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "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/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/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/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", + "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.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/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/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": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest-axe": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/vitest-axe/-/vitest-axe-0.1.0.tgz", + "integrity": "sha512-jvtXxeQPg8R/2ANTY8QicA5pvvdRP4F0FsVUAHANJ46YCDASie/cuhlSzu0DGcLmZvGBSBNsNuK3HqfaeknyvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.0.0", + "axe-core": "^4.4.2", + "chalk": "^5.0.1", + "dom-accessibility-api": "^0.5.14", + "lodash-es": "^4.17.21", + "redent": "^3.0.0" + }, + "peerDependencies": { + "vitest": ">=0.16.0" + } + }, + "node_modules/vitest-axe/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "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.npmjs.org/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.npmjs.org/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.npmjs.org/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.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/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/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "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/package.json b/package.json new file mode 100644 index 0000000..bf91927 --- /dev/null +++ b/package.json @@ -0,0 +1,60 @@ +{ + "name": "outsourcing-risk-assessment", + "version": "0.1.0", + "private": true, + "description": "外包项目风险评估 AI 智能体 - 领域引擎与前端表现层", + "type": "module", + "engines": { + "node": ">=20" + }, + "scripts": { + "build": "tsc --build && tsc -p web/tsconfig.json --noEmit", + "dev": "vite", + "build:web": "tsc -p web/tsconfig.json --noEmit && vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit && tsc -p web/tsconfig.json --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "e2e": "node scripts/e2e-full.mjs", + "migrate": "node-pg-migrate -m migrations", + "migrate:up": "node-pg-migrate -m migrations up", + "migrate:down": "node-pg-migrate -m migrations down", + "lint": "eslint \"src/**/*.ts\" \"web/**/*.{ts,tsx}\"", + "lint:fix": "eslint \"src/**/*.ts\" \"web/**/*.{ts,tsx}\" --fix" + }, + "dependencies": { + "@hono/node-server": "^2.0.4", + "@types/pg": "^8.20.0", + "hono": "^4.12.25", + "node-pg-migrate": "^8.0.4", + "nodejieba": "^3.5.8", + "pg": "^8.21.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0", + "recharts": "^2.13.3", + "zustand": "^5.0.14" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", + "@types/node": "^25.9.2", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "axe-core": "^4.10.2", + "eslint": "^9.17.0", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.1.0", + "fast-check": "^3.23.1", + "jsdom": "^25.0.1", + "playwright": "^1.58.2", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0", + "vitest": "^2.1.8", + "vitest-axe": "^0.1.0" + } +} diff --git a/scripts/demo-record.mjs b/scripts/demo-record.mjs new file mode 100644 index 0000000..d05f60e --- /dev/null +++ b/scripts/demo-record.mjs @@ -0,0 +1,155 @@ +/** + * 端到端演示录制:登录 → 6 步评估向导 → 评估详情页。 + * + * 产出: + * - docs/demo/NN-*.png 关键步骤截图(deviceScaleFactor=2,高清) + * - docs/demo/walkthrough.webm 全程操作录像(Playwright 原生录制) + * + * 运行:node scripts/demo-record.mjs + */ +import { chromium } from 'playwright'; +import fs from 'node:fs'; +import path from 'node:path'; + +const ROOT = '/Users/freedak/Documents/AIDashboard/RiskAgent'; +const SHOTS = path.join(ROOT, 'docs/demo'); +const VIDEO_DIR = path.join(SHOTS, 'video'); +const BASE = 'http://localhost:5173'; +const W = 1440; +const H = 900; + +fs.mkdirSync(SHOTS, { recursive: true }); +fs.mkdirSync(VIDEO_DIR, { recursive: true }); + +let n = 0; +async function shot(page, label) { + n += 1; + const name = `${String(n).padStart(2, '0')}-${label}.png`; + await page.screenshot({ path: path.join(SHOTS, name) }); + console.log('📸', name); +} + +async function expandAll(page) { + for (let i = 0; i < 15; i += 1) { + const collapsed = page.locator('button[aria-expanded="false"]'); + const c = await collapsed.count(); + if (c === 0) break; + try { + await collapsed.first().click({ timeout: 5000 }); + } catch { + break; + } + await page.waitForTimeout(150); + } +} + +(async () => { + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ + viewport: { width: W, height: H }, + deviceScaleFactor: 2, + locale: 'zh-CN', + recordVideo: { dir: VIDEO_DIR, size: { width: W, height: H } }, + }); + const page = await context.newPage(); + page.setDefaultTimeout(120000); + const video = page.video(); + + // ---------- 登录 ---------- + await page.goto(BASE, { waitUntil: 'networkidle' }); + await page.waitForTimeout(800); + const userBox = page.getByPlaceholder('请输入用户名'); + if ((await userBox.count()) > 0) { + await shot(page, 'login'); + await userBox.fill('销售账号'); + await page.getByPlaceholder('请输入密码').fill('123456'); + await page.getByRole('button', { name: '登录' }).click(); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(1200); + } + + // ---------- 首页:评估历史 ---------- + await shot(page, 'dashboard'); + + // ---------- 进入新建评估 ---------- + await page.getByRole('button', { name: /新建评估/ }).first().click(); + await page.getByText('① 立项信息').waitFor(); + await page.getByPlaceholder('如 某银行信用卡客服 BPO').fill('某全国性股份制银行信用卡中心客服外包项目'); + await page.getByPlaceholder('客户公司全称').fill('某全国性股份制商业银行信用卡中心'); + await page.locator('select').first().selectOption('上海'); + await page.waitForTimeout(300); + await shot(page, 'step1-立项信息'); + await page.getByRole('button', { name: '下一步', exact: true }).click(); + + // ---------- 步骤②:项目描述 ---------- + await page.getByText('② 项目描述').waitFor(); + const desc = + '为客户信用卡中心提供电话客服与在线客服外包服务,采用业务/服务外包模式,坐席约 80 人,' + + '7×12 小时三班排班;服务质量按接通率、平均处理时长与客户满意度考核;结算账期约 3 个月;' + + '需符合金融数据安全与个人信息保护合规要求,涉及呼叫中心系统对接与驻场管理。'; + await page.locator('textarea').fill(desc); + await page.waitForTimeout(300); + await shot(page, 'step2-项目描述'); + await page.getByRole('button', { name: /识别业务类型|识别中/ }).click(); + + // ---------- 步骤③:业务类型确认(LLM 分类) ---------- + await page.getByText('③ 业务类型确认').waitFor({ timeout: 120000 }); + await page.waitForTimeout(600); + await shot(page, 'step3-业务确认-LLM分类'); + await page.getByRole('button', { name: /补全指标|加载指标中/ }).click(); + + // ---------- 步骤④:指标补全(LLM 预填) ---------- + await page.getByText('④ 指标补全', { exact: false }).waitFor({ timeout: 120000 }); + await page.waitForTimeout(800); + await shot(page, 'step4-指标补全-LLM预填'); + await page.getByRole('button', { name: /费用输入/ }).click(); + + // ---------- 步骤⑤:报价与成本 ---------- + await page.getByText('⑤ 报价与成本').waitFor(); + await page.getByPlaceholder('如 运维工程师').fill('信用卡客服坐席'); + await page.getByPlaceholder('10', { exact: true }).fill('80'); + await page.getByPlaceholder('10000', { exact: true }).fill('6000'); + await page.getByPlaceholder('默认=工资', { exact: true }).fill('6000'); + await page.getByPlaceholder('0', { exact: true }).fill('500'); + await page.getByPlaceholder('如 16000', { exact: true }).fill('11000'); + await page.waitForTimeout(300); + await shot(page, 'step5-报价与成本'); + await page.getByRole('button', { name: /运行风险与盈利评估|评估运行中/ }).click(); + + // ---------- 评估详情页 ---------- + await page.waitForURL(/\/assessments\/[^/]+$/, { timeout: 180000 }); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(2000); + await shot(page, 'detail-顶部结论'); + + // 等待 AI 综合研判(异步 LLM),最多 ~25s + for (let i = 0; i < 25; i += 1) { + if ((await page.getByText(/综合研判|综合意见|AI 综合/).count()) > 0) break; + await page.waitForTimeout(1000); + } + await expandAll(page); + await page.waitForTimeout(800); + + // 逐屏滚动截图,覆盖全部分区 + await page.evaluate(() => window.scrollTo(0, 0)); + const totalH = await page.evaluate(() => document.body.scrollHeight); + const stepY = H - 140; + for (let y = stepY; y < totalH; y += stepY) { + await page.evaluate((yy) => window.scrollTo(0, yy), y); + await page.waitForTimeout(450); + await shot(page, 'detail-分区'); + if (n > 22) break; + } + + await context.close(); + await browser.close(); + + const vp = await video.path(); + const dest = path.join(SHOTS, 'walkthrough.webm'); + fs.renameSync(vp, dest); + console.log('🎬 video saved:', dest); + console.log('✅ done, screenshots:', n); +})().catch((e) => { + console.error('FAILED:', e); + process.exit(1); +}); diff --git a/scripts/e2e-full.mjs b/scripts/e2e-full.mjs new file mode 100644 index 0000000..89a7f70 --- /dev/null +++ b/scripts/e2e-full.mjs @@ -0,0 +1,269 @@ +/** + * 全功能 E2E 测试(API 层,针对运行中的后端 :3005)。 + * 覆盖:分类/问题(差异化)/岗位抽取/盈利预览/创建(草稿)/数据完整度/坏账/目标净利率分层/ + * 申报/风控审核/管理层审批/编辑原地重评/可计算红线(数值/指标/派遣/最低工资/AND)/ + * 人工裁定/报告导出/校准/客户自动统计/费率红线CRUD/看板/归档。 + * 结束时清理所有创建的数据。 + */ +import { Client } from 'pg'; + +const BASE = 'http://localhost:3005'; +const SEP = '\u0000'; +let pass = 0, fail = 0; +const created = []; // assessment ids +const redlines = []; // redline rule ids +const tokens = { '商务/销售': '', '风控': '', '管理层': '' }; +let authMode = false; + +function ok(cond, msg) { + if (cond) { pass += 1; console.log(' ✓', msg); } + else { fail += 1; console.log(' ✗ FAIL:', msg); } +} +async function login(role) { + const r = await fetch(BASE + '/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'E2E' + role, role }) }); + const d = await r.json(); + return typeof d.token === 'string' ? d.token : ''; +} +async function j(method, path, body, role) { + const headers = { 'Content-Type': 'application/json' }; + const tk = role !== undefined ? tokens[role] : tokens['管理层']; + if (tk) headers.Authorization = 'Bearer ' + tk; + const init = { method, headers }; + if (body !== undefined) init.body = JSON.stringify(body); + const r = await fetch(BASE + path, init); + const text = await r.text(); + let data; try { data = JSON.parse(text); } catch { data = text; } + return { status: r.status, data, headers: r.headers }; +} +async function runAssessment(payload) { + const r = await j('POST', '/api/assessments/run', payload); + if (r.data?.assessmentId) created.push(r.data.assessmentId); + return r; +} +async function addRedline(rule) { + const r = await j('POST', '/api/redline-rules', rule, '管理层'); + redlines.push(rule.id); + return r; +} + +async function main() { + console.log('\n=== 0. 登录获取角色令牌 ==='); + for (const role of ['商务/销售', '风控', '管理层']) tokens[role] = await login(role); + authMode = tokens['管理层'] !== ''; + console.log(' 鉴权模式:', authMode ? '已开启(JWT)' : '演示模式(无强制鉴权)'); + + console.log('\n=== 1. 基础健康 & LLM 状态 ==='); + ok((await j('GET', '/api/health')).data.status === 'ok', 'health ok'); + const llm = (await j('GET', '/api/llm/status')).data; + console.log(' LLM 启用:', llm.enabled); + + console.log('\n=== 2. 业务分类 ==='); + const cls = await j('POST', '/api/assessments/classify', { projectDescription: '为某银行提供信用卡客服外包,呼叫中心坐席50人,处理客户咨询与投诉。' }); + ok(cls.status === 200 && typeof cls.data.businessType === 'string', '分类返回业务类型: ' + cls.data.businessType); + + console.log('\n=== 3. 差异化指标(questions) ==='); + const qDispatch = (await j('POST', '/api/assessments/questions', { businessType: '劳务派遣' })).data; + const qBPO = (await j('POST', '/api/assessments/questions', { businessType: 'BPO' })).data; + const dispIds = qDispatch.indicators.map((i) => i.indicatorId); + const bpoIds = qBPO.indicators.map((i) => i.indicatorId); + ok(dispIds.includes('dispatch-ratio'), '劳务派遣含派遣比例指标'); + ok(bpoIds.includes('data-security') && bpoIds.includes('capacity-stability'), 'BPO含数据安全+产能指标'); + ok(!dispIds.includes('capacity-stability'), '劳务派遣不含BPO专属指标(差异化)'); + ok(qDispatch.indicators.every((i) => i.levels.length === 5), '每指标含1-5级锚点'); + + console.log('\n=== 4. 岗位抽取(LLM, 容错) ==='); + const ep = await j('POST', '/api/assessments/extract-positions', { projectDescription: '服务器运维20人、网络安全5人、桌面运维10人' }); + ok(ep.status === 200 && Array.isArray(ep.data.positions), '岗位抽取返回数组(' + ep.data.positions.length + '个)'); + + console.log('\n=== 5. 盈利预览(无状态) ==='); + const prev = await j('POST', '/api/assessments/profitability', { businessType: '岗位外包', region: '北京', pricingModel: 'per_head', contractMonths: 12, positions: [{ name: '运维', headcount: 10, monthlyGrossSalary: 10000 }] }); + ok(prev.status === 200 && prev.data.positions[0].perHead.fullyLoaded > 10000, '全口径成本 > 应发工资: ' + prev.data.positions[0].perHead.fullyLoaded); + + console.log('\n=== 6. 客户(坏账驱动)选取 ==='); + const custs = (await j('GET', '/api/customers')).data; + ok(Array.isArray(custs) && custs.length > 0, '客户档案非空(' + custs.length + ')'); + const badCust = custs.find((c) => c.creditRating === 'B' || c.creditRating === 'BB') ?? custs[0]; + + console.log('\n=== 7. 创建评估(草稿) + 完整度/坏账/分层 ==='); + const run = await runAssessment({ + projectDescription: `【项目】E2E主流程|【客户】${badCust.name}\n岗位外包,运维10人,月薪10000。`, + confirmation: { businessType: '岗位外包' }, region: '北京', assessorId: 'E2E销售', + knownData: [['dim-customer' + SEP + 'customer-credit', 4], ['dim-financial' + SEP + 'gross-margin', 3]], + profitabilityInputs: { businessType: '岗位外包', region: '北京', pricingModel: 'per_head', contractMonths: 12, positions: [{ name: '运维', headcount: 10, monthlyGrossSalary: 10000, unitPrice: 16000 }] }, + }); + const id = run.data.assessmentId; + ok(run.status === 200 && typeof id === 'string', '创建成功 id=' + id); + let det = (await j('GET', '/api/assessments/' + id)).data; + ok(det.status === 'draft', '初始状态为草稿待申报'); + ok(det.profitabilityInputs && det.profitabilityInputs.markupRate === undefined, '原始输入已保存'); + ok(det.profitability.monthly.badDebtReserve > 0, '坏账准备金已计提(信用' + badCust.creditRating + '): ' + det.profitability.monthly.badDebtReserve); + ok(det.recommendation.targetMargin > 0, '目标净利率分层: ' + det.recommendation.targetMargin); + ok(typeof det.expiresAt === 'string', '有效期已写入'); + + console.log('\n=== 8. 申报 → 待风控审核 ==='); + const sub = await j('POST', '/api/assessments/' + id + '/submit', { user: 'E2E销售' }); + ok(sub.data.status === 'pending_risk_review', '申报后状态 pending_risk_review'); + + console.log('\n=== 9. 风控审核通过 → 风控已审核 ==='); + const rev = await j('POST', '/api/assessments/' + id + '/review', { action: 'approve', user: 'E2E风控', comment: 'E2E通过' }, '风控'); + ok(rev.data.status === 'risk_reviewed', '风控审核通过'); + + console.log('\n=== 10. 管理层审批通过 → 已通过 ==='); + const apr = await j('POST', '/api/assessments/' + id + '/approve', { action: 'approve', user: 'E2E管理' }, '管理层'); + ok(apr.data.status === 'approved', '管理层审批通过'); + + console.log('\n=== 11. 报告导出(JSON/HTML) ==='); + const expJson = await fetch(`${BASE}/api/assessments/${id}/report/export?format=json`); + const expHtml = await fetch(`${BASE}/api/assessments/${id}/report/export?format=html`); + ok(expJson.status === 200, 'JSON 导出 200'); + const htmlText = await expHtml.text(); + ok(expHtml.status === 200 && htmlText.includes('=', threshold: 5 }); + await addRedline({ id: 'e2e-dispatch', title: 'E2E派遣超限', triggerCondition: '派遣>10%', consequence: '一票否决', enabled: true, version: 1, linkedMetric: 'dispatchRatio', compareOp: '>', threshold: 10 }); + await addRedline({ id: 'e2e-minwage', title: 'E2E低于最低工资', triggerCondition: '低于最低工资', consequence: '一票否决', enabled: true, version: 1, linkedMetric: 'belowMinWageCount', compareOp: '>=', threshold: 1 }); + await addRedline({ id: 'e2e-and', title: 'E2E毛利净利双负', triggerCondition: '毛利<0且净利<0', consequence: '一票否决', enabled: true, version: 1, linkedMetric: 'grossMargin', compareOp: '<', threshold: 0, linkedMetric2: 'netMargin', compareOp2: '<', threshold2: 0 }); + + const rlRun = await runAssessment({ + projectDescription: '【项目】E2E红线|【客户】红线测试客户E2E\n劳务派遣,派遣工30人,月薪2000。', + confirmation: { businessType: '劳务派遣' }, region: '北京', assessorId: 'E2E销售', clientTotalHeadcount: 100, + knownData: [['dim-legal' + SEP + 'qualification', 5]], + profitabilityInputs: { businessType: '劳务派遣', region: '北京', pricingModel: 'per_head', contractMonths: 12, positions: [{ name: '派遣工', headcount: 30, monthlyGrossSalary: 2000, unitPrice: 2200 }] }, + }); + const rlDet = (await j('GET', '/api/assessments/' + rlRun.data.assessmentId)).data; + const rr = (rid) => rlDet.assessment.redlineResults.find((x) => x.redlineId === rid)?.status; + ok(rr('e2e-margin') === '命中', '净利率红线命中'); + ok(rr('e2e-qual') === '命中', '指标等级红线(资质=5)命中'); + ok(rr('e2e-dispatch') === '命中', '派遣比例红线命中(30%)'); + ok(rr('e2e-minwage') === '命中', '最低工资红线命中(2000<2420)'); + ok(rr('e2e-and') === '命中', 'AND 复合红线命中(毛利&净利双负)'); + ok(rlDet.assessment.acceptability === '不可接受', '命中红线→不可接受'); + + console.log('\n=== 16. 红线人工裁定(待核实→命中) ==='); + const pend = rlDet.assessment.redlineResults.find((x) => x.status === '待核实'); + if (pend) { + const vd = await j('POST', '/api/assessments/' + rlRun.data.assessmentId + '/redline-verdict', { redlineId: pend.redlineId, status: '命中', user: 'E2E风控', role: '风控', title: '人工核实项' }, '风控'); + ok(vd.data.status === '命中', '人工裁定命中成功'); + } else { ok(true, '无待核实红线(跳过裁定)'); } + + console.log('\n=== 17. 客户自动统计(集中度基于真实数据) ==='); + const custDet = (await j('GET', '/api/assessments/' + id)).data.customerDetail; + ok(custDet !== null, '评估关联到客户档案: ' + (custDet?.name ?? 'null')); + + console.log('\n=== 18. 校准(准确度) ==='); + // 回填实际值以产生偏差数据 + await j('POST', '/api/assessments/' + rlRun.data.assessmentId + '/actuals', { month: 1, metrics: [{ name: '月净利率', value: 0.02 }] }); + const calib = await j('GET', '/api/calibration'); + ok(calib.status === 200 && typeof calib.data.currentBase === 'number', '校准查询返回当前/建议基准'); + console.log(' 当前基准:', calib.data.currentBase, '建议:', calib.data.suggestedBase, '偏差:', calib.data.deviationPct, calib.data.bias); + + console.log('\n=== 19. 看板/告警/统计 ==='); + ok((await j('GET', '/api/dashboard/stats')).status === 200, '组合看板 200'); + ok(Array.isArray((await j('GET', '/api/assessments/expiring')).data), '到期提醒返回数组'); + ok(Array.isArray((await j('GET', '/api/assessments/overdue')).data), '超时提醒返回数组'); + ok((await j('GET', '/api/assessments/summary')).status === 200, '状态汇总 200'); + + console.log('\n=== 20. 费率/红线/相似项目 ==='); + ok((await j('GET', '/api/region-rates/engine-defaults')).status === 200, '引擎默认费率 200'); + ok(Array.isArray((await j('GET', '/api/redline-rules')).data), '红线列表返回数组'); + const sim = await j('GET', '/api/similar?description=' + encodeURIComponent('劳务派遣呼叫中心客服外包')); + ok(Array.isArray(sim.data), '相似项目检索返回数组(' + sim.data.length + ')'); + + console.log('\n=== 21. 回款数据→逾期自动化 ==='); + const tmpCustId = 'e2e-pay-cust'; + await j('POST', '/api/customers', { id: tmpCustId, name: 'E2E回款客户', creditRating: 'A' }, '管理层'); + const payRes = await j('POST', `/api/customers/${tmpCustId}/payments`, { invoiceAmount: 100000, dueDate: '2024-01-01' }, '管理层'); + ok(payRes.data.avgOverdueDays > 0, '录入逾期回款后自动重算平均逾期天数: ' + payRes.data.avgOverdueDays + ' 天'); + const pays = (await j('GET', `/api/customers/${tmpCustId}/payments`, undefined, '管理层')).data; + ok(Array.isArray(pays) && pays.length === 1, '回款记录可读取'); + await j('DELETE', `/api/customers/${tmpCustId}`, undefined, '管理层'); // 级联删除回款 + + console.log('\n=== 22. 最低工资表后台化 ==='); + const mws = (await j('GET', '/api/min-wages')).data; + ok(Array.isArray(mws) && mws.some((m) => m.region === '北京' && m.monthlyWage > 0), '最低工资表含北京标准(' + (mws.find((m) => m.region === '北京')?.monthlyWage) + ')'); + + console.log('\n=== 23. RBAC 强制鉴权(仅鉴权模式断言) ==='); + if (authMode) { + const noTok = await fetch(BASE + '/api/redline-rules', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 'e2e-noauth', title: 'x', triggerCondition: 'x', consequence: 'x', enabled: true, version: 1 }) }); + ok(noTok.status === 403, '无令牌写红线 → 403'); + const wrongRole = await j('POST', '/api/redline-rules', { id: 'e2e-wrongrole', title: 'x', triggerCondition: 'x', consequence: 'x', enabled: true, version: 1 }, '商务/销售'); + ok(wrongRole.status === 403, '销售令牌写红线 → 403(角色不足)'); + const rightRole = await addRedline({ id: 'e2e-rbac-ok', title: 'RBAC通过', triggerCondition: 'x', consequence: 'x', enabled: true, version: 1 }); + ok(rightRole.status === 200, '管理层令牌写红线 → 200'); + } else { + ok(true, '演示模式:跳过 RBAC 强制断言(设置 AUTH_SECRET 后启用)'); + } + + console.log('\n=== 24. 向导草稿服务端持久化(跨设备) ==='); + const draftId = `e2e-draft-${Date.now()}`; + const upserted = (await j('POST', '/api/drafts', { id: draftId, assessorId: 'e2e-sales', projectName: 'E2E草稿项目', form: { step: 3, description: '草稿描述', answers: { a: 2 } } })).data; + ok(upserted.id === draftId && upserted.projectName === 'E2E草稿项目', '草稿 upsert 成功'); + const draftList = (await j('GET', '/api/drafts?assessorId=e2e-sales')).data; + ok(Array.isArray(draftList) && draftList.some((d) => d.id === draftId), '草稿出现在列表(按评估人过滤)'); + const got = (await j('GET', `/api/drafts/${draftId}`)).data; + ok(got.form && got.form.step === 3 && got.form.answers.a === 2, '草稿详情含完整向导快照 form'); + // 编辑草稿:source_assessment_id 非空 + const editDraftId = `e2e-draft-edit-${Date.now()}`; + const editDraft = (await j('POST', '/api/drafts', { id: editDraftId, assessorId: 'e2e-sales', sourceAssessmentId: 'some-assessment', projectName: '编辑草稿', form: { step: 4 } })).data; + ok(editDraft.sourceAssessmentId === 'some-assessment', '编辑草稿记录源评估 id'); + const del = (await j('DELETE', `/api/drafts/${draftId}`)).data; + ok(del.deleted === true, '草稿删除成功'); + await j('DELETE', `/api/drafts/${editDraftId}`); + const afterList = (await j('GET', '/api/drafts?assessorId=e2e-sales')).data; + ok(Array.isArray(afterList) && !afterList.some((d) => d.id === draftId), '删除后草稿不再出现在列表'); + + console.log(`\n=== 结果:${pass} 通过 / ${fail} 失败 ===`); +} + +async function cleanup() { + console.log('\n=== 清理测试数据 ==='); + const c = new Client({ connectionString: 'postgresql://localhost:5432/riskagent' }); + await c.connect(); + for (const id of created) { + for (const t of ['workflow_status', 'audit_logs', 'profitability', 'recommendations', 'actuals']) { + await c.query(`DELETE FROM ${t} WHERE assessment_id=$1`, [id]).catch(() => {}); + } + await c.query('DELETE FROM assessments WHERE id=$1', [id]).catch(() => {}); + } + for (const rid of redlines) { + await c.query('DELETE FROM redline_rules WHERE id=$1', [rid]).catch(() => {}); + } + await c.end(); + console.log(' 清理评估', created.length, '条, 红线', redlines.length, '条'); +} + +main() + .catch((e) => { console.error('E2E 异常:', e); fail += 1; }) + .finally(async () => { + await cleanup().catch((e) => console.error('清理异常:', e)); + console.log(fail === 0 ? '\n✅ 全部 E2E 通过' : `\n❌ 有 ${fail} 项失败`); + process.exit(fail === 0 ? 0 : 1); + }); diff --git a/scripts/seed-api.mjs b/scripts/seed-api.mjs new file mode 100644 index 0000000..e3dcf12 --- /dev/null +++ b/scripts/seed-api.mjs @@ -0,0 +1,102 @@ +// 一次性脚本:通过 HTTP API 向运行中的后端批量灌入 3 个评估示例。 +const BASE = process.env.API_BASE ?? 'http://localhost:3005'; +const SEP = '\u0000'; // indicatorKey 复合键分隔符(NUL) +const kd = (rows) => rows.map(([d, i, l]) => [`${d}${SEP}${i}`, l]); + +const cases = [ + { + name: '案例1 银行IT运维岗位外包(预期可接受)', + body: { + projectDescription: + '为某大型国有银行提供IT运维岗位外包服务。客户资信优良、注册资本充足、无失信记录;账期30天且历史回款准时;岗位为常规运维支持非高危;SLA交付标准清晰可量化;毛利率充足、人力成本可控。', + confirmation: { businessType: '岗位外包', industry: '通用' }, + region: 'CN-SH', + assessorId: 'sales-001', + knownData: kd([ + ['dim-customer', 'customer-credit', 1], + ['dim-customer', 'payment-ability', 1], + ['dim-position', 'position-nature', 2], + ['dim-business', 'delivery-standard', 1], + ['dim-labor', 'layoff-risk', 2], + ['dim-labor', 'injury-risk', 1], + ['dim-financial', 'gross-margin', 2], + ['dim-legal', 'qualification', 1], + ['dim-lifecycle', 'lifecycle-stage', 2], + ['dim-macro', 'macro-policy', 2], + ]), + costInputs: { baselineQuote: 2000000 }, + topN: 5, + }, + }, + { + name: '案例2 互联网客服BPO(预期有条件接受)', + body: { + projectDescription: + '为某成长期互联网公司提供客服BPO外包。客户为创业公司资信一般、账期90天且偶有逾期;岗位含部分替代性用工;SLA标准较模糊;毛利率偏薄;存在一定裁员与合规风险。', + confirmation: { businessType: 'BPO', industry: '通用' }, + region: 'CN-GD', + assessorId: 'sales-002', + knownData: kd([ + ['dim-customer', 'customer-credit', 4], + ['dim-customer', 'payment-ability', 4], + ['dim-position', 'position-nature', 4], + ['dim-business', 'delivery-standard', 4], + ['dim-labor', 'layoff-risk', 4], + ['dim-labor', 'injury-risk', 3], + ['dim-financial', 'gross-margin', 4], + ['dim-legal', 'qualification', 3], + ['dim-lifecycle', 'lifecycle-stage', 3], + ['dim-macro', 'macro-policy', 3], + ]), + costInputs: { baselineQuote: 1200000 }, + topN: 5, + }, + }, + { + name: '案例3 衰退期制造高危劳务派遣(预期不可接受)', + body: { + projectDescription: + '为某衰退期制造企业提供高危产线劳务派遣。客户资信差、已有涉诉与逾期记录;账期超120天回款困难;岗位为高危作业工伤风险高;测算毛利率为负;资质不全存在假外包真派遣风险。', + confirmation: { businessType: '劳务派遣', industry: '通用' }, + region: 'CN-HB', + assessorId: 'sales-003', + knownData: kd([ + ['dim-customer', 'customer-credit', 5], + ['dim-customer', 'payment-ability', 5], + ['dim-position', 'position-nature', 5], + ['dim-business', 'delivery-standard', 5], + ['dim-labor', 'layoff-risk', 5], + ['dim-labor', 'injury-risk', 5], + ['dim-financial', 'gross-margin', 5], + ['dim-legal', 'qualification', 5], + ['dim-lifecycle', 'lifecycle-stage', 5], + ['dim-macro', 'macro-policy', 4], + ]), + costInputs: { baselineQuote: 800000 }, + topN: 5, + }, + }, +]; + +for (const c of cases) { + const res = await fetch(`${BASE}/api/assessments/run`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(c.body), + }); + const j = await res.json(); + if (!res.ok) { + console.error(`✗ ${c.name} 失败 [${res.status}]:`, j.error ?? j); + continue; + } + console.log( + `✓ ${c.name}\n id=${j.assessmentId} | score=${j.riskScore} grade=${j.riskGrade} | 结论=${j.acceptability} | 报价 ${j.assessment.costEstimate.baselineQuote}->${j.assessment.costEstimate.riskAdjustedQuote}`, + ); +} + +// 校验列表接口 +const list = await (await fetch(`${BASE}/api/assessments`)).json(); +console.log(`\n后端现有评估记录数:${list.length}`); +for (const r of list) { + console.log(` - ${r.id} | ${r.businessType} | ${r.riskGrade} | ${r.acceptability} | 状态=${r.status}`); +} diff --git a/scripts/seed-e2e.mjs b/scripts/seed-e2e.mjs new file mode 100644 index 0000000..1aa67a2 --- /dev/null +++ b/scripts/seed-e2e.mjs @@ -0,0 +1,26 @@ +/** + * E2E 基础数据播种:插入少量客户档案(含各信用等级),供 e2e-full.mjs 使用。 + * 幂等(ON CONFLICT DO NOTHING)。CI 与本地均可运行。 + */ +import { Client } from 'pg'; + +const URL = process.env.DATABASE_URL ?? 'postgresql://localhost:5432/riskagent'; + +const customers = [ + { id: 'seed-cust-aaa', name: '某国有大行', creditRating: 'AAA', avgOverdueDays: 3, totalContractAmount: 8000000, assessmentCount: 0, notes: null }, + { id: 'seed-cust-a', name: '某制造企业', creditRating: 'A', avgOverdueDays: 15, totalContractAmount: 2000000, assessmentCount: 0, notes: null }, + { id: 'seed-cust-bbb', name: '某省电信公司', creditRating: 'BBB', avgOverdueDays: 45, totalContractAmount: 3000000, assessmentCount: 0, notes: null }, + { id: 'seed-cust-b', name: '某互联网初创企业', creditRating: 'B', avgOverdueDays: 70, totalContractAmount: 500000, assessmentCount: 0, notes: null }, +]; + +const c = new Client({ connectionString: URL }); +await c.connect(); +for (const x of customers) { + await c.query( + `INSERT INTO customers(id, name, credit_rating, avg_overdue_days, total_contract_amount, assessment_count, notes, updated_at) + VALUES($1,$2,$3,$4,$5,$6,$7,now()) ON CONFLICT(id) DO NOTHING`, + [x.id, x.name, x.creditRating, x.avgOverdueDays, x.totalContractAmount, x.assessmentCount, x.notes], + ); +} +await c.end(); +console.log('已播种', customers.length, '个客户档案'); diff --git a/scripts/test-parse.mjs b/scripts/test-parse.mjs new file mode 100644 index 0000000..5fb34d9 --- /dev/null +++ b/scripts/test-parse.mjs @@ -0,0 +1,14 @@ +const desc = '为省级政府数据中心提供全栈IT运维外包服务,涵盖服务器运维20人、网络安全5人、桌面运维10人,共35人团队。5x8+7x24值班,按ITIL流程管理,月度SLA 99.9%,合同3年,账期3个月,需安全审查资质。'; + +const pattern1 = /([^\d,,。、;\n]{2,8}?)\s*(\d{1,4})\s*人/g; +let m; +const results = []; +while ((m = pattern1.exec(desc)) !== null) { + const name = m[1].trim(); + const count = m[2]; + if (!/共|总|约|含/.test(name)) { + results.push({ name, count }); + } +} +console.log('正则解析:', results.length, '个岗位'); +results.forEach(r => console.log(' ', r.name, r.count + '人')); diff --git a/src/__tests__/optimizations.test.ts b/src/__tests__/optimizations.test.ts new file mode 100644 index 0000000..ab71356 --- /dev/null +++ b/src/__tests__/optimizations.test.ts @@ -0,0 +1,135 @@ +/** + * 优化项单元测试:覆盖本轮新增的引擎逻辑分支 + * - 坏账准备金(客户信用驱动)降低净利 + * - 可计算红线(数值度量 / 指标等级 / AND 复合条件) + * - 目标净利率分层 + * - 差异化业务类型模板(指标差异 + 维度/指标权重合规) + */ + +import { describe, expect, it } from 'vitest'; +import { analyzeProfitability, type ProfitabilityInputs } from '../cost/profitability.js'; +import { buildMetricRedline, compareMetric, metricLabel, type ComputableRedlineConfig } from '../scoring/redlineMetrics.js'; +import { checkRedlines } from '../scoring/checkRedlines.js'; +import { targetMarginForGrade, DEFAULT_TARGET_NET_MARGIN } from '../strategy/recommendation.js'; +import { DEFAULT_TEMPLATES } from '../server/templates/defaultTemplates.js'; +import type { Redline } from '../domain/model.js'; + +const baseInputs: ProfitabilityInputs = { + businessType: '岗位外包', + region: '北京', + pricingModel: 'per_head', + contractMonths: 12, + positions: [{ name: '运维', headcount: 10, monthlyGrossSalary: 10000, unitPrice: 16000 }], +}; + +describe('坏账准备金(客户信用驱动)', () => { + it('badDebtRate 越高净利越低,且 0 时无坏账行', () => { + const r0 = analyzeProfitability({ ...baseInputs, badDebtRate: 0 }); + const r6 = analyzeProfitability({ ...baseInputs, badDebtRate: 0.06 }); + expect(r0.monthly.badDebtReserve).toBe(0); + expect(r6.monthly.badDebtReserve).toBeGreaterThan(0); + expect(r6.monthly.netProfit).toBeLessThan(r0.monthly.netProfit); + // 坏账准备金 = 不含税收入 × 比例 + expect(r6.monthly.badDebtReserve).toBeCloseTo(r6.monthly.revenueNet * 0.06, 0); + }); +}); + +describe('可计算红线', () => { + const redlines: Redline[] = [ + { id: 'rl-margin', triggerCondition: '净利率<0', consequence: '一票否决', enabled: true }, + { id: 'rl-qual', triggerCondition: '资质=5', consequence: '一票否决', enabled: true }, + { id: 'rl-compound', triggerCondition: '毛利为负且净利为负', consequence: '一票否决', enabled: true }, + ]; + + it('数值度量:净利率 < 0 命中', () => { + const cfg = new Map([ + ['rl-margin', { linkedMetric: 'netMargin', compareOp: '<', threshold: 0 }], + ]); + const { resolveCondition, dataContext } = buildMetricRedline(cfg, { netMargin: -5 }); + const res = checkRedlines([redlines[0]!], resolveCondition, dataContext); + expect(res[0]!.status).toBe('命中'); + }); + + it('指标等级:资质 >= 5 命中、=3 未命中', () => { + const cfg = new Map([ + ['rl-qual', { linkedMetric: 'ind:qualification', compareOp: '>=', threshold: 5 }], + ]); + const hit = buildMetricRedline(cfg, { 'ind:qualification': 5 }); + expect(checkRedlines([redlines[1]!], hit.resolveCondition, hit.dataContext)[0]!.status).toBe('命中'); + const miss = buildMetricRedline(cfg, { 'ind:qualification': 3 }); + expect(checkRedlines([redlines[1]!], miss.resolveCondition, miss.dataContext)[0]!.status).toBe('未命中'); + }); + + it('AND 复合:两条件都满足才命中', () => { + const cfg = new Map([ + ['rl-compound', { linkedMetric: 'grossMargin', compareOp: '<', threshold: 0, and: { linkedMetric: 'netMargin', compareOp: '<', threshold: 0 } }], + ]); + const both = buildMetricRedline(cfg, { grossMargin: -2, netMargin: -5 }); + expect(checkRedlines([redlines[2]!], both.resolveCondition, both.dataContext)[0]!.status).toBe('命中'); + const onlyOne = buildMetricRedline(cfg, { grossMargin: 5, netMargin: -5 }); + expect(checkRedlines([redlines[2]!], onlyOne.resolveCondition, onlyOne.dataContext)[0]!.status).toBe('未命中'); + }); + + it('度量数据缺失 → 待核实', () => { + const cfg = new Map([ + ['rl-margin', { linkedMetric: 'netMargin', compareOp: '<', threshold: 0 }], + ]); + const { resolveCondition, dataContext } = buildMetricRedline(cfg, {}); + expect(checkRedlines([redlines[0]!], resolveCondition, dataContext)[0]!.status).toBe('待核实'); + }); + + it('compareMetric / metricLabel 基本正确', () => { + expect(compareMetric(5, '>=', 5)).toBe(true); + expect(compareMetric(4, '>=', 5)).toBe(false); + expect(metricLabel('netMargin')).toContain('净利率'); + expect(metricLabel('ind:qualification')).toContain('qualification'); + }); +}); + +describe('目标净利率分层', () => { + it('风险越高目标净利率越高', () => { + const low = targetMarginForGrade('低'); + const mid = targetMarginForGrade('中'); + const high = targetMarginForGrade('高'); + const extreme = targetMarginForGrade('极高'); + expect(low).toBeLessThan(mid); + expect(mid).toBeLessThan(high); + expect(high).toBeLessThan(extreme); + expect(mid).toBeCloseTo(DEFAULT_TARGET_NET_MARGIN, 6); + expect(targetMarginForGrade(undefined)).toBeCloseTo(DEFAULT_TARGET_NET_MARGIN, 6); + }); +}); + +describe('差异化业务类型模板', () => { + it('各业务类型指标集差异化', () => { + const byType = new Map(DEFAULT_TEMPLATES.map((t) => [t.businessType, t.riskModelConfig.dimensions.flatMap((d) => d.indicators.map((i) => i.id))])); + expect(byType.get('劳务派遣')).toContain('dispatch-ratio'); + expect(byType.get('BPO')).toContain('data-security'); + expect(byType.get('BPO')).toContain('capacity-stability'); + expect(byType.get('项目制外包')).toContain('scope-clarity'); + // 劳务派遣不含 BPO 专属的产能指标 + expect(byType.get('劳务派遣')).not.toContain('capacity-stability'); + }); + + it('每个维度的启用指标权重之和为 100、维度权重之和为 100', () => { + for (const t of DEFAULT_TEMPLATES) { + const dims = t.riskModelConfig.dimensions; + const dimSum = dims.reduce((s, d) => s + d.weight, 0); + expect(dimSum).toBe(100); + for (const d of dims) { + const indSum = d.indicators.filter((i) => i.enabled).reduce((s, i) => s + i.weight, 0); + expect(indSum).toBe(100); + } + } + }); + + it('每个指标的评分规则覆盖 1-5 级', () => { + for (const t of DEFAULT_TEMPLATES) { + for (const d of t.riskModelConfig.dimensions) { + for (const i of d.indicators) { + expect(i.scoringRules.map((r) => r.level).sort()).toEqual([1, 2, 3, 4, 5]); + } + } + } + }); +}); diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts new file mode 100644 index 0000000..5c41d46 --- /dev/null +++ b/src/__tests__/setup.ts @@ -0,0 +1,13 @@ +import fc from 'fast-check'; + +/** + * Global fast-check configuration. + * + * Property-based tests for this feature MUST run at least 100 iterations per + * property (see tasks.md "属性测试约束"). fast-check's default `numRuns` is 100; + * we set it explicitly here so the guarantee is enforced for every property test + * and survives any future change to library defaults. + */ +fc.configureGlobal({ + numRuns: 100, +}); diff --git a/src/__tests__/smoke.test.ts b/src/__tests__/smoke.test.ts new file mode 100644 index 0000000..8c05dfa --- /dev/null +++ b/src/__tests__/smoke.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest'; +import fc from 'fast-check'; +import { VERSION } from '../index.js'; + +describe('project skeleton smoke test', () => { + it('exposes the package version', () => { + expect(VERSION).toBe('0.1.0'); + }); + + it('runs fast-check property tests with the configured iteration count', () => { + // Verify the global fast-check config applies at least 100 iterations. + expect(fc.readConfigureGlobal()?.numRuns).toBe(100); + + // A trivial always-true property to confirm fast-check is wired up. + fc.assert( + fc.property(fc.integer(), fc.integer(), (a, b) => { + return a + b === b + a; + }), + ); + }); +}); diff --git a/src/adapters/__tests__/adapter.integration.test.ts b/src/adapters/__tests__/adapter.integration.test.ts new file mode 100644 index 0000000..ea71f30 --- /dev/null +++ b/src/adapters/__tests__/adapter.integration.test.ts @@ -0,0 +1,311 @@ +/** + * External_Data_Adapter 外部数据适配集成测试(Req 15.1, 15.5)。 + * + * 覆盖两条端到端路径: + * + * 1. 成功取数路径(Req 15.1, 15.2):以 mock {@link DataSourceAdapter} 在超时上限内 + * 成功返回数据点,经 {@link fetchExternalData} / {@link fetchWithFallback} 验证每个 + * 数据点 Data_Provenance 恒为"外部数据"、Confidence 落在 [0,1],且能回填客户风险 + * 相关 Indicator 取值。 + * + * 2. 可插拔扩展路径(Req 15.5):以一个最小适配器注册表(Map)注册 + * 一个全新的外部数据源适配器,再将其取数结果驱动 Scoring_Engine 的 `scoreDimension` + * / `scoreIndicator` 评分。整个过程仅 import 现有 scoringEngine.ts 而**不修改其源代码**, + * 以此证明新增数据源无需改动 Scoring_Engine(Req 15.5)。 + * + * Feature: outsourcing-risk-assessment + * Validates: Requirements 15.1, 15.5 + */ + +import { describe, expect, it } from 'vitest'; +import { + EXTERNAL_DATA_PROVENANCE, + annotateExternalDataPoints, + fetchExternalData, + fetchWithFallback, + type DataPoint, + type DataSourceAdapter, + type DataSourceQuery, + type RawExternalDataPoint, +} from '../index.js'; +import { + scoreDimension, + scoreIndicator, + type RiskLevelResolver, +} from '../../scoring/index.js'; +import type { Dimension, Indicator } from '../../domain/model.js'; +import type { RiskLevel } from '../../domain/common.js'; + +// ---------------------------------------------------------------------------- +// 测试替身:可配置的 mock 数据源适配器。 +// ---------------------------------------------------------------------------- + +/** + * 构造一个 mock 适配器:在调用时按给定原始数据点产出已标注的数据点(成功路径)。 + * 标注统一经 {@link annotateExternalDataPoints},保证 Req 15.2 的来源/置信不变式。 + */ +function makeMockAdapter( + sourceId: string, + raws: readonly RawExternalDataPoint[], + sourceName?: string, +): DataSourceAdapter { + const base = { + sourceId, + fetch: (_query: DataSourceQuery): Promise => + Promise.resolve(annotateExternalDataPoints(raws)), + }; + return sourceName === undefined ? base : { ...base, sourceName }; +} + +/** 最小适配器注册表:以 sourceId 为键插拔注册外部数据源(Req 15.5)。 */ +class AdapterRegistry { + private readonly adapters = new Map(); + + register(adapter: DataSourceAdapter): void { + this.adapters.set(adapter.sourceId, adapter); + } + + get(sourceId: string): DataSourceAdapter { + const adapter = this.adapters.get(sourceId); + if (adapter === undefined) { + throw new Error(`未注册的数据源: ${sourceId}`); + } + return adapter; + } + + get size(): number { + return this.adapters.size; + } +} + +// ---------------------------------------------------------------------------- +// 测试夹具:客户风险维度与指标,以及由外部数据点构造的风险等级解析器。 +// ---------------------------------------------------------------------------- + +/** 构造一个启用的客户风险 Indicator(评分规则细节与本集成测试无关,留空)。 */ +function makeIndicator(id: string, name: string, weight: number): Indicator { + return { + id, + name, + weight, + enabled: true, + scoringRules: [], + evidenceRequired: '', + askPrompt: '', + }; +} + +/** 将外部数值取值夹取为 1-5 的 Risk_Level。 */ +function toRiskLevel(value: number): RiskLevel { + const clamped = Math.min(5, Math.max(1, Math.round(value))); + return clamped as RiskLevel; +} + +/** + * 由外部数据点构造 Scoring_Engine 所需的 RiskLevelResolver。 + * + * 这是适配层与评分引擎之间的"接缝":外部数据经此函数转换为引擎可消费的 + * 风险等级解析器,引擎本身无需感知数据来源(Req 15.5)。 + */ +function buildResolver(points: readonly DataPoint[]): RiskLevelResolver { + const levelByIndicator = new Map(); + for (const point of points) { + if (point.indicatorId !== undefined && typeof point.value === 'number') { + levelByIndicator.set(point.indicatorId, toRiskLevel(point.value)); + } + } + return (indicator: Indicator): RiskLevel => + levelByIndicator.get(indicator.id) ?? 1; +} + +// ---------------------------------------------------------------------------- +// 路径一:mock 数据源验证成功取数路径(Req 15.1, 15.2)。 +// ---------------------------------------------------------------------------- + +describe('外部数据适配集成 - 成功取数路径 (Req 15.1)', () => { + const creditRaws: readonly RawExternalDataPoint[] = [ + { + field: '信用评级', + value: 3, + category: '企业资信', + confidence: 0.9, + indicatorId: 'cust.credit', + }, + { + field: '征信查询次数', + value: 2, + category: '征信', + confidence: 1.4, // 越界值,应被规整至 1 + indicatorId: 'cust.creditInquiry', + }, + ]; + + it('mock 适配器在超时内成功取数, 每个数据点标注为"外部数据"且 Confidence ∈ [0,1]', async () => { + const adapter = makeMockAdapter('mock-credit', creditRaws, '资信 Mock'); + const query: DataSourceQuery = { + subjectName: '示例外包供应商', + categories: ['企业资信', '征信'], + indicatorIds: ['cust.credit', 'cust.creditInquiry'], + }; + + const outcome = await fetchExternalData(adapter, query); + + expect(outcome.status).toBe('success'); + if (outcome.status !== 'success') { + throw new Error('期望成功取数'); + } + expect(outcome.dataPoints).toHaveLength(2); + for (const point of outcome.dataPoints) { + expect(point.provenance).toBe(EXTERNAL_DATA_PROVENANCE); + expect(point.confidence).toBeGreaterThanOrEqual(0); + expect(point.confidence).toBeLessThanOrEqual(1); + } + }); + + it('成功取数经 fetchWithFallback 回填指标取值且不触发降级', async () => { + const adapter = makeMockAdapter('mock-credit', creditRaws); + const query: DataSourceQuery = { + subjectName: '示例外包供应商', + indicatorIds: ['cust.credit', 'cust.creditInquiry'], + }; + + const result = await fetchWithFallback({ adapter, query }); + + expect(result.outcome.status).toBe('success'); + expect(result.usedFallback).toBe(false); + expect(result.dataPoints).toHaveLength(2); + for (const point of result.dataPoints) { + expect(point.provenance).toBe(EXTERNAL_DATA_PROVENANCE); + } + // 数据点确实按请求的 Indicator 标识回填。 + const ids = result.dataPoints.map((point) => point.indicatorId); + expect(ids).toContain('cust.credit'); + expect(ids).toContain('cust.creditInquiry'); + }); +}); + +// ---------------------------------------------------------------------------- +// 路径二:注册新适配器, 无需改 Scoring_Engine 源码即可使用(Req 15.5)。 +// ---------------------------------------------------------------------------- + +describe('外部数据适配集成 - 可插拔扩展无需改 Scoring_Engine (Req 15.5)', () => { + /** 客户风险维度:两个启用指标。 */ + function makeCustomerDimension(): Dimension { + return { + id: 'dim.customer', + name: '客户风险', + weight: 100, + enabled: true, + indicators: [ + makeIndicator('cust.credit', '信用评级', 6), + makeIndicator('cust.litigation', '涉诉风险', 4), + ], + }; + } + + it('用已注册适配器取数, 其结果可直接驱动 scoreDimension 评分', async () => { + const registry = new AdapterRegistry(); + registry.register( + makeMockAdapter('mock-credit', [ + { + field: '信用评级', + value: 3, + category: '企业资信', + confidence: 0.8, + indicatorId: 'cust.credit', + }, + { + field: '涉诉风险', + value: 2, + category: '涉诉', + confidence: 0.7, + indicatorId: 'cust.litigation', + }, + ]), + ); + + const dimension = makeCustomerDimension(); + const query: DataSourceQuery = { + subjectName: '供应商A', + indicatorIds: ['cust.credit', 'cust.litigation'], + }; + + const outcome = await fetchExternalData(registry.get('mock-credit'), query); + expect(outcome.status).toBe('success'); + if (outcome.status !== 'success') { + throw new Error('期望成功取数'); + } + + // 评分引擎源码未改动: 仅用其导出的纯函数 + 适配层产出的 resolver。 + const resolver = buildResolver(outcome.dataPoints); + const score = scoreDimension(dimension, resolver); + + // cust.credit: level 3 × weight 6 = 18; cust.litigation: level 2 × weight 4 = 8 → 26。 + expect(score).toBe(26); + }); + + it('注册一个全新数据源适配器后, 同一评分流程无需改动即可使用 (Req 15.5)', async () => { + const registry = new AdapterRegistry(); + + // 先注册一个既有适配器。 + registry.register(makeMockAdapter('mock-credit', [])); + const sizeBefore = registry.size; + + // 注册一个此前不存在的"工商数据"适配器 —— 仅实现接口并注册, 无任何引擎改动。 + const businessRegistryAdapter = makeMockAdapter( + 'mock-business-registry', + [ + { + field: '注册资本异常', + value: 4, + category: '工商', + confidence: 0.95, + indicatorId: 'cust.credit', + }, + { + field: '失信记录', + value: 5, + category: '失信', + confidence: 0.99, + indicatorId: 'cust.litigation', + }, + ], + '工商数据 Mock', + ); + registry.register(businessRegistryAdapter); + + expect(registry.size).toBe(sizeBefore + 1); + + const dimension = makeCustomerDimension(); + const query: DataSourceQuery = { + subjectName: '供应商B', + indicatorIds: ['cust.credit', 'cust.litigation'], + }; + + // 取出新注册的适配器, 复用与既有适配器完全相同的取数 + 评分链路。 + const outcome = await fetchExternalData( + registry.get('mock-business-registry'), + query, + ); + expect(outcome.status).toBe('success'); + if (outcome.status !== 'success') { + throw new Error('期望成功取数'); + } + for (const point of outcome.dataPoints) { + expect(point.provenance).toBe(EXTERNAL_DATA_PROVENANCE); + } + + const resolver = buildResolver(outcome.dataPoints); + + // 单指标得分: cust.credit level 4 × weight 6 = 24。 + const creditIndicator = dimension.indicators[0]; + expect(creditIndicator).toBeDefined(); + if (creditIndicator === undefined) { + throw new Error('缺少 cust.credit 指标'); + } + expect(scoreIndicator(creditIndicator, resolver(creditIndicator))).toBe(24); + + // 维度得分: cust.credit 4×6=24; cust.litigation 5×4=20 → 44。 + expect(scoreDimension(dimension, resolver)).toBe(44); + }); +}); diff --git a/src/adapters/__tests__/annotate.property51.test.ts b/src/adapters/__tests__/annotate.property51.test.ts new file mode 100644 index 0000000..373f297 --- /dev/null +++ b/src/adapters/__tests__/annotate.property51.test.ts @@ -0,0 +1,121 @@ +/** + * Property 51: 外部数据点来源与置信标注 的属性化测试(External_Data_Adapter,Req 15.2)。 + * + * 属性陈述:对任意成功获取的外部数据点,经来源标注后其 Data_Provenance 必为"外部数据", + * 且 Confidence 落在 [0, 1] 内并保留两位小数。 + * + * 本测试以智能生成器构造任意原始外部数据点 RawExternalDataPoint: + * - 原始 confidence 跨越 [0, 1] 内、负值、>1、极端值与边界值(0 / 1 / 0.005 等), + * 充分覆盖 normalizeConfidence 的夹取与两位小数规整分支; + * - field / value / category / 可选 indicatorId 取遍各形态,使输入空间贴近真实适配器产出。 + * + * 对单点 annotateExternalDataPoint 与批量 annotateExternalDataPoints 均断言: + * - provenance === EXTERNAL_DATA_PROVENANCE("外部数据") + * - confidence ∈ [0, 1] + * - confidence 为两位小数(confidence × 100 在浮点容差内为整数) + * + * Feature: outsourcing-risk-assessment, Property 51: 外部数据点来源与置信标注 + * Validates: Requirements 15.2 + */ + +import { describe, expect, it } from 'vitest'; +import fc from 'fast-check'; +import { + annotateExternalDataPoint, + annotateExternalDataPoints, + EXTERNAL_DATA_CATEGORY_VALUES, + EXTERNAL_DATA_PROVENANCE, + type ExternalDataCategory, + type ExternalDataValue, + type RawExternalDataPoint, +} from '../index.js'; + +// ---------------------------------------------------------------------------- +// 生成器:构造任意原始外部数据点。 +// ---------------------------------------------------------------------------- + +/** 数据点取值:标量(string | number | boolean | null)。 */ +const valueArb: fc.Arbitrary = fc.oneof( + fc.string(), + fc.double({ noNaN: true, noDefaultInfinity: true }), + fc.boolean(), + fc.constant(null), +); + +/** 外部数据类别:取自规定的五类。 */ +const categoryArb: fc.Arbitrary = fc.constantFrom( + ...EXTERNAL_DATA_CATEGORY_VALUES, +); + +/** + * 原始置信度:覆盖 [0,1] 内、越界(负值 / >1)、边界值与极端值, + * 充分检验 normalizeConfidence 的夹取与两位小数规整。 + */ +const rawConfidenceArb: fc.Arbitrary = fc.oneof( + fc.double({ min: 0, max: 1, noNaN: true }), + fc.double({ min: -1000, max: 1000, noNaN: true, noDefaultInfinity: true }), + fc.constantFrom(0, 1, 0.005, 0.125, 0.999, -0.5, 1.5, 42), +); + +/** 原始外部数据点生成器(indicatorId 可选)。 */ +const rawDataPointArb: fc.Arbitrary = fc + .record( + { + field: fc.string({ minLength: 1, maxLength: 16 }), + value: valueArb, + category: categoryArb, + confidence: rawConfidenceArb, + indicatorId: fc.option(fc.string({ minLength: 1, maxLength: 12 }), { + nil: undefined, + }), + }, + { requiredKeys: ['field', 'value', 'category', 'confidence'] }, + ) + .map((r) => { + // exactOptionalPropertyTypes:仅在存在时附加 indicatorId。 + if (r.indicatorId === undefined) { + const { field, value, category, confidence } = r; + return { field, value, category, confidence }; + } + return r as RawExternalDataPoint; + }); + +/** 两位小数判定:value × 100 在浮点容差内为整数。 */ +function isTwoDecimals(value: number): boolean { + return Math.abs(value * 100 - Math.round(value * 100)) < 1e-9; +} + +/** 单点不变式断言:provenance 为"外部数据",confidence ∈ [0,1] 且两位小数。 */ +function assertAnnotated(point: { provenance: string; confidence: number }): void { + expect(point.provenance).toBe(EXTERNAL_DATA_PROVENANCE); + expect(point.confidence).toBeGreaterThanOrEqual(0); + expect(point.confidence).toBeLessThanOrEqual(1); + expect(isTwoDecimals(point.confidence)).toBe(true); +} + +describe('Property 51: 外部数据点来源与置信标注 (Req 15.2)', () => { + it('单点标注后 provenance 恒为"外部数据"且 confidence ∈ [0,1] 两位小数', () => { + fc.assert( + fc.property(rawDataPointArb, (raw) => { + assertAnnotated(annotateExternalDataPoint(raw)); + }), + { numRuns: 100 }, + ); + }); + + it('批量标注的每个数据点均满足来源与置信不变式', () => { + fc.assert( + fc.property( + fc.array(rawDataPointArb, { minLength: 0, maxLength: 12 }), + (raws) => { + const points = annotateExternalDataPoints(raws); + expect(points).toHaveLength(raws.length); + for (const point of points) { + assertAnnotated(point); + } + }, + ), + { numRuns: 100 }, + ); + }); +}); diff --git a/src/adapters/__tests__/fallback.property52.test.ts b/src/adapters/__tests__/fallback.property52.test.ts new file mode 100644 index 0000000..1eff771 --- /dev/null +++ b/src/adapters/__tests__/fallback.property52.test.ts @@ -0,0 +1,227 @@ +/** + * Property 52: 外部数据失败降级回退 的属性化测试(External_Data_Adapter,Req 15.3, 15.4)。 + * + * 属性陈述:对任意外部数据源连接失败、超过约定时长未返回(超时)或返回错误响应的情形, + * 受影响数据点必回退到用户输入并标注 Data_Provenance="用户输入"(Req 15.3);若回退后 + * 用户输入仍不完整(缺失),则必标注为"智能体假设"并继续执行评估、不中断流程(Req 15.4)。 + * + * 本测试以智能生成器构造三类失败场景的适配器(连接失败 / 超时 / 错误响应),并为每个被 + * 请求键随机决定其用户输入是否存在且非缺失、以及智能体假设兜底是否存在。断言: + * - 取数结果恒为 status==="failed"(外部数据不可用),usedFallback===true; + * - 用户输入存在且非缺失的键 → provenance==="用户输入"(Req 15.3); + * - 用户输入缺失的键 → provenance==="智能体假设"(Req 15.4); + * - 无论何种失败场景,fetchWithFallback 恒返回完整解析结果、不抛出、不中断流程(Req 15.4); + * - 失败场景下没有任何数据点被标注为"外部数据"。 + * + * 另以纯函数 resolveWithFallback 在"外部取数失败=空外部数据点"前提下覆盖更广输入空间, + * 验证优先级链路"外部数据 → 用户输入 → 智能体假设"的解析正确性。 + * + * Feature: outsourcing-risk-assessment, Property 52: 外部数据失败降级回退 + * Validates: Requirements 15.3, 15.4 + */ + +import { describe, expect, it } from 'vitest'; +import fc from 'fast-check'; +import { + fetchWithFallback, + resolveWithFallback, + EXTERNAL_DATA_TIMEOUT_MS, + type DataPoint, + type DataSourceAdapter, + type ExternalDataValue, + type FallbackInput, +} from '../index.js'; + +// ---------------------------------------------------------------------------- +// 生成器。 +// ---------------------------------------------------------------------------- + +/** 外部取数失败的三类成因(Req 15.3)。 */ +type FailureKind = '连接失败' | '超时' | '错误响应'; + +const failureKindArb: fc.Arbitrary = fc.constantFrom( + '连接失败', + '超时', + '错误响应', +); + +/** 非缺失取值:非空白字符串 / 数值 / 布尔(含 false、0 等有效值)。 */ +const nonMissingValueArb: fc.Arbitrary = fc.oneof( + fc.string({ minLength: 1, maxLength: 16 }).filter((s) => s.trim() !== ''), + fc.double({ noNaN: true, noDefaultInfinity: true }), + fc.boolean(), +); + +/** 任意取值:在非缺失基础上额外包含 null 与空白串(用于假设候选)。 */ +const anyValueArb: fc.Arbitrary = fc.oneof( + nonMissingValueArb, + fc.constant(null), + fc.constant(' '), +); + +/** 对齐键:非空白字符串。 */ +const keyArb: fc.Arbitrary = fc + .string({ minLength: 1, maxLength: 10 }) + .filter((s) => s.trim() !== ''); + +/** 每个键的回退方案:用户输入是否存在(非缺失)、假设兜底是否存在及其取值。 */ +interface KeyPlan { + userPresent: boolean; + userValue: ExternalDataValue; + assumptionPresent: boolean; + assumptionValue: ExternalDataValue; +} + +const keyPlanArb: fc.Arbitrary = fc.record({ + userPresent: fc.boolean(), + userValue: nonMissingValueArb, + assumptionPresent: fc.boolean(), + assumptionValue: anyValueArb, +}); + +/** 完整场景:唯一键集合、各键回退方案、失败成因。 */ +interface Scenario { + keys: string[]; + plans: KeyPlan[]; + failureKind: FailureKind; +} + +const scenarioArb: fc.Arbitrary = fc + .uniqueArray(keyArb, { minLength: 1, maxLength: 8 }) + .chain((keys) => + fc.record({ + keys: fc.constant(keys), + plans: fc.array(keyPlanArb, { + minLength: keys.length, + maxLength: keys.length, + }), + failureKind: failureKindArb, + }), + ); + +// ---------------------------------------------------------------------------- +// 失败适配器构造。 +// ---------------------------------------------------------------------------- + +/** 构造对应失败成因的适配器;超时场景返回永不结算的 Promise,由超时包装器拒绝。 */ +function makeFailingAdapter(kind: FailureKind): DataSourceAdapter { + return { + sourceId: 'test-failing-source', + fetch: (): Promise => { + if (kind === '超时') { + // 永不结算 → 由 withTimeout 在 timeoutMs 后以超时拒绝。 + return new Promise(() => { + /* never settles */ + }); + } + if (kind === '连接失败') { + return Promise.reject(new Error('ECONNREFUSED 模拟连接失败')); + } + return Promise.reject(new Error('HTTP 500 模拟错误响应')); + }, + }; +} + +/** 由场景构造用户输入与假设候选数组。 */ +function buildFallbacks(scenario: Scenario): { + userInputs: FallbackInput[]; + assumptions: FallbackInput[]; +} { + const userInputs: FallbackInput[] = []; + const assumptions: FallbackInput[] = []; + scenario.keys.forEach((key, i) => { + const plan = scenario.plans[i]!; + if (plan.userPresent) { + userInputs.push({ field: key, value: plan.userValue }); + } + if (plan.assumptionPresent) { + assumptions.push({ field: key, value: plan.assumptionValue }); + } + }); + return { userInputs, assumptions }; +} + +// ---------------------------------------------------------------------------- +// 属性。 +// ---------------------------------------------------------------------------- + +describe('Property 52: 外部数据失败降级回退 (Req 15.3, 15.4)', () => { + it('任意失败/超时/错误响应场景:受影响点回退用户输入,仍缺失则标智能体假设,且不中断', async () => { + await fc.assert( + fc.asyncProperty(scenarioArb, async (scenario) => { + const adapter = makeFailingAdapter(scenario.failureKind); + const { userInputs, assumptions } = buildFallbacks(scenario); + // 超时场景使用极短超时上限以保证测试快速;其余场景立即拒绝,上限无影响。 + const timeoutMs = + scenario.failureKind === '超时' ? 5 : EXTERNAL_DATA_TIMEOUT_MS; + + // 恒不抛出 / 不中断(Req 15.4):asyncProperty 等待其正常结算。 + const result = await fetchWithFallback({ + adapter, + query: { subjectName: '测试主体' }, + userInputs, + assumptions, + requestedKeys: scenario.keys, + timeoutMs, + }); + + // 外部数据不可用:取数失败且发生回退。 + expect(result.outcome.status).toBe('failed'); + expect(result.usedFallback).toBe(true); + expect(result.dataPoints).toHaveLength(scenario.keys.length); + + const byField = new Map(result.dataPoints.map((p) => [p.field, p])); + scenario.keys.forEach((key, i) => { + const plan = scenario.plans[i]!; + const point = byField.get(key); + expect(point).toBeDefined(); + // 失败场景下不应出现"外部数据"来源。 + expect(point!.provenance).not.toBe('外部数据'); + if (plan.userPresent) { + // 受影响数据点回退到用户输入(Req 15.3)。 + expect(point!.provenance).toBe('用户输入'); + expect(point!.value).toBe(plan.userValue); + } else { + // 回退后仍缺失 → 标注智能体假设并继续(Req 15.4)。 + expect(point!.provenance).toBe('智能体假设'); + } + }); + }), + { numRuns: 100 }, + ); + }); + + it('纯函数解析:空外部数据点时按 用户输入→智能体假设 优先级解析(Req 15.3, 15.4)', () => { + fc.assert( + fc.property(scenarioArb, (scenario) => { + const { userInputs, assumptions } = buildFallbacks(scenario); + // 外部取数失败 → 外部数据点为空。 + const resolved = resolveWithFallback( + [], + scenario.keys, + userInputs, + assumptions, + ); + + expect(resolved).toHaveLength(scenario.keys.length); + const byField = new Map(resolved.map((p) => [p.field, p])); + scenario.keys.forEach((key, i) => { + const plan = scenario.plans[i]!; + const point = byField.get(key); + expect(point).toBeDefined(); + expect(point!.provenance).not.toBe('外部数据'); + if (plan.userPresent) { + expect(point!.provenance).toBe('用户输入'); + } else { + expect(point!.provenance).toBe('智能体假设'); + } + // 来源恒为三态之一。 + expect(['用户输入', '外部数据', '智能体假设']).toContain( + point!.provenance, + ); + }); + }), + { numRuns: 100 }, + ); + }); +}); diff --git a/src/adapters/annotate.ts b/src/adapters/annotate.ts new file mode 100644 index 0000000..c647a35 --- /dev/null +++ b/src/adapters/annotate.ts @@ -0,0 +1,52 @@ +/** + * 外部数据成功取数的来源标注(Req 15.2)。 + * + * 将适配器产出的原始数据点统一标注为 Data_Provenance="外部数据",并经 + * {@link normalizeConfidence} 将 Confidence 规整至 [0, 1] 区间、保留两位小数。 + * 所有数据源经此单一出口产出数据点,保证 Req 15.2 在全部适配器上的一致性。 + */ + +import { normalizeConfidence } from '../domain/provenance.js'; +import { + EXTERNAL_DATA_PROVENANCE, + type DataPoint, + type RawExternalDataPoint, +} from './types.js'; + +/** + * 为单个原始外部数据点施加成功取数的来源标注(Req 15.2)。 + * + * 不变式:返回数据点的 `provenance` 恒为"外部数据",`confidence` 落在 [0, 1] 内 + * 并保留两位小数。该函数为纯函数、无副作用。 + * + * @param raw 适配器产出的原始数据点。 + * @returns 完成来源标注与置信度规整的数据点。 + * @throws {RangeError} 当 `raw.confidence` 非有限数(NaN / ±Infinity)时。 + */ +export function annotateExternalDataPoint(raw: RawExternalDataPoint): DataPoint { + const point: DataPoint = { + field: raw.field, + value: raw.value, + category: raw.category, + provenance: EXTERNAL_DATA_PROVENANCE, + confidence: normalizeConfidence(raw.confidence), + }; + + // exactOptionalPropertyTypes:仅在存在时附加可选字段,避免赋值 undefined。 + if (raw.indicatorId !== undefined) { + return { ...point, indicatorId: raw.indicatorId }; + } + return point; +} + +/** + * 批量为原始外部数据点施加成功取数的来源标注(Req 15.2)。 + * + * @param raws 原始数据点集合。 + * @returns 完成来源标注的数据点集合(顺序与输入一致)。 + */ +export function annotateExternalDataPoints( + raws: readonly RawExternalDataPoint[], +): DataPoint[] { + return raws.map(annotateExternalDataPoint); +} diff --git a/src/adapters/fallback.ts b/src/adapters/fallback.ts new file mode 100644 index 0000000..1a952ab --- /dev/null +++ b/src/adapters/fallback.ts @@ -0,0 +1,457 @@ +/** + * External_Data_Adapter 超时 / 失败降级回退(Req 15.3, 15.4)。 + * + * 本模块在适配器成功取数(Req 15.2)之上补齐降级回退链路: + * + * - 连接失败 / 超过 10 秒未返回 / 返回错误响应(Req 15.3):受影响数据点回退到 + * 用户输入并标注 Data_Provenance="用户输入"。 + * - 回退后用户输入仍不完整(Req 15.4):将缺失数据点标注为"智能体假设", + * 并继续执行评估、不中断流程。 + * + * 设计要点: + * - {@link withTimeout} 为通用超时包装器,超过 {@link EXTERNAL_DATA_TIMEOUT_MS} + * 即视为失败(Req 15.1 的 10 秒约定)。 + * - {@link fetchExternalData} 捕获连接失败 / 超时 / 错误响应并归一为 {@link FetchOutcome}, + * 恒不抛出,保证主流程不被中断(Req 15.4)。 + * - {@link resolveWithFallback} 为纯函数,按"外部数据 → 用户输入 → 智能体假设" + * 的优先级为每个被请求项解析最终数据点。 + * - {@link fetchWithFallback} 将取数与解析串联,对外提供一站式入口。 + */ + +import { + markAsAssumption, + normalizeConfidence, +} from '../domain/provenance.js'; +import type { Confidence, DataProvenance } from '../domain/common.js'; +import { + EXTERNAL_DATA_TIMEOUT_MS, + type DataPoint, + type DataSourceAdapter, + type DataSourceQuery, + type ExternalDataCategory, + type ExternalDataValue, +} from './types.js'; + +/** 用户输入回退的来源标注常量(Req 15.3)。与 DataProvenance 三态之一对齐。 */ +export const USER_INPUT_PROVENANCE: DataProvenance = '用户输入'; + +/** 用户输入回退缺省置信度(评估者直接录入,默认视为确定)。 */ +export const DEFAULT_USER_INPUT_CONFIDENCE = 1 as const; + +/** "智能体假设"兜底缺省置信度(缺失项的默认取值,默认视为不确定)。 */ +export const DEFAULT_ASSUMPTION_CONFIDENCE = 0 as const; + +/** + * 计时器全局函数的最小类型门面。 + * + * 本工程未引入 DOM / Node 的全局类型库,故以受限的 `globalThis` 视图获取 + * `setTimeout` / `clearTimeout`,避免对环境类型库的依赖。 + */ +const timers = globalThis as unknown as { + setTimeout: (handler: () => void, ms: number) => TimerHandle; + clearTimeout: (handle: TimerHandle) => void; +}; + +/** 计时器句柄的不透明类型(可选携带 unref)。 */ +interface TimerHandle { + unref?: () => void; +} + +/** + * 外部数据取数失败原因(Req 15.3)。 + * - 超时:超过约定时长未返回。 + * - 失败:连接失败或返回错误响应(二者降级处理一致,统一归类)。 + */ +export type FetchFailureReason = '超时' | '失败'; + +/** + * 外部数据取数结果(恒不抛出,保证主流程不中断,Req 15.4)。 + * - success:成功取得已完成来源标注的数据点(Req 15.2)。 + * - failed:连接失败 / 超时 / 错误响应,附失败原因与原始错误(Req 15.3)。 + */ +export type FetchOutcome = + | { readonly status: 'success'; readonly dataPoints: DataPoint[] } + | { + readonly status: 'failed'; + readonly reason: FetchFailureReason; + readonly error: unknown; + }; + +/** + * 回退输入项:用户输入或智能体假设的候选取值。 + * + * 以 `indicatorId`(若存在)否则 `field` 作为与外部数据点对齐的键。 + */ +export interface FallbackInput { + /** 数据点字段名 / 指标键。 */ + field: string; + /** 关联的 Indicator 标识;存在时优先作为对齐键。 */ + indicatorId?: string; + /** 数据点所属类别,可选。 */ + category?: ExternalDataCategory; + /** 取值;null 或空白字符串视为缺失。 */ + value: ExternalDataValue; + /** 置信度(将被规整至 [0,1] 两位小数);省略时取对应缺省值。 */ + confidence?: number; +} + +/** + * 经降级回退解析后的最终数据点。 + * + * 不变式:`provenance` 为三态之一,`confidence` 落在 [0,1] 内并保留两位小数。 + */ +export interface ResolvedDataPoint { + /** 数据点字段名 / 指标键。 */ + field: string; + /** 取值。 */ + value: ExternalDataValue; + /** 数据点所属类别,可选。 */ + category?: ExternalDataCategory; + /** 来源标注:外部数据 / 用户输入 / 智能体假设。 */ + provenance: DataProvenance; + /** 置信度,取值 [0,1]、保留两位小数。 */ + confidence: Confidence; + /** 关联的 Indicator 标识,可选。 */ + indicatorId?: string; +} + +/** {@link fetchWithFallback} 的参数。 */ +export interface FetchWithFallbackOptions { + /** 外部数据源适配器。 */ + adapter: DataSourceAdapter; + /** 查询请求。 */ + query: DataSourceQuery; + /** 用户输入回退候选(Req 15.3)。 */ + userInputs?: readonly FallbackInput[]; + /** 智能体假设兜底候选(Req 15.4);缺省时缺失项取值为 null。 */ + assumptions?: readonly FallbackInput[]; + /** + * 待解析的键集合(indicatorId 或 field)。 + * 省略时回退至 `query.indicatorIds`,再回退至外部/用户/假设候选键的并集。 + */ + requestedKeys?: readonly string[]; + /** "智能体假设"兜底缺省置信度,默认 {@link DEFAULT_ASSUMPTION_CONFIDENCE}。 */ + assumptionConfidence?: number; + /** 单次请求超时上限(毫秒),默认 {@link EXTERNAL_DATA_TIMEOUT_MS}(10 秒)。 */ + timeoutMs?: number; +} + +/** {@link fetchWithFallback} 的返回结果。 */ +export interface FetchWithFallbackResult { + /** 解析后的最终数据点集合(按解析键顺序)。 */ + dataPoints: ResolvedDataPoint[]; + /** 外部取数结果(成功 / 失败原因)。 */ + outcome: FetchOutcome; + /** 是否发生降级回退(外部取数失败或存在非"外部数据"来源的数据点)。 */ + usedFallback: boolean; +} + +/** + * 外部数据取数超时错误(Req 15.1 的 10 秒约定)。 + */ +export class ExternalDataTimeoutError extends Error { + /** 触发超时的时长(毫秒)。 */ + readonly timeoutMs: number; + + constructor(timeoutMs: number) { + super(`外部数据取数超过 ${timeoutMs} 毫秒未返回,触发降级回退`); + this.name = 'ExternalDataTimeoutError'; + this.timeoutMs = timeoutMs; + } +} + +/** + * 判断回退取值是否缺失:null 或空白字符串视为缺失(Req 15.4)。 + * + * 注意:布尔 false 与数值 0 为有效取值,不视为缺失。 + * + * @param value 待判断取值。 + * @returns 当且仅当取值缺失时返回 true。 + */ +export function isMissingValue(value: ExternalDataValue): boolean { + return value === null || (typeof value === 'string' && value.trim() === ''); +} + +/** + * 通用超时包装器:在 `timeoutMs` 内未结算则以 {@link ExternalDataTimeoutError} 拒绝(Req 15.1)。 + * + * 一旦底层操作先行结算(成功或失败),计时器立即清除;超时优先结算后底层操作的 + * 后续结算被忽略,避免重复结算。 + * + * @param operation 产出 Promise 的取数操作(惰性,便于在调用内统一捕获同步抛出)。 + * @param timeoutMs 超时上限(毫秒),默认 {@link EXTERNAL_DATA_TIMEOUT_MS}。 + * @returns 底层操作的结果。 + * @throws {ExternalDataTimeoutError} 超过 `timeoutMs` 未返回时。 + */ +export function withTimeout( + operation: () => Promise, + timeoutMs: number = EXTERNAL_DATA_TIMEOUT_MS, +): Promise { + return new Promise((resolve, reject) => { + let settled = false; + const timer = timers.setTimeout(() => { + if (!settled) { + settled = true; + reject(new ExternalDataTimeoutError(timeoutMs)); + } + }, timeoutMs); + + // 避免计时器在 Node 环境阻止进程退出。 + timer.unref?.(); + + // 以 Promise.resolve 包裹,统一捕获 operation() 同步抛出的情形。 + Promise.resolve() + .then(operation) + .then( + (value) => { + if (!settled) { + settled = true; + timers.clearTimeout(timer); + resolve(value); + } + }, + (error: unknown) => { + if (!settled) { + settled = true; + timers.clearTimeout(timer); + reject(error instanceof Error ? error : new Error(String(error))); + } + }, + ); + }); +} + +/** + * 执行外部取数并归一化失败(Req 15.1, 15.3)。 + * + * 在超时上限内尝试 `adapter.fetch`;连接失败 / 超时 / 错误响应均被捕获并归一为 + * `status: 'failed'` 的 {@link FetchOutcome}。该函数恒不抛出,保证主流程不中断(Req 15.4)。 + * + * @param adapter 外部数据源适配器。 + * @param query 查询请求。 + * @param timeoutMs 超时上限(毫秒),默认 {@link EXTERNAL_DATA_TIMEOUT_MS}。 + * @returns 取数结果(成功数据点或失败原因)。 + */ +export async function fetchExternalData( + adapter: DataSourceAdapter, + query: DataSourceQuery, + timeoutMs: number = EXTERNAL_DATA_TIMEOUT_MS, +): Promise { + try { + const dataPoints = await withTimeout(() => adapter.fetch(query), timeoutMs); + return { status: 'success', dataPoints }; + } catch (error) { + const reason: FetchFailureReason = + error instanceof ExternalDataTimeoutError ? '超时' : '失败'; + return { status: 'failed', reason, error }; + } +} + +/** 计算数据点 / 回退输入的对齐键:indicatorId 优先,否则 field。 */ +function keyOf(point: { + field: string; + indicatorId?: string; +}): string { + return point.indicatorId ?? point.field; +} + +/** 由外部数据点构造解析结果(来源恒为"外部数据")。 */ +function fromExternal(point: DataPoint): ResolvedDataPoint { + const resolved: ResolvedDataPoint = { + field: point.field, + value: point.value, + provenance: point.provenance, + confidence: point.confidence, + }; + return withOptional(resolved, point.category, point.indicatorId); +} + +/** 由回退输入构造解析结果,施加指定来源与置信度。 */ +function fromFallback( + input: FallbackInput, + provenance: DataProvenance, + defaultConfidence: number, +): ResolvedDataPoint { + const resolved: ResolvedDataPoint = { + field: input.field, + value: input.value, + provenance, + confidence: normalizeConfidence(input.confidence ?? defaultConfidence), + }; + return withOptional(resolved, input.category, input.indicatorId); +} + +/** 构造缺失项的"智能体假设"兜底数据点(Req 15.4)。 */ +function assumedFor( + key: string, + assumption: FallbackInput | undefined, + assumptionConfidence: number, +): ResolvedDataPoint { + // 即便存在前序来源,markAsAssumption 也保证结果恒为"智能体假设"(Req 3.7 单调永久)。 + const provenance = markAsAssumption(USER_INPUT_PROVENANCE); + const base: ResolvedDataPoint = { + field: assumption?.field ?? key, + value: assumption?.value ?? null, + provenance, + confidence: normalizeConfidence( + assumption?.confidence ?? assumptionConfidence, + ), + }; + return withOptional(base, assumption?.category, assumption?.indicatorId ?? key); +} + +/** exactOptionalPropertyTypes:仅在存在时附加可选字段,避免赋值 undefined。 */ +function withOptional( + point: ResolvedDataPoint, + category: ExternalDataCategory | undefined, + indicatorId: string | undefined, +): ResolvedDataPoint { + let result = point; + if (category !== undefined) { + result = { ...result, category }; + } + if (indicatorId !== undefined) { + result = { ...result, indicatorId }; + } + return result; +} + +/** + * 按"外部数据 → 用户输入 → 智能体假设"优先级为每个被请求键解析最终数据点(Req 15.3, 15.4)。 + * + * 解析规则(对每个 key): + * 1. 若外部数据点存在且取值非缺失 → 采用"外部数据"。 + * 2. 否则若用户输入存在且取值非缺失 → 回退"用户输入"(Req 15.3)。 + * 3. 否则 → 标注"智能体假设"并采用假设候选取值(无候选则取 null),继续不中断(Req 15.4)。 + * + * 该函数为纯函数、无副作用,输出顺序与 `keys` 一致。 + * + * @param externalPoints 外部成功取得的数据点(取数失败时传空数组)。 + * @param keys 待解析的键集合(indicatorId 或 field)。 + * @param userInputs 用户输入回退候选。 + * @param assumptions 智能体假设兜底候选。 + * @param assumptionConfidence "智能体假设"缺省置信度。 + * @returns 解析后的数据点集合。 + */ +export function resolveWithFallback( + externalPoints: readonly DataPoint[], + keys: readonly string[], + userInputs: readonly FallbackInput[] = [], + assumptions: readonly FallbackInput[] = [], + assumptionConfidence: number = DEFAULT_ASSUMPTION_CONFIDENCE, +): ResolvedDataPoint[] { + const externalByKey = indexBy(externalPoints, keyOf); + const userByKey = indexBy(userInputs, keyOf); + const assumptionByKey = indexBy(assumptions, keyOf); + + return keys.map((key) => { + const external = externalByKey.get(key); + if (external !== undefined && !isMissingValue(external.value)) { + return fromExternal(external); + } + + const userInput = userByKey.get(key); + if (userInput !== undefined && !isMissingValue(userInput.value)) { + return fromFallback( + userInput, + USER_INPUT_PROVENANCE, + DEFAULT_USER_INPUT_CONFIDENCE, + ); + } + + return assumedFor(key, assumptionByKey.get(key), assumptionConfidence); + }); +} + +/** 以键函数构造 Map;同键后者覆盖前者。 */ +function indexBy( + items: readonly T[], + key: (item: T) => string, +): Map { + const map = new Map(); + for (const item of items) { + map.set(key(item), item); + } + return map; +} + +/** + * 取数 + 降级回退一站式入口(Req 15.3, 15.4)。 + * + * 流程: + * 1. 经 {@link fetchExternalData} 在超时上限内取数;失败不抛出(Req 15.3)。 + * 2. 解析待处理键集合:`requestedKeys` → `query.indicatorIds` → 候选键并集。 + * 3. 经 {@link resolveWithFallback} 按优先级解析最终数据点(Req 15.3, 15.4)。 + * + * 无论外部取数成功与否,本函数恒返回完整解析结果、不中断流程(Req 15.4)。 + * + * @param options 取数与回退参数。 + * @returns 解析结果、外部取数结果与是否回退标志。 + */ +export async function fetchWithFallback( + options: FetchWithFallbackOptions, +): Promise { + const { + adapter, + query, + userInputs = [], + assumptions = [], + requestedKeys, + assumptionConfidence = DEFAULT_ASSUMPTION_CONFIDENCE, + timeoutMs = EXTERNAL_DATA_TIMEOUT_MS, + } = options; + + const outcome = await fetchExternalData(adapter, query, timeoutMs); + const externalPoints = outcome.status === 'success' ? outcome.dataPoints : []; + + const keys = resolveKeys( + requestedKeys, + query, + externalPoints, + userInputs, + assumptions, + ); + + const dataPoints = resolveWithFallback( + externalPoints, + keys, + userInputs, + assumptions, + assumptionConfidence, + ); + + const usedFallback = + outcome.status === 'failed' || + dataPoints.some((point) => point.provenance !== '外部数据'); + + return { dataPoints, outcome, usedFallback }; +} + +/** + * 解析待处理键集合:`requestedKeys` 优先,其次 `query.indicatorIds`, + * 最后取外部 / 用户 / 假设候选键的并集(保持首次出现顺序、去重)。 + */ +function resolveKeys( + requestedKeys: readonly string[] | undefined, + query: DataSourceQuery, + externalPoints: readonly DataPoint[], + userInputs: readonly FallbackInput[], + assumptions: readonly FallbackInput[], +): string[] { + if (requestedKeys !== undefined) { + return dedupe(requestedKeys); + } + if (query.indicatorIds !== undefined && query.indicatorIds.length > 0) { + return dedupe(query.indicatorIds); + } + return dedupe([ + ...externalPoints.map(keyOf), + ...userInputs.map(keyOf), + ...assumptions.map(keyOf), + ]); +} + +/** 去重并保持首次出现顺序。 */ +function dedupe(keys: readonly string[]): string[] { + return [...new Set(keys)]; +} diff --git a/src/adapters/index.ts b/src/adapters/index.ts new file mode 100644 index 0000000..7f7b8ef --- /dev/null +++ b/src/adapters/index.ts @@ -0,0 +1,15 @@ +/** + * External_Data_Adapter 外部数据适配层模块(Req 15)。 + * + * 职责: + * - 定义可插拔的 {@link DataSourceAdapter} 接口,新增数据源无需改动 + * Scoring_Engine 源代码(Req 15.1, 15.5)。 + * - 为成功取得的数据点统一标注 Data_Provenance="外部数据" 与 [0,1] Confidence + * ({@link annotateExternalDataPoint} / {@link annotateExternalDataPoints},Req 15.2)。 + * - 超时 / 失败 / 错误响应的降级回退:回退用户输入("用户输入"),仍缺失则标注 + * "智能体假设"并继续不中断({@link fetchWithFallback} / {@link resolveWithFallback},Req 15.3, 15.4)。 + */ + +export * from './types.js'; +export * from './annotate.js'; +export * from './fallback.js'; diff --git a/src/adapters/types.ts b/src/adapters/types.ts new file mode 100644 index 0000000..1e576c7 --- /dev/null +++ b/src/adapters/types.ts @@ -0,0 +1,134 @@ +/** + * External_Data_Adapter 适配层核心类型(Req 15)。 + * + * 定义可插拔的 {@link DataSourceAdapter} 接口及其查询/数据点契约。设计目标是 + * 让新增外部数据源(企业资信 / 征信 / 涉诉 / 失信 / 工商)只需实现该接口并注册, + * 而无需改动 Scoring_Engine 源代码(Req 15.5)。 + * + * 契约要点: + * - 单次请求遵循 10 秒超时约定({@link EXTERNAL_DATA_TIMEOUT_MS},Req 15.1)。 + * - 成功取数的每个数据点其 Data_Provenance 恒为"外部数据"且 Confidence 落在 + * [0, 1] 内(Req 15.2);该标注由 {@link annotateExternalDataPoint} 统一施加。 + * + * 注意:超时 / 失败 / 错误响应的降级回退(Req 15.3, 15.4)见 fallback.ts, + * 本模块仅定义接口与成功路径的来源标注。 + */ + +import type { Confidence, DataProvenance } from '../domain/common.js'; + +/** + * 外部数据单次请求的超时上限(毫秒,Req 15.1)。 + * 超过该时长未返回视为失败,触发降级回退(回退逻辑见 fallback.ts)。 + */ +export const EXTERNAL_DATA_TIMEOUT_MS = 10_000 as const; + +/** + * 成功取数的来源标注常量(Req 15.2)。 + * 与 DataProvenance 三态之一"外部数据"对齐。 + */ +export const EXTERNAL_DATA_PROVENANCE: DataProvenance = '外部数据'; + +/** + * 外部数据类别(Req 15.1):企业资信、征信、涉诉、失信、工商。 + */ +export type ExternalDataCategory = + | '企业资信' + | '征信' + | '涉诉' + | '失信' + | '工商'; + +/** ExternalDataCategory 的全部取值,便于运行时校验与遍历。 */ +export const EXTERNAL_DATA_CATEGORY_VALUES = [ + '企业资信', + '征信', + '涉诉', + '失信', + '工商', +] as const; + +/** + * 外部数据点的取值类型。结构化原值在被引擎消费前一般规整为标量。 + */ +export type ExternalDataValue = string | number | boolean | null; + +/** + * 外部数据查询请求。描述待查询主体与期望取得的数据类别。 + */ +export interface DataSourceQuery { + /** 被查询主体(企业)名称。 */ + subjectName: string; + /** 被查询主体唯一标识(如统一社会信用代码),可选。 */ + subjectId?: string; + /** 期望取得的数据类别;省略表示由适配器决定其支持的全部类别。 */ + categories?: readonly ExternalDataCategory[]; + /** 关联的客户风险相关 Indicator 标识集合,用于回填指标取值(Req 15.1)。 */ + indicatorIds?: readonly string[]; +} + +/** + * 适配器返回的原始数据点(尚未施加来源标注)。 + * + * 适配器实现可仅产出原始数据点,再经 {@link annotateExternalDataPoint} 统一 + * 标注 Data_Provenance="外部数据" 与规整后的 Confidence,从而保证 Req 15.2 + * 在所有数据源上的一致性。 + */ +export interface RawExternalDataPoint { + /** 数据点字段名 / 指标键。 */ + field: string; + /** 数据点取值。 */ + value: ExternalDataValue; + /** 数据点所属类别。 */ + category: ExternalDataCategory; + /** 原始置信度(将由 normalizeConfidence 夹取至 [0,1] 并保留两位小数)。 */ + confidence: number; + /** 关联的客户风险相关 Indicator 标识,可选。 */ + indicatorId?: string; +} + +/** + * 成功取得并完成来源标注的外部数据点(Req 15.2)。 + * + * 不变式:`provenance` 恒为"外部数据",`confidence` 落在 [0, 1] 内并保留两位小数。 + */ +export interface DataPoint { + /** 数据点字段名 / 指标键。 */ + field: string; + /** 数据点取值。 */ + value: ExternalDataValue; + /** 数据点所属类别。 */ + category: ExternalDataCategory; + /** 来源标注,成功取数恒为"外部数据"(Req 15.2)。 */ + provenance: DataProvenance; + /** 置信度,取值 [0,1]、保留两位小数(Req 15.2)。 */ + confidence: Confidence; + /** 关联的客户风险相关 Indicator 标识,可选。 */ + indicatorId?: string; +} + +/** + * 可插拔的外部数据源适配器接口(Req 15.1, 15.5)。 + * + * 每个外部数据源实现该接口并以唯一 `sourceId` 注册即可被 System 使用, + * 新增数据源无需改动 Scoring_Engine 源代码(Req 15.5)。 + * + * 契约: + * - `fetch` 单次请求应遵循 {@link EXTERNAL_DATA_TIMEOUT_MS}(10 秒)超时约定(Req 15.1)。 + * - 成功返回的每个数据点须满足来源标注不变式(Req 15.2),推荐经 + * {@link annotateExternalDataPoint} 产出以保证一致性。 + */ +export interface DataSourceAdapter { + /** 适配器唯一标识,用于注册与插拔。 */ + readonly sourceId: string; + /** 适配器可读名称,可选。 */ + readonly sourceName?: string; + /** 该适配器支持的数据类别,可选;用于路由查询。 */ + readonly supportedCategories?: readonly ExternalDataCategory[]; + /** + * 取数:依据查询返回已完成来源标注的数据点集合。 + * + * @param query 查询请求。 + * @returns 成功取得并标注的数据点集合(Req 15.1, 15.2)。 + */ + fetch(query: DataSourceQuery): Promise; +} diff --git a/src/auth/index.ts b/src/auth/index.ts new file mode 100644 index 0000000..de6c2c0 --- /dev/null +++ b/src/auth/index.ts @@ -0,0 +1,96 @@ +/** + * 认证与权限模块(生产级基础)。 + * + * 当前实现:基于 JWT 的无状态认证 + 角色权限校验中间件。 + * 密钥取自环境变量 AUTH_SECRET,未配置时降级为无校验(演示模式)。 + */ + +import type { Context, Next } from 'hono'; +import { createHmac } from 'node:crypto'; + +export type AuthRole = '商务/销售' | '风控' | '管理层'; + +export interface AuthPayload { + username: string; + role: AuthRole; + iat: number; + exp: number; +} + +const SECRET = (): string => process.env.AUTH_SECRET ?? ''; + +/** 简易 HMAC-SHA256 签名(不依赖外部库,生产建议替换为 jose)。 */ +function base64url(buf: Buffer): string { + return buf.toString('base64url'); +} + +function sign(payload: object): string { + if (SECRET() === '') return ''; // 演示模式不签发 + const header = base64url(Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))); + const body = base64url(Buffer.from(JSON.stringify(payload))); + const sig = base64url(createHmac('sha256', SECRET()).update(`${header}.${body}`).digest()); + return `${header}.${body}.${sig}`; +} + +function verify(token: string): AuthPayload | null { + if (SECRET() === '') return null; + try { + const [header, body, sig] = token.split('.'); + if (!header || !body || !sig) return null; + const expected = base64url(createHmac('sha256', SECRET()).update(`${header}.${body}`).digest()); + if (sig !== expected) return null; + const payload = JSON.parse(Buffer.from(body, 'base64url').toString()) as AuthPayload; + if (payload.exp < Date.now() / 1000) return null; + return payload; + } catch { + return null; + } +} + +/** 签发 JWT(登录成功后调用)。 */ +export function issueToken(username: string, role: AuthRole): string { + const now = Math.floor(Date.now() / 1000); + return sign({ username, role, iat: now, exp: now + 86400 }); +} + +/** 无需鉴权的公共路径(登录与健康检查)。 */ +const PUBLIC_PATHS = new Set(['/api/health', '/api/auth/login', '/api/llm/status']); + +/** + * Hono 中间件:从 Authorization Bearer token 解析并注入当前用户(供 requireRole 使用)。 + * + * 设计:本中间件**只负责识别身份、不负责拦截**——读操作保持开放,敏感写操作由 + * {@link requireRole} 按角色拦截。AUTH_SECRET 未配置时为演示模式(不识别身份)。 + * 这样开启鉴权后既能强制敏感操作的角色校验,又不破坏只读接口与看板。 + */ +export function authMiddleware() { + return async (c: Context, next: Next): Promise => { + if (SECRET() === '' || PUBLIC_PATHS.has(c.req.path)) { + await next(); + return; + } + const auth = c.req.header('Authorization'); + if (auth !== undefined && auth.startsWith('Bearer ')) { + const payload = verify(auth.slice(7)); + if (payload !== null) { + c.set('user', payload); + } + } + await next(); + }; +} + +/** 角色权限校验中间件。 */ +export function requireRole(...roles: AuthRole[]) { + return async (c: Context, next: Next): Promise => { + if (SECRET() === '') { + await next(); + return; + } + const user = c.get('user') as AuthPayload | undefined; + if (user === undefined || !roles.includes(user.role)) { + return c.json({ error: '权限不足' }, 403); + } + await next(); + }; +} diff --git a/src/classifier/__tests__/classifier.industry.test.ts b/src/classifier/__tests__/classifier.industry.test.ts new file mode 100644 index 0000000..c556edd --- /dev/null +++ b/src/classifier/__tests__/classifier.industry.test.ts @@ -0,0 +1,67 @@ +/** + * Classifier 行业语义识别单元测试(Req 1.2)。 + * + * 以代表性项目描述验证两条分支: + * 1. 可识别行业:描述命中某行业关键词时,Classifier 输出该行业且置信度 > 0; + * 2. "未识别"分支:描述不含任何行业关键词时,Classifier 输出"未识别"且置信度为 0。 + * + * 本文件仅覆盖行业判定语义,业务类型与输入校验由 classifier.test.ts 覆盖。 + */ + +import { describe, expect, it } from 'vitest'; +import { INDUSTRY_UNRECOGNIZED } from '../../domain/common.js'; +import { INDUSTRY_KEYWORDS } from '../keywords.js'; +import { classify } from '../classifier.js'; + +describe('classify - 可识别行业语义识别 (Req 1.2)', () => { + // 每个行业各一条代表性描述,关键词强命中目标行业、不命中其他行业。 + const cases: ReadonlyArray = [ + ['制造业', '为某大型制造工厂提供生产线车间装配的人员外包服务。'], + ['信息技术', '承接软件系统开发与互联网研发的IT项目外包服务。'], + ['金融业', '为银行金融机构及保险证券公司提供用工外包服务。'], + ['零售业', '为零售门店商超与电商企业提供促销人员外包服务。'], + ['物流业', '为物流仓储配送快递企业提供分拣人员外包服务。'], + ['建筑业', '为建筑施工工地的工程项目提供劳务用工外包服务。'], + ['餐饮业', '为连锁餐饮餐厅后厨提供厨工与备餐人员外包服务。'], + ['医疗健康', '为医疗医院提供护理及辅助岗位的人员用工外包服务。'], + ['客服服务', '为呼叫中心客服坐席团队提供运营人员外包服务。'], + ]; + + it('词典中的全部行业均被本测试覆盖', () => { + const covered = new Set(cases.map(([industry]) => industry)); + for (const industry of Object.keys(INDUSTRY_KEYWORDS)) { + expect(covered.has(industry)).toBe(true); + } + }); + + for (const [expectedIndustry, description] of cases) { + it(`识别"${expectedIndustry}"行业`, () => { + const result = classify(description); + expect(result.industry).toBe(expectedIndustry); + expect(result.industry).not.toBe(INDUSTRY_UNRECOGNIZED); + expect(result.industryConfidence).toBeGreaterThan(0); + expect(result.industryConfidence).toBeLessThanOrEqual(1); + }); + } +}); + +describe('classify - 行业"未识别"分支 (Req 1.2)', () => { + it('描述不含任何行业关键词时输出"未识别"且置信度为 0', () => { + const result = classify('提供一般性人力用工外包,未提及任何具体所属领域信息。'); + expect(result.industry).toBe(INDUSTRY_UNRECOGNIZED); + expect(result.industryConfidence).toBe(0); + }); + + it('纯业务类型描述无行业线索时不误判行业', () => { + const result = classify('本项目为劳务派遣用工,需派遣被派遣员工至客户现场。'); + expect(result.industry).toBe(INDUSTRY_UNRECOGNIZED); + expect(result.industryConfidence).toBe(0); + }); + + it('"未识别"分支不返回行业候选且无需行业确认', () => { + const result = classify('提供综合性人员外包,描述中未涉及任何行业领域关键词。'); + expect(result.industry).toBe(INDUSTRY_UNRECOGNIZED); + expect(result.needsIndustryConfirm).toBe(false); + expect(result.industryCandidates).toEqual([]); + }); +}); diff --git a/src/classifier/__tests__/classifier.property1.test.ts b/src/classifier/__tests__/classifier.property1.test.ts new file mode 100644 index 0000000..9cbedf7 --- /dev/null +++ b/src/classifier/__tests__/classifier.property1.test.ts @@ -0,0 +1,100 @@ +/** + * Classifier 属性化测试 —— Property 1: 业务类型判定唯一且取最高置信。 + * + * Feature: outsourcing-risk-assessment, Property 1: 业务类型判定唯一且取最高置信 + * + * 对任意有效项目描述产生的业务类型置信分布,Classifier 输出的 businessType + * 必为五类业务类型中 Confidence 最高的唯一一项。 + * + * Validates: Requirements 1.1 + */ + +import { describe, expect, it } from 'vitest'; +import fc from 'fast-check'; +import { BUSINESS_TYPE_VALUES } from '../../domain/common.js'; +import { BUSINESS_TYPE_KEYWORDS, type KeywordWeight } from '../keywords.js'; +import { classify, scoreLabels } from '../classifier.js'; + +/** + * 五类业务类型的全部关键词(去重),用作智能生成器的取词池, + * 以构造能驱动出多样置信分布的有效项目描述。 + */ +const ALL_BUSINESS_TYPE_TERMS: string[] = Array.from( + new Set( + Object.values(BUSINESS_TYPE_KEYWORDS) + .flat() + .map((kw: KeywordWeight) => kw.term), + ), +); + +/** + * 中文填充片段:保证生成的描述非纯空白且有效字符数 ≥ 10(Req 1.6 边界之外), + * 使生成样本恒为"有效项目描述",聚焦验证 Property 1 本身。 + */ +const FILLERS = [ + '本项目面向客户现场交付相关服务内容说明。', + '甲方要求乙方按约定标准提供对应人力安排。', + '合作期限内按月结算并接受过程质量监控。', + '项目背景与范围如下所述供评估参考使用。', +]; + +/** + * 智能生成器:从业务类型关键词池中随机取若干词,混入随机填充文本, + * 组合为有效项目描述。既覆盖"多类型关键词共现"导致的平局/接近平局分布, + * 也覆盖"无任何关键词命中"的全零分布,输入空间贴合 classify 的判定逻辑。 + */ +const validDescriptionArb: fc.Arbitrary = fc + .record({ + terms: fc.subarray(ALL_BUSINESS_TYPE_TERMS, { minLength: 0, maxLength: 6 }), + filler: fc.constantFrom(...FILLERS), + // 额外自由文本,增加多样性(可能为空)。 + extra: fc.string({ maxLength: 20 }), + // 关键词与填充的拼接顺序随机化。 + shuffle: fc.boolean(), + }) + .map(({ terms, filler, extra, shuffle }) => { + const head = terms.join(','); + const parts = shuffle ? [filler, head, extra] : [head, filler, extra]; + return parts.join(''); + }); + +describe('Property 1: 业务类型判定唯一且取最高置信 (Req 1.1)', () => { + it('classify 的 businessType 恒为五类中置信度最高的唯一一项', () => { + fc.assert( + fc.property(validDescriptionArb, (description) => { + const result = classify(description); + + // 独立复算五类业务类型的置信分布(与实现共用同一打分函数与词典)。 + const distribution = scoreLabels( + description, + BUSINESS_TYPE_VALUES.map( + (bt) => [bt, BUSINESS_TYPE_KEYWORDS[bt]] as const, + ), + ); + + // (a) 判定结果必为五类之一。 + expect(BUSINESS_TYPE_VALUES).toContain(result.businessType); + + // (b) 其置信度必等于全部五类中的最大置信度(取最高置信)。 + const maxConfidence = Math.max( + ...distribution.map((c) => c.confidence), + ); + expect(result.businessTypeConfidence).toBe(maxConfidence); + + const chosen = distribution.find( + (c) => c.label === result.businessType, + ); + expect(chosen).toBeDefined(); + expect(chosen!.confidence).toBe(maxConfidence); + + // (c) 唯一性:在所有达到最大置信度的类型中,判定结果必为按业务类型 + // 声明顺序(BUSINESS_TYPE_VALUES)最靠前的那一项,确保输出唯一确定, + // 不存在另一项同时被判定。 + const firstAtMax = distribution.find( + (c) => c.confidence === maxConfidence, + ); + expect(result.businessType).toBe(firstAtMax!.label); + }), + ); + }); +}); diff --git a/src/classifier/__tests__/classifier.property2.test.ts b/src/classifier/__tests__/classifier.property2.test.ts new file mode 100644 index 0000000..1aaa405 --- /dev/null +++ b/src/classifier/__tests__/classifier.property2.test.ts @@ -0,0 +1,88 @@ +/** + * Property 2: 置信度恒在有效值域内 的属性化测试(Classifier,Req 1.3)。 + * + * 属性陈述:对任意分类结果,业务类型 Confidence 与行业 Confidence 均落在区间 [0, 1] + * 内且保留两位小数。 + * + * 本测试以智能生成器构造任意有效项目描述(有效字符数 ≥ 10 且非纯空白,满足 Req 1.6 + * 前置以避免 InsufficientInputError),其中混入业务类型与行业关键词片段,使识别打分 + * 真实跨越"无命中 / 部分命中 / 多类命中"的输入空间,从而充分覆盖置信度取值分布。 + * + * 对每个分类结果断言: + * - businessTypeConfidence ∈ [0, 1] + * - industryConfidence ∈ [0, 1] + * - 二者均为两位小数(confidence × 100 为整数,浮点容差内) + * + * Feature: outsourcing-risk-assessment, Property 2: 置信度恒在有效值域内 + * Validates: Requirements 1.3 + */ + +import { describe, expect, it } from 'vitest'; +import fc from 'fast-check'; +import { classify, countValidChars } from '../classifier.js'; +import { BUSINESS_TYPE_KEYWORDS, INDUSTRY_KEYWORDS } from '../keywords.js'; + +// ---------------------------------------------------------------------------- +// 生成器:构造任意有效项目描述。 +// +// 输入空间设计: +// - 关键词片段池汇集全部业务类型与行业关键词,使描述高概率命中识别词典, +// 覆盖单类命中、多类命中、跨业务/行业混合命中等分支。 +// - 随机自由文本片段引入噪声与无命中情形。 +// - 末尾补足填充字符,保证有效字符数 ≥ 10(满足 Req 1.6 前置,避免抛错)。 +// ---------------------------------------------------------------------------- + +/** 全部识别关键词词条,作为可拼接的描述片段池。 */ +const KEYWORD_TERMS: string[] = [ + ...Object.values(BUSINESS_TYPE_KEYWORDS).flatMap((kws) => kws.map((k) => k.term)), + ...Object.values(INDUSTRY_KEYWORDS).flatMap((kws) => kws.map((k) => k.term)), +]; + +/** 片段:关键词词条或任意自由文本(含中英文与空白)。 */ +const fragmentArb: fc.Arbitrary = fc.oneof( + fc.constantFrom(...KEYWORD_TERMS), + fc.string({ minLength: 0, maxLength: 12 }), + fc.constantFrom('外包', '用工', '服务', '项目', '客户', '的', ',', '。', ' '), +); + +/** + * 有效项目描述:拼接若干片段后补足填充字符使有效字符数 ≥ 10。 + * 保证生成的描述恒为合法输入(不触发 InsufficientInputError)。 + */ +const validDescriptionArb: fc.Arbitrary = fc + .array(fragmentArb, { minLength: 1, maxLength: 8 }) + .map((fragments) => { + let desc = fragments.join(''); + // 补足有效字符至 ≥ 10(使用非空白填充字符)。 + while (countValidChars(desc) < 10) { + desc += '项'; + } + return desc; + }); + +/** 两位小数判定:value × 100 在浮点容差内为整数。 */ +function isTwoDecimals(value: number): boolean { + return Math.abs(value * 100 - Math.round(value * 100)) < 1e-9; +} + +describe('Property 2: 置信度恒在有效值域内 (Req 1.3)', () => { + it('业务类型与行业置信度均落在 [0,1] 且保留两位小数', () => { + fc.assert( + fc.property(validDescriptionArb, (description) => { + const result = classify(description); + + for (const confidence of [ + result.businessTypeConfidence, + result.industryConfidence, + ]) { + // 值域 [0, 1]。 + expect(confidence).toBeGreaterThanOrEqual(0); + expect(confidence).toBeLessThanOrEqual(1); + // 两位小数精度。 + expect(isTwoDecimals(confidence)).toBe(true); + } + }), + { numRuns: 100 }, + ); + }); +}); diff --git a/src/classifier/__tests__/classifier.property3.test.ts b/src/classifier/__tests__/classifier.property3.test.ts new file mode 100644 index 0000000..c7a4224 --- /dev/null +++ b/src/classifier/__tests__/classifier.property3.test.ts @@ -0,0 +1,126 @@ +/** + * Property 3: 低置信触发候选确认 的属性化测试(Classifier,Req 1.4, 1.5)。 + * + * 属性陈述:对任意分类结果,当某判定的 Confidence 低于 0.6(行业判定附加条件:行业标记 + * 不为"未识别")时,System 必返回按 Confidence 降序排列、数量至多 3 项的候选列表并置 + * 确认标志为真;否则不触发确认。 + * + * 本测试以智能生成器构造任意有效项目描述(有效字符数 ≥ 10 且非纯空白,满足 Req 1.6 + * 前置以避免 InsufficientInputError),其中混入业务类型与行业关键词片段,使识别打分 + * 真实跨越"无命中 / 部分命中 / 多类命中"的输入空间,从而覆盖高/低置信两类分支。 + * + * 对每个分类结果断言: + * 业务类型(Req 1.4): + * - 置信度 < 0.6 → needsBusinessTypeConfirm 为真,候选非空、≤3 项且按置信度降序; + * - 否则 needsBusinessTypeConfirm 为假,候选列表为空。 + * 行业(Req 1.5): + * - 置信度 < 0.6 且行业 ≠ "未识别" → needsIndustryConfirm 为真,候选非空、≤3 项且降序; + * - 否则 needsIndustryConfirm 为假,候选列表为空。 + * + * Feature: outsourcing-risk-assessment, Property 3: 低置信触发候选确认 + * Validates: Requirements 1.4, 1.5 + */ + +import { describe, expect, it } from 'vitest'; +import fc from 'fast-check'; +import { + classify, + countValidChars, + CONFIRMATION_CONFIDENCE_THRESHOLD, + MAX_CANDIDATES, + type ScoredCandidate, +} from '../classifier.js'; +import { BUSINESS_TYPE_KEYWORDS, INDUSTRY_KEYWORDS } from '../keywords.js'; +import { INDUSTRY_UNRECOGNIZED } from '../../domain/common.js'; + +// ---------------------------------------------------------------------------- +// 生成器:构造任意有效项目描述。 +// +// 输入空间设计: +// - 关键词片段池汇集全部业务类型与行业关键词,使描述高概率命中识别词典, +// 覆盖单类命中、多类命中、跨业务/行业混合命中等分支(驱动高/低置信两类结果)。 +// - 随机自由文本片段引入噪声与无命中情形(行业可能落到"未识别",业务类型可能低置信)。 +// - 末尾补足填充字符,保证有效字符数 ≥ 10(满足 Req 1.6 前置,避免抛错)。 +// ---------------------------------------------------------------------------- + +/** 全部识别关键词词条,作为可拼接的描述片段池。 */ +const KEYWORD_TERMS: string[] = [ + ...Object.values(BUSINESS_TYPE_KEYWORDS).flatMap((kws) => kws.map((k) => k.term)), + ...Object.values(INDUSTRY_KEYWORDS).flatMap((kws) => kws.map((k) => k.term)), +]; + +/** 片段:关键词词条或任意自由文本(含中英文与空白)。 */ +const fragmentArb: fc.Arbitrary = fc.oneof( + fc.constantFrom(...KEYWORD_TERMS), + fc.string({ minLength: 0, maxLength: 12 }), + fc.constantFrom('外包', '用工', '服务', '项目', '客户', '的', ',', '。', ' '), +); + +/** + * 有效项目描述:拼接若干片段后补足填充字符使有效字符数 ≥ 10。 + * 保证生成的描述恒为合法输入(不触发 InsufficientInputError)。 + */ +const validDescriptionArb: fc.Arbitrary = fc + .array(fragmentArb, { minLength: 1, maxLength: 8 }) + .map((fragments) => { + let desc = fragments.join(''); + while (countValidChars(desc) < 10) { + desc += '项'; + } + return desc; + }); + +/** 断言候选列表按置信度由高到低排序(允许相等)。 */ +function expectSortedDesc( + candidates: ScoredCandidate[], +): void { + for (let i = 1; i < candidates.length; i++) { + expect(candidates[i - 1]!.confidence).toBeGreaterThanOrEqual( + candidates[i]!.confidence, + ); + } +} + +describe('Property 3: 低置信触发候选确认 (Req 1.4, 1.5)', () => { + it('低置信判定返回降序、≤3 项候选并置确认标志;否则不触发确认', () => { + fc.assert( + fc.property(validDescriptionArb, (description) => { + const result = classify(description); + + // --- 业务类型(Req 1.4)--- + const businessLow = + result.businessTypeConfidence < CONFIRMATION_CONFIDENCE_THRESHOLD; + expect(result.needsBusinessTypeConfirm).toBe(businessLow); + if (businessLow) { + // 触发确认:候选非空、至多 3 项、按置信度降序。 + expect(result.businessTypeCandidates.length).toBeGreaterThan(0); + expect(result.businessTypeCandidates.length).toBeLessThanOrEqual( + MAX_CANDIDATES, + ); + expectSortedDesc(result.businessTypeCandidates); + } else { + // 不触发确认:候选列表为空。 + expect(result.businessTypeCandidates).toHaveLength(0); + } + + // --- 行业(Req 1.5)--- + const industryLow = + result.industry !== INDUSTRY_UNRECOGNIZED && + result.industryConfidence < CONFIRMATION_CONFIDENCE_THRESHOLD; + expect(result.needsIndustryConfirm).toBe(industryLow); + if (industryLow) { + // 触发确认:候选非空、至多 3 项、按置信度降序。 + expect(result.industryCandidates.length).toBeGreaterThan(0); + expect(result.industryCandidates.length).toBeLessThanOrEqual( + MAX_CANDIDATES, + ); + expectSortedDesc(result.industryCandidates); + } else { + // 不触发确认(含行业为"未识别"或高置信):候选列表为空。 + expect(result.industryCandidates).toHaveLength(0); + } + }), + { numRuns: 100 }, + ); + }); +}); diff --git a/src/classifier/__tests__/classifier.property4.test.ts b/src/classifier/__tests__/classifier.property4.test.ts new file mode 100644 index 0000000..1638e2c --- /dev/null +++ b/src/classifier/__tests__/classifier.property4.test.ts @@ -0,0 +1,102 @@ +/** + * Property 4: 描述信息不足必被拒绝 + * + * Feature: outsourcing-risk-assessment, Property 4: 描述信息不足必被拒绝 + * + * 对任意为空、仅含空白字符或有效字符数少于 10 的项目描述,System 必拒绝执行业务类型与 + * 行业判定并返回信息不足错误;而任意有效字符数不少于 10 的非空白描述不会因长度被拒。 + * + * Validates: Requirements 1.6 + */ + +import fc from 'fast-check'; +import { describe, expect, it } from 'vitest'; +import { + classify, + countValidChars, + InsufficientInputError, + MIN_VALID_CHARS, +} from '../classifier.js'; + +/** + * 一组明确的非空白(有效)字符:经 countValidChars 计数后每个均计为 1 个有效字符。 + * 覆盖中文、拉丁字母、数字与标点,以体现真实描述的多样性。 + */ +const NON_WHITESPACE_CHARS = + '一二三四五六七八九十甲乙丙丁项目外包派遣业务abcXYZ0123456789!@#。,'.split(''); + +/** 一组空白字符(含全角空格 U+3000),countValidChars 会将其全部剔除。 */ +const WHITESPACE_CHARS = [' ', '\t', '\n', '\r', '\f', '\v', '\u3000']; + +/** 任意单个非空白有效字符。 */ +const nonWhitespaceCharArb = fc.constantFrom(...NON_WHITESPACE_CHARS); +/** 任意单个空白字符。 */ +const whitespaceCharArb = fc.constantFrom(...WHITESPACE_CHARS); + +/** + * 将有效字符与空白字符交错拼接为一个字符串。 + * 拼接顺序不影响 countValidChars 的计数,但交错可更贴近真实输入分布。 + */ +function interleave(valids: string[], spaces: string[]): string { + const out: string[] = []; + const max = Math.max(valids.length, spaces.length); + for (let i = 0; i < max; i++) { + if (i < valids.length) out.push(valids[i]!); + if (i < spaces.length) out.push(spaces[i]!); + } + return out.join(''); +} + +/** + * 生成有效字符数严格小于 MIN_VALID_CHARS 的描述(含为空与仅含空白两种边界)。 + * 有效字符数 k ∈ [0, MIN_VALID_CHARS - 1];空白字符任意数量(不计入有效字符数)。 + */ +const insufficientDescriptionArb: fc.Arbitrary = fc + .tuple( + fc.array(nonWhitespaceCharArb, { + minLength: 0, + maxLength: MIN_VALID_CHARS - 1, + }), + fc.array(whitespaceCharArb, { minLength: 0, maxLength: 20 }), + ) + .map(([valids, spaces]) => interleave(valids, spaces)); + +/** + * 生成有效字符数不少于 MIN_VALID_CHARS 的非空白描述。 + * 有效字符数 k ∈ [MIN_VALID_CHARS, MIN_VALID_CHARS + 30];可附带任意空白字符。 + */ +const sufficientDescriptionArb: fc.Arbitrary = fc + .tuple( + fc.array(nonWhitespaceCharArb, { + minLength: MIN_VALID_CHARS, + maxLength: MIN_VALID_CHARS + 30, + }), + fc.array(whitespaceCharArb, { minLength: 0, maxLength: 20 }), + ) + .map(([valids, spaces]) => interleave(valids, spaces)); + +describe('Property 4: 描述信息不足必被拒绝 (Req 1.6)', () => { + it('任意有效字符数 < 10(含空 / 仅空白)的描述必被拒绝并抛 InsufficientInputError', () => { + fc.assert( + fc.property(insufficientDescriptionArb, (description) => { + // 前置:生成器确保有效字符数严格小于阈值。 + expect(countValidChars(description)).toBeLessThan(MIN_VALID_CHARS); + // 必拒绝执行判定并返回信息不足错误。 + expect(() => classify(description)).toThrow(InsufficientInputError); + }), + ); + }); + + it('任意有效字符数 ≥ 10 的非空白描述不会因长度被拒', () => { + fc.assert( + fc.property(sufficientDescriptionArb, (description) => { + // 前置:生成器确保有效字符数不小于阈值。 + expect(countValidChars(description)).toBeGreaterThanOrEqual( + MIN_VALID_CHARS, + ); + // classify 仅在信息不足时抛 InsufficientInputError;此处不应因长度被拒。 + expect(() => classify(description)).not.toThrow(); + }), + ); + }); +}); diff --git a/src/classifier/__tests__/classifier.property5.test.ts b/src/classifier/__tests__/classifier.property5.test.ts new file mode 100644 index 0000000..b90befc --- /dev/null +++ b/src/classifier/__tests__/classifier.property5.test.ts @@ -0,0 +1,171 @@ +/** + * Property 5: 确认值驱动后续加载 + * + * Feature: outsourcing-risk-assessment, Property 5: 确认值驱动后续加载 + * + * 对任意评估者确认或修改后的(业务类型, 行业),System 后续加载模板必以该确认值为依据: + * 将 confirmClassification 的输出(确认值)喂入 loadTemplate 时,所选中的模板必与确认后的 + * businessType / industry 一致;当评估者修改了 System 原判定时,加载以修改后的确认值(而非 + * 原判定值)为基准。 + * + * Validates: Requirements 1.7 + */ + +import fc from 'fast-check'; +import { describe, expect, it } from 'vitest'; +import { BUSINESS_TYPE_VALUES, type BusinessType } from '../../domain/common.js'; +import type { Template } from '../../domain/model.js'; +import { loadTemplate } from '../../config/loadTemplate.js'; +import { confirmClassification } from '../confirm.js'; + +/** 任意五类业务类型之一。 */ +const businessTypeArb: fc.Arbitrary = fc.constantFrom( + ...BUSINESS_TYPE_VALUES, +); + +/** 一组确定的非空白行业名(含"未识别"分支),均经规范化后稳定。 */ +const INDUSTRY_POOL = [ + '制造业', + '金融', + '物流', + '零售', + '医疗', + '信息技术', + '建筑', + '未识别', +] as const; + +/** 任意单个行业名(非空、非空白)。 */ +const baseIndustryArb = fc.constantFrom(...INDUSTRY_POOL); + +/** 任意空白填充(可为空),用于检验确认环节的首尾裁剪贯穿到模板加载。 */ +const whitespaceArb = fc.stringOf(fc.constantFrom(' ', '\t', '\u3000'), { + maxLength: 3, +}); + +/** + * 一份评估者确认/修改后的分类: + * - `businessType`:确认的业务类型; + * - `industryRaw`:评估者实际提交的行业文本(可带首尾空白); + * - `industry`:规范化(裁剪)后的期望行业值。 + */ +interface Classification { + businessType: BusinessType; + industryRaw: string; + industry: string; +} + +const classificationArb: fc.Arbitrary = fc + .record({ + businessType: businessTypeArb, + base: baseIndustryArb, + pad: fc.tuple(whitespaceArb, whitespaceArb), + }) + .map(({ businessType, base, pad }) => ({ + businessType, + industryRaw: `${pad[0]}${base}${pad[1]}`, + industry: base, + })); + +/** 构造一个最小但合法的 Template(loadTemplate 仅消费 businessType/industry/isDefault/id)。 */ +function makeTemplate( + id: string, + businessType: BusinessType, + industry: string, + isDefault: boolean, +): Template { + return { + id, + name: `${id}-${businessType}-${industry}`, + businessType, + industry, + isDefault, + riskModelConfig: { + name: id, + businessType, + dimensions: [], + redlines: [], + }, + }; +} + +describe('Property 5: 确认值驱动后续加载 (Req 1.7)', () => { + it('确认值即加载依据:loadTemplate 选中与确认 businessType/industry 一致的模板', () => { + fc.assert( + fc.property(classificationArb, (c) => { + const confirmed = confirmClassification(c.businessType, c.industryRaw); + // 确认值以评估者提交(裁剪后)取值为准。 + expect(confirmed.businessType).toBe(c.businessType); + expect(confirmed.industry).toBe(c.industry); + + // 知识库:含确认组合的精确模板、该业务类型的默认模板。 + const exact = makeTemplate( + 't-exact', + confirmed.businessType, + confirmed.industry, + false, + ); + const fallback = makeTemplate( + 't-default', + confirmed.businessType, + '__业务类型默认__', + true, + ); + const templates: Template[] = [fallback, exact]; + + // 将确认值喂入后续加载:必以确认值为依据命中行业专用模板。 + const result = loadTemplate( + confirmed.businessType, + confirmed.industry, + templates, + ); + expect(result.template.id).toBe('t-exact'); + expect(result.matchedIndustrySpecific).toBe(true); + expect(result.template.businessType).toBe(confirmed.businessType); + expect(result.template.industry.trim()).toBe(confirmed.industry); + }), + ); + }); + + it('评估者修改后以确认(修改)值而非原判定值为依据加载模板', () => { + fc.assert( + fc.property(classificationArb, classificationArb, (original, modified) => { + // 仅考察确认值与原判定不同的情形(评估者确有修改)。 + fc.pre( + original.businessType !== modified.businessType || + original.industry !== modified.industry, + ); + + const confirmed = confirmClassification( + modified.businessType, + modified.industryRaw, + ); + + // 知识库同时含「原判定」与「修改后确认值」两套精确模板。 + const tOriginal = makeTemplate( + 't-original', + original.businessType, + original.industry, + false, + ); + const tConfirmed = makeTemplate( + 't-confirmed', + confirmed.businessType, + confirmed.industry, + false, + ); + const templates: Template[] = [tOriginal, tConfirmed]; + + // 加载必跟随确认(修改)值,而非原判定值。 + const result = loadTemplate( + confirmed.businessType, + confirmed.industry, + templates, + ); + expect(result.template.id).toBe('t-confirmed'); + expect(result.template.businessType).toBe(confirmed.businessType); + expect(result.template.industry.trim()).toBe(confirmed.industry); + }), + ); + }); +}); diff --git a/src/classifier/__tests__/classifier.test.ts b/src/classifier/__tests__/classifier.test.ts new file mode 100644 index 0000000..faae498 --- /dev/null +++ b/src/classifier/__tests__/classifier.test.ts @@ -0,0 +1,104 @@ +/** + * Classifier 基础识别与输入校验单元测试(Req 1.1, 1.2, 1.3, 1.6)。 + */ + +import { describe, expect, it } from 'vitest'; +import { BUSINESS_TYPE_VALUES, INDUSTRY_UNRECOGNIZED } from '../../domain/common.js'; +import { + classify, + countValidChars, + InsufficientInputError, + MIN_VALID_CHARS, + toConfidence, +} from '../classifier.js'; + +describe('countValidChars', () => { + it('剔除空白字符后统计有效字符数', () => { + expect(countValidChars(' ab\tc\n ')).toBe(3); + expect(countValidChars('全角 空格')).toBe(4); + expect(countValidChars('')).toBe(0); + }); +}); + +describe('toConfidence', () => { + it('夹取到 [0,1] 并保留两位小数', () => { + expect(toConfidence(-0.5)).toBe(0); + expect(toConfidence(1.5)).toBe(1); + expect(toConfidence(0.3333)).toBe(0.33); + expect(toConfidence(0.666)).toBe(0.67); + expect(toConfidence(Number.NaN)).toBe(0); + }); +}); + +describe('classify - 输入校验 (Req 1.6)', () => { + it('空字符串被拒绝', () => { + expect(() => classify('')).toThrow(InsufficientInputError); + }); + + it('仅含空白字符被拒绝', () => { + expect(() => classify(' \t \n  ')).toThrow(InsufficientInputError); + }); + + it('有效字符数少于 10 被拒绝', () => { + expect(() => classify('短描述项目')).toThrow(InsufficientInputError); + }); + + it('有效字符数恰为 10 不因长度被拒', () => { + const desc = '一二三四五六七八九十'; + expect(countValidChars(desc)).toBe(MIN_VALID_CHARS); + expect(() => classify(desc)).not.toThrow(); + }); +}); + +describe('classify - 业务类型判定 (Req 1.1)', () => { + it('识别劳务派遣为最高置信类型', () => { + const result = classify('本项目为劳务派遣用工,需派遣被派遣员工至客户现场。'); + expect(result.businessType).toBe('劳务派遣'); + }); + + it('识别 BPO 业务流程外包', () => { + const result = classify('承接客户呼叫中心业务流程外包(BPO),提供客服坐席服务。'); + expect(result.businessType).toBe('BPO'); + }); + + it('返回的业务类型置信度为五类中的最高值', () => { + const desc = '本项目为岗位外包,提供驻场人员外包用工服务。'; + const result = classify(desc); + expect(BUSINESS_TYPE_VALUES).toContain(result.businessType); + // 置信度应为有效值域内 + expect(result.businessTypeConfidence).toBeGreaterThanOrEqual(0); + expect(result.businessTypeConfidence).toBeLessThanOrEqual(1); + }); + + it('无任何关键词命中时仍确定性返回唯一业务类型', () => { + const result = classify('这是一段没有任何业务关键词的普通中文描述文本内容。'); + expect(BUSINESS_TYPE_VALUES).toContain(result.businessType); + // 平局时按声明顺序取首项 + expect(result.businessType).toBe(BUSINESS_TYPE_VALUES[0]); + }); +}); + +describe('classify - 行业判定 (Req 1.2)', () => { + it('识别可判定行业', () => { + const result = classify('为某大型制造工厂提供生产线车间用工外包服务。'); + expect(result.industry).toBe('制造业'); + expect(result.industryConfidence).toBeGreaterThan(0); + }); + + it('行业不可判定时输出"未识别"且置信度为 0', () => { + const result = classify('提供一般性人力用工外包,未提及具体所属领域信息。'); + expect(result.industry).toBe(INDUSTRY_UNRECOGNIZED); + expect(result.industryConfidence).toBe(0); + }); +}); + +describe('classify - 置信度值域 (Req 1.3)', () => { + it('两类置信度均落在 [0,1] 且至多两位小数', () => { + const result = classify('为银行金融机构提供业务流程外包BPO客服坐席服务。'); + for (const c of [result.businessTypeConfidence, result.industryConfidence]) { + expect(c).toBeGreaterThanOrEqual(0); + expect(c).toBeLessThanOrEqual(1); + expect(Math.abs(c * 100 - Math.round(c * 100))).toBeLessThan(1e-9); + } + }); +}); diff --git a/src/classifier/classifier.ts b/src/classifier/classifier.ts new file mode 100644 index 0000000..42e2e4f --- /dev/null +++ b/src/classifier/classifier.ts @@ -0,0 +1,247 @@ +/** + * Classifier 业务类型与行业识别(Req 1.1, 1.2, 1.3, 1.6)。 + * + * 本模块实现基础识别与输入校验: + * - 输入为空 / 仅含空白字符 / 有效字符数 < 10 时拒绝并抛 InsufficientInputError(Req 1.6)。 + * - 业务类型判定为五类中 Confidence 最高的唯一一项(Req 1.1)。 + * - 行业无法判定时输出标记"未识别"(Req 1.2)。 + * - 业务类型与行业各输出取值 [0,1]、保留两位小数的 Confidence(Req 1.3)。 + * - 当业务类型 Confidence < 0.6 时,返回按 Confidence 降序、至多 3 项的候选业务类型列表 + * 并置 needsBusinessTypeConfirm 为真(Req 1.4)。 + * - 当行业 Confidence < 0.6 且行业标记不为"未识别"时,返回按 Confidence 降序、至多 3 项的 + * 候选行业列表并置 needsIndustryConfirm 为真(Req 1.5)。 + * + * 确认值驱动(Req 1.7)由后续任务实现。 + */ + +import { + BUSINESS_TYPE_VALUES, + INDUSTRY_UNRECOGNIZED, + type BusinessType, + type Confidence, + type Industry, +} from '../domain/common.js'; +import { + BUSINESS_TYPE_KEYWORDS, + INDUSTRY_KEYWORDS, + type KeywordWeight, +} from './keywords.js'; + +/** 有效项目描述所需的最小有效字符数(Req 1.6)。 */ +export const MIN_VALID_CHARS = 10; + +/** + * 触发候选确认的置信度阈值(Req 1.4, 1.5)。 + * 判定置信度严格低于该阈值时,返回候选列表并请求评估者确认。 + */ +export const CONFIRMATION_CONFIDENCE_THRESHOLD = 0.6; + +/** 候选列表的最大长度(Req 1.4, 1.5:至多 3 项)。 */ +export const MAX_CANDIDATES = 3; + +/** + * 项目描述信息不足错误(Req 1.6)。 + * 当描述为空、仅含空白字符或有效字符数少于 MIN_VALID_CHARS 时抛出。 + */ +export class InsufficientInputError extends Error { + constructor( + message = `项目描述信息不足:有效字符数须不少于 ${MIN_VALID_CHARS} 个`, + ) { + super(message); + this.name = 'InsufficientInputError'; + } +} + +/** + * 识别结果(Req 1.1-1.5)。 + */ +export interface ClassificationResult { + /** 业务类型判定:五类中 Confidence 最高的唯一一项(Req 1.1)。 */ + businessType: BusinessType; + /** 业务类型判定的置信度,取值 [0,1]、两位小数(Req 1.3)。 */ + businessTypeConfidence: Confidence; + /** + * 候选业务类型列表:按 Confidence 由高到低排序、至多 3 项(Req 1.4)。 + * 仅在 needsBusinessTypeConfirm 为真时具语义,否则为空数组。 + */ + businessTypeCandidates: ScoredCandidate[]; + /** + * 业务类型确认标志:businessTypeConfidence < 0.6 时为真(Req 1.4)。 + * 为真表示需向评估者展示候选列表并请求确认。 + */ + needsBusinessTypeConfirm: boolean; + /** 行业判定;无法判定时为"未识别"(Req 1.2)。 */ + industry: Industry; + /** 行业判定的置信度,取值 [0,1]、两位小数(Req 1.3)。 */ + industryConfidence: Confidence; + /** + * 候选行业列表:按 Confidence 由高到低排序、至多 3 项(Req 1.5)。 + * 仅在 needsIndustryConfirm 为真时具语义,否则为空数组。 + */ + industryCandidates: ScoredCandidate[]; + /** + * 行业确认标志:industryConfidence < 0.6 且行业标记不为"未识别"时为真(Req 1.5)。 + * 为真表示需向评估者展示候选列表并请求确认。 + */ + needsIndustryConfirm: boolean; +} + +/** 候选项:标识与其置信度。供业务类型与行业打分复用。 */ +export interface ScoredCandidate { + /** 候选标识(业务类型或行业)。 */ + label: T; + /** 置信度,取值 [0,1]、两位小数。 */ + confidence: Confidence; +} + +/** + * 统计有效字符数:剔除全部空白字符(含半角/全角空格、制表、换行)后的字符数(Req 1.6)。 + */ +export function countValidChars(text: string): number { + // \s 覆盖常见空白;额外剔除全角空格 U+3000。 + return text.replace(/[\s\u3000]/g, '').length; +} + +/** + * 将任意实数夹取到 [0,1] 并保留两位小数(Req 1.3)。 + */ +export function toConfidence(value: number): Confidence { + if (!Number.isFinite(value)) { + return 0; + } + const clamped = Math.min(1, Math.max(0, value)); + return Math.round(clamped * 100) / 100; +} + +/** + * 计算单个标签对文本的原始匹配分:累加全部命中关键词的权重(大小写不敏感)。 + */ +function rawMatchScore(text: string, keywords: KeywordWeight[]): number { + const haystack = text.toLowerCase(); + let score = 0; + for (const { term, weight } of keywords) { + if (haystack.includes(term.toLowerCase())) { + score += weight; + } + } + return score; +} + +/** + * 对一组标签按关键词词典打分并归一化为置信度分布。 + * + * 归一化:confidence = rawScore / Σ rawScore,落在 [0,1];全部为 0 时返回全 0 分布。 + * 返回顺序与传入 entries 顺序一致,保证确定性(平局消歧依赖该顺序)。 + */ +export function scoreLabels( + text: string, + entries: ReadonlyArray, +): ScoredCandidate[] { + const raw = entries.map( + ([label, keywords]) => [label, rawMatchScore(text, keywords)] as const, + ); + const total = raw.reduce((sum, [, s]) => sum + s, 0); + return raw.map(([label, s]) => ({ + label, + confidence: total > 0 ? toConfidence(s / total) : 0, + })); +} + +/** + * 从置信度分布中选出唯一最高项(Req 1.1)。 + * 平局时按 candidates 顺序(即词典声明顺序)确定性取靠前者,保证输出唯一确定。 + */ +function pickHighest( + candidates: ScoredCandidate[], +): ScoredCandidate { + // candidates 至少含一项(业务类型恒为五类,调用方保证非空)。 + let best = candidates[0]!; + for (let i = 1; i < candidates.length; i++) { + const current = candidates[i]!; + if (current.confidence > best.confidence) { + best = current; + } + } + return best; +} + +/** + * 选出按 Confidence 由高到低排序、至多 MAX_CANDIDATES 项的候选列表(Req 1.4, 1.5)。 + * + * @param candidates 已打分的候选分布(顺序为词典声明顺序)。 + * @param includeZero 是否保留置信度为 0 的候选;行业候选剔除 0("未识别"无候选意义)。 + * + * 排序采用稳定排序:Confidence 相同的候选保持其在 candidates 中的相对顺序, + * 保证同一输入恒产生同一候选顺序(确定性,支撑属性化测试)。 + */ +function topCandidates( + candidates: ScoredCandidate[], + includeZero: boolean, +): ScoredCandidate[] { + const pool = includeZero + ? candidates.slice() + : candidates.filter((c) => c.confidence > 0); + // Array.prototype.sort 在 Node ≥ 11 为稳定排序,平局保持原相对顺序(确定性消歧)。 + pool.sort((a, b) => b.confidence - a.confidence); + return pool.slice(0, MAX_CANDIDATES); +} + +/** + * 业务类型与行业识别(Req 1.1, 1.2, 1.3, 1.6)。 + * + * @param description 项目描述文本。 + * @throws {InsufficientInputError} 描述为空、仅含空白字符或有效字符数 < 10 时(Req 1.6)。 + * @returns 业务类型与行业判定及各自置信度。 + */ +export function classify(description: string): ClassificationResult { + // Req 1.6:输入校验前置——为空 / 仅空白 / 有效字符 < 10 一律拒绝。 + if (countValidChars(description) < MIN_VALID_CHARS) { + throw new InsufficientInputError(); + } + + // Req 1.1:业务类型取五类中置信度最高的唯一一项(平局按声明顺序消歧)。 + const businessTypeScores = scoreLabels( + description, + BUSINESS_TYPE_VALUES.map( + (bt) => [bt, BUSINESS_TYPE_KEYWORDS[bt]] as const, + ), + ); + const topBusinessType = pickHighest(businessTypeScores); + + // Req 1.2:行业无任何关键词命中(置信度全 0)时输出"未识别"。 + const industryScores = scoreLabels( + description, + Object.entries(INDUSTRY_KEYWORDS) as ReadonlyArray< + readonly [string, KeywordWeight[]] + >, + ); + const topIndustry = pickHighest(industryScores); + const industryDeterminable = topIndustry.confidence > 0; + const industry = industryDeterminable + ? topIndustry.label + : INDUSTRY_UNRECOGNIZED; + const industryConfidence = industryDeterminable ? topIndustry.confidence : 0; + + // Req 1.4:业务类型置信度 < 0.6 时置确认标志并返回 ≤3 项按置信度降序的候选。 + const needsBusinessTypeConfirm = + topBusinessType.confidence < CONFIRMATION_CONFIDENCE_THRESHOLD; + // Req 1.5:行业置信度 < 0.6 且行业标记不为"未识别"时置确认标志并返回候选。 + const needsIndustryConfirm = + industry !== INDUSTRY_UNRECOGNIZED && + industryConfidence < CONFIRMATION_CONFIDENCE_THRESHOLD; + + return { + businessType: topBusinessType.label, + businessTypeConfidence: topBusinessType.confidence, + businessTypeCandidates: needsBusinessTypeConfirm + ? topCandidates(businessTypeScores, true) + : [], + needsBusinessTypeConfirm, + industry, + industryConfidence, + industryCandidates: needsIndustryConfirm + ? topCandidates(industryScores, false) + : [], + needsIndustryConfirm, + }; +} diff --git a/src/classifier/confirm.ts b/src/classifier/confirm.ts new file mode 100644 index 0000000..a6987d3 --- /dev/null +++ b/src/classifier/confirm.ts @@ -0,0 +1,81 @@ +/** + * Classifier 确认值驱动(Req 1.7)。 + * + * 当评估者确认或修改 System 判定的业务类型与行业后,本模块校验并产出 + * 一个"确认后的分类值"(ConfirmedClassification),作为后续 Config_Center + * loadTemplate(businessType, industry) 加载模板的唯一依据(Req 1.7 → Req 2.1)。 + * + * 设计取舍:design.md 以 `confirmClassification(assessmentId, businessType, industry)` + * 的有状态伪签名描述该契约。此处按本仓库的纯函数模块风格,将其实现为返回确认值的 + * 纯函数:输入为评估者确认/修改后的业务类型与行业,输出为校验通过、可直接供下游 + * 模板加载消费的确认值。调用方据此更新对应 Assessment 的 businessType/industry。 + */ + +import { + BUSINESS_TYPE_VALUES, + INDUSTRY_UNRECOGNIZED, + type BusinessType, + type Industry, +} from '../domain/common.js'; + +/** + * 确认后的分类值(Req 1.7)。 + * 以评估者确认/修改后的取值为准,作为后续加载模板的依据。 + */ +export interface ConfirmedClassification { + /** 评估者确认/修改后的业务类型(五类之一)。 */ + businessType: BusinessType; + /** 评估者确认/修改后的行业标记(非空,可为"未识别")。 */ + industry: Industry; +} + +/** + * 分类确认无效错误(Req 1.7)。 + * 当确认的业务类型不属于五类、或行业为空 / 仅含空白字符时抛出。 + */ +export class InvalidConfirmationError extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidConfirmationError'; + } +} + +/** 判定给定字符串是否为合法的五类业务类型之一。 */ +export function isBusinessType(value: string): value is BusinessType { + return (BUSINESS_TYPE_VALUES as readonly string[]).includes(value); +} + +/** + * 采用评估者确认/修改后的业务类型与行业作为后续加载模板的依据(Req 1.7)。 + * + * 行为:以评估者提交的取值为准(无论其是否修改了 System 的判定),校验合法后 + * 产出确认值供下游 loadTemplate 消费。 + * + * @param businessType 评估者确认/修改后的业务类型,须为五类之一。 + * @param industry 评估者确认/修改后的行业;前后空白将被裁剪,须非空。 + * 允许取"未识别"以表达评估者确认行业不可判定(下游据此回退默认模板,Req 2.2)。 + * @throws {InvalidConfirmationError} 业务类型非五类之一,或行业为空 / 仅含空白字符时。 + * @returns 校验通过、可直接供后续加载模板消费的确认值。 + */ +export function confirmClassification( + businessType: BusinessType, + industry: Industry, +): ConfirmedClassification { + if (!isBusinessType(businessType)) { + throw new InvalidConfirmationError( + `确认的业务类型无效:须为 ${BUSINESS_TYPE_VALUES.join(' / ')} 之一`, + ); + } + + const normalizedIndustry = industry.trim(); + if (normalizedIndustry.length === 0) { + throw new InvalidConfirmationError( + `确认的行业无效:不可为空或仅含空白字符(不可判定时请取"${INDUSTRY_UNRECOGNIZED}")`, + ); + } + + return { + businessType, + industry: normalizedIndustry, + }; +} diff --git a/src/classifier/index.ts b/src/classifier/index.ts new file mode 100644 index 0000000..0b96c6c --- /dev/null +++ b/src/classifier/index.ts @@ -0,0 +1,7 @@ +/** + * Classifier 模块聚合导出(业务类型与行业识别,Req 1)。 + */ + +export * from './keywords.js'; +export * from './classifier.js'; +export * from './confirm.js'; diff --git a/src/classifier/keywords.ts b/src/classifier/keywords.ts new file mode 100644 index 0000000..2dfb704 --- /dev/null +++ b/src/classifier/keywords.ts @@ -0,0 +1,123 @@ +/** + * Classifier 关键词词典(Req 1.1, 1.2)。 + * + * 业务类型与行业的识别采用确定性的关键词匹配打分:对项目描述文本逐一匹配每个 + * 业务类型 / 行业的关键词,按命中关键词的权重累加得到原始分,再归一化为 [0,1] 置信度。 + * + * 词典内容与排序均稳定,保证同一输入恒产生同一识别结果(确定性,支撑属性化测试)。 + * 后续可由 Knowledge_Base 行业分区扩展,但识别算法本身不变(配置驱动思想)。 + */ + +import type { BusinessType } from '../domain/common.js'; + +/** 关键词及其权重;权重越高表示该词对判定的指示性越强。 */ +export interface KeywordWeight { + /** 关键词(大小写不敏感匹配)。 */ + term: string; + /** 匹配权重(正数)。 */ + weight: number; +} + +/** + * 五类业务类型的关键词词典(Req 1.1)。 + * 键顺序与 BUSINESS_TYPE_VALUES 一致,用于平局时的确定性消歧(优先级由前到后)。 + */ +export const BUSINESS_TYPE_KEYWORDS: Record = { + 岗位外包: [ + { term: '岗位外包', weight: 5 }, + { term: '岗位', weight: 2 }, + { term: '驻场', weight: 2 }, + { term: '人员外包', weight: 3 }, + { term: '用工', weight: 1 }, + ], + 劳务派遣: [ + { term: '劳务派遣', weight: 5 }, + { term: '派遣', weight: 3 }, + { term: '用工比例', weight: 2 }, + { term: '被派遣', weight: 3 }, + ], + '业务/服务外包': [ + { term: '业务外包', weight: 5 }, + { term: '服务外包', weight: 5 }, + { term: '业务/服务', weight: 4 }, + { term: '外包服务', weight: 3 }, + { term: '服务', weight: 1 }, + ], + BPO: [ + { term: 'BPO', weight: 5 }, + { term: '业务流程外包', weight: 5 }, + { term: '流程外包', weight: 4 }, + { term: '呼叫中心', weight: 3 }, + { term: '客服', weight: 2 }, + { term: '坐席', weight: 2 }, + ], + 项目制外包: [ + { term: '项目制外包', weight: 5 }, + { term: '项目制', weight: 4 }, + { term: '项目外包', weight: 4 }, + { term: '项目交付', weight: 3 }, + { term: '交钥匙', weight: 3 }, + { term: '总包', weight: 2 }, + ], +}; + +/** + * 行业关键词词典(Req 1.2)。 + * 键为行业标识;无任何行业关键词命中时,Classifier 输出"未识别"。 + * 键的插入顺序用于平局时的确定性消歧。 + */ +export const INDUSTRY_KEYWORDS: Record = { + 制造业: [ + { term: '制造', weight: 4 }, + { term: '工厂', weight: 3 }, + { term: '生产线', weight: 3 }, + { term: '车间', weight: 3 }, + { term: '装配', weight: 2 }, + ], + 信息技术: [ + { term: '软件', weight: 4 }, + { term: '互联网', weight: 3 }, + { term: '系统开发', weight: 4 }, + { term: '研发', weight: 2 }, + { term: 'IT', weight: 3 }, + ], + 金融业: [ + { term: '银行', weight: 4 }, + { term: '金融', weight: 4 }, + { term: '保险', weight: 3 }, + { term: '证券', weight: 3 }, + ], + 零售业: [ + { term: '零售', weight: 4 }, + { term: '门店', weight: 3 }, + { term: '商超', weight: 3 }, + { term: '电商', weight: 3 }, + ], + 物流业: [ + { term: '物流', weight: 4 }, + { term: '仓储', weight: 3 }, + { term: '配送', weight: 3 }, + { term: '快递', weight: 3 }, + ], + 建筑业: [ + { term: '建筑', weight: 4 }, + { term: '施工', weight: 4 }, + { term: '工地', weight: 3 }, + { term: '工程', weight: 2 }, + ], + 餐饮业: [ + { term: '餐饮', weight: 4 }, + { term: '餐厅', weight: 3 }, + { term: '后厨', weight: 3 }, + ], + 医疗健康: [ + { term: '医疗', weight: 4 }, + { term: '医院', weight: 4 }, + { term: '护理', weight: 3 }, + ], + 客服服务: [ + { term: '呼叫中心', weight: 4 }, + { term: '客服', weight: 3 }, + { term: '坐席', weight: 3 }, + ], +}; diff --git a/src/compliance/__tests__/judgment.examples.test.ts b/src/compliance/__tests__/judgment.examples.test.ts new file mode 100644 index 0000000..659a11b --- /dev/null +++ b/src/compliance/__tests__/judgment.examples.test.ts @@ -0,0 +1,258 @@ +/** + * 合规判定/费用测算的代表性算例单元测试(Req 16.2)。 + * + * 以社保缴费基数、社保费率、约定月薪、工作年限与劳务派遣比例的代表性数值, + * 验证: + * - 经济补偿月数折算(N 规则):满整年、不足六个月(半月)、六个月以上不满一年(按一年计)。 + * - 经济补偿金额 N = 月数 × 月薪,N+1 = N + 月薪。 + * - 社保缴费金额 = 缴费基数 × 费率。 + * - 测算金额标注所依据的规则项与输入项。 + * + * 与既有属性化测试(Property 53/54)互补:此处固定具体数值核对计算正确性。 + * Validates: Requirements 16.2 + */ + +import { describe, expect, it } from 'vitest'; +import type { ComplianceRuleSet } from '../../domain/region.js'; +import { REGION_CN } from '../../domain/region.js'; +import type { CnComplianceInput } from '../judgment.js'; +import { + assessCnCompliance, + computeCompensationMonths, +} from '../judgment.js'; + +// ---------------------------------------------------------------------------- +// 代表性规则集:填充具体的社保下限、最低工资标准(CN 默认规则集以占位值发布)。 +// ---------------------------------------------------------------------------- + +const REPRESENTATIVE_RULE_SET: ComplianceRuleSet = { + region: REGION_CN, + socialInsuranceBase: { lowerBound: 4000 }, + economicCompensation: { + nRule: + '按在本单位工作年限,每满一年支付一个月工资;六个月以上不满一年按一年计;不满六个月支付半个月工资(N)', + nPlusOneRule: '在 N 的基础上额外支付一个月工资作为代通知金(N+1)', + }, + dispatchRatioCap: 0.1, + minimumWage: { byLocality: { 上海: 2690, 北京: 2420 } }, +}; + +/** 构造一份合法的基准输入,便于各算例按需覆盖字段。 */ +function baseInput(overrides: Partial = {}): CnComplianceInput { + return { + socialInsuranceBase: 8000, + socialInsuranceContributionRate: 0.16, + monthlyWage: 10000, + serviceYears: 3, + compensationScheme: 'N', + ...overrides, + }; +} + +/** 从测算结果中按标签取金额。 */ +function amountByLabel( + result: ReturnType, + label: string, +): number { + const found = result.amounts.find((a) => a.label === label); + expect(found, `应存在标签为「${label}」的测算金额`).toBeDefined(); + // 上一行断言已保证 found 存在,这里非空断言用于满足类型收窄。 + return found!.amount; +} + +describe('经济补偿月数折算(N 规则)— 代表性算例', () => { + it('满整年:3 年 → 3 个月', () => { + const months = computeCompensationMonths(3); + expect(months.fullYears).toBe(3); + expect(months.remainderMonths).toBe(0); + expect(months.total).toBe(3); + }); + + it('不满六个月:3 年又 3 个月 → 3.5 个月(半月)', () => { + // 3 + 3/12 = 3.25 年,余 3 个月(<6 个月)→ 折半月。 + const months = computeCompensationMonths(3.25); + expect(months.fullYears).toBe(3); + expect(months.remainderMonths).toBe(0.5); + expect(months.total).toBe(3.5); + }); + + it('恰满六个月:3 年又 6 个月 → 4 个月(按一年计)', () => { + // 六个月以上不满一年按一年计。 + const months = computeCompensationMonths(3.5); + expect(months.fullYears).toBe(3); + expect(months.remainderMonths).toBe(1); + expect(months.total).toBe(4); + }); + + it('六个月以上不满一年:5 年又 9 个月 → 6 个月(按一年计)', () => { + const months = computeCompensationMonths(5.75); + expect(months.fullYears).toBe(5); + expect(months.remainderMonths).toBe(1); + expect(months.total).toBe(6); + }); + + it('不足一年:0.4 年(约 4.8 个月)→ 0.5 个月(半月)', () => { + const months = computeCompensationMonths(0.4); + expect(months.fullYears).toBe(0); + expect(months.remainderMonths).toBe(0.5); + expect(months.total).toBe(0.5); + }); +}); + +describe('经济补偿金额 N 与 N+1 — 代表性算例', () => { + it('5 年整、月薪 10000:N = 50000,N+1 = 60000', () => { + const result = assessCnCompliance( + baseInput({ monthlyWage: 10000, serviceYears: 5, compensationScheme: 'N' }), + REPRESENTATIVE_RULE_SET, + ); + + // N = 月数 × 月薪 = 5 × 10000。 + expect(amountByLabel(result, '经济补偿(N)')).toBe(50000); + // N+1 = N + 月薪 = 50000 + 10000。 + expect(amountByLabel(result, '经济补偿(N+1)')).toBe(60000); + }); + + it('3 年又 6 个月、月薪 8000:月数按一年计 → 4 个月,N = 32000,N+1 = 40000', () => { + const result = assessCnCompliance( + baseInput({ monthlyWage: 8000, serviceYears: 3.5 }), + REPRESENTATIVE_RULE_SET, + ); + + // 月数 4(3 满年 + 六个月以上按一年计)× 8000 = 32000。 + expect(amountByLabel(result, '经济补偿(N)')).toBe(32000); + expect(amountByLabel(result, '经济补偿(N+1)')).toBe(40000); + }); + + it('3 年又 3 个月、月薪 12000:月数折半月 → 3.5 个月,N = 42000,N+1 = 54000', () => { + const result = assessCnCompliance( + baseInput({ monthlyWage: 12000, serviceYears: 3.25 }), + REPRESENTATIVE_RULE_SET, + ); + + // 月数 3.5 × 12000 = 42000;N+1 = 42000 + 12000 = 54000。 + expect(amountByLabel(result, '经济补偿(N)')).toBe(42000); + expect(amountByLabel(result, '经济补偿(N+1)')).toBe(54000); + }); + + it('N+1 恒等于 N 加一个月薪(代表性数值核验)', () => { + const result = assessCnCompliance( + baseInput({ monthlyWage: 9500, serviceYears: 2 }), + REPRESENTATIVE_RULE_SET, + ); + + const n = amountByLabel(result, '经济补偿(N)'); + const nPlusOne = amountByLabel(result, '经济补偿(N+1)'); + expect(n).toBe(19000); // 2 × 9500 + expect(nPlusOne - n).toBe(9500); + }); +}); + +describe('社保缴费金额 = 基数 × 费率 — 代表性算例', () => { + it('基数 8000、费率 0.16 → 1280', () => { + const result = assessCnCompliance( + baseInput({ socialInsuranceBase: 8000, socialInsuranceContributionRate: 0.16 }), + REPRESENTATIVE_RULE_SET, + ); + expect(amountByLabel(result, '社保缴费金额')).toBe(1280); + }); + + it('基数 6000、费率 0.105 → 630', () => { + const result = assessCnCompliance( + baseInput({ socialInsuranceBase: 6000, socialInsuranceContributionRate: 0.105 }), + REPRESENTATIVE_RULE_SET, + ); + expect(amountByLabel(result, '社保缴费金额')).toBeCloseTo(630, 9); + }); + + it('费率为 0 → 社保缴费金额为 0', () => { + const result = assessCnCompliance( + baseInput({ socialInsuranceBase: 12000, socialInsuranceContributionRate: 0 }), + REPRESENTATIVE_RULE_SET, + ); + expect(amountByLabel(result, '社保缴费金额')).toBe(0); + }); +}); + +describe('相关合规费用合计与金额标注 — 代表性算例', () => { + it('N 计法:合计 = 经济补偿(N) + 社保缴费金额', () => { + const result = assessCnCompliance( + baseInput({ + monthlyWage: 10000, + serviceYears: 5, + socialInsuranceBase: 8000, + socialInsuranceContributionRate: 0.16, + compensationScheme: 'N', + }), + REPRESENTATIVE_RULE_SET, + ); + + // 经济补偿(N)= 50000,社保缴费金额 = 1280 → 合计 51280。 + expect(amountByLabel(result, '相关合规费用合计')).toBe(51280); + }); + + it('N+1 计法:合计 = 经济补偿(N+1) + 社保缴费金额', () => { + const result = assessCnCompliance( + baseInput({ + monthlyWage: 10000, + serviceYears: 5, + socialInsuranceBase: 8000, + socialInsuranceContributionRate: 0.16, + compensationScheme: 'N+1', + }), + REPRESENTATIVE_RULE_SET, + ); + + // 经济补偿(N+1)= 60000,社保缴费金额 = 1280 → 合计 61280。 + expect(amountByLabel(result, '相关合规费用合计')).toBe(61280); + }); + + it('每项测算金额均标注规则项与输入项(Req 16.2)', () => { + const result = assessCnCompliance( + baseInput({ socialInsuranceBase: 8000, socialInsuranceContributionRate: 0.16 }), + REPRESENTATIVE_RULE_SET, + ); + + for (const amount of result.amounts) { + expect(amount.ruleReference.length).toBeGreaterThan(0); + expect(amount.inputs.length).toBeGreaterThan(0); + } + + // 社保缴费金额一项的输入应包含基数与费率两项。 + const social = result.amounts.find((a) => a.label === '社保缴费金额'); + expect(social).toBeDefined(); + const fields = social!.inputs.map((i) => i.field); + expect(fields).toContain('实际社保缴费基数'); + expect(fields).toContain('社保缴费费率'); + }); +}); + +describe('劳务派遣用工比例代表性算例(与最低工资判定联动)', () => { + it('派遣比例 10%(10/100)恰为上限 → 合规通过', () => { + const result = assessCnCompliance( + baseInput({ dispatch: { dispatchedHeadcount: 10, totalHeadcount: 100 } }), + REPRESENTATIVE_RULE_SET, + ); + const dispatch = result.judgments.find((j) => j.item === '劳务派遣用工比例'); + expect(dispatch?.status).toBe('合规通过'); + }); + + it('派遣比例 12%(12/100)超过上限 → 合规不通过', () => { + const result = assessCnCompliance( + baseInput({ dispatch: { dispatchedHeadcount: 12, totalHeadcount: 100 } }), + REPRESENTATIVE_RULE_SET, + ); + const dispatch = result.judgments.find((j) => j.item === '劳务派遣用工比例'); + expect(dispatch?.status).toBe('合规不通过'); + expect(result.passed).toBe(false); + }); + + it('约定月薪低于当地(上海 2690)最低工资 → 合规不通过', () => { + const result = assessCnCompliance( + baseInput({ monthlyWage: 2500, locality: '上海' }), + REPRESENTATIVE_RULE_SET, + ); + const minWage = result.judgments.find((j) => j.item === '当地最低工资'); + expect(minWage?.status).toBe('合规不通过'); + expect(result.passed).toBe(false); + }); +}); diff --git a/src/compliance/__tests__/judgment.property53.test.ts b/src/compliance/__tests__/judgment.property53.test.ts new file mode 100644 index 0000000..d719748 --- /dev/null +++ b/src/compliance/__tests__/judgment.property53.test.ts @@ -0,0 +1,164 @@ +/** + * Property 53: 合规判定覆盖与计量标注(Req 16.1, 16.2)。 + * + * 属性陈述: + * 对任意 Region 为中国大陆(CN)的合法评估输入,`assessCnCompliance` 的判定结果: + * - 合规判定必覆盖四项判定项(社保缴费基数、经济补偿、劳务派遣用工比例、当地最低工资); + * - 经济补偿(N 与 N+1)、社保缴费金额与相关合规费用必被测算,且每项金额必标注其所 + * 依据的规则项(ruleReference)与输入项(inputs)。 + * + * Feature: outsourcing-risk-assessment, Property 53: 合规判定覆盖与计量标注 + * Validates: Requirements 16.1, 16.2 + */ + +import { describe, expect, it } from 'vitest'; +import fc from 'fast-check'; +import type { ComplianceRuleSet } from '../../domain/region.js'; +import { REGION_CN } from '../../domain/region.js'; +import { + assessCnCompliance, + COMPLIANCE_ITEM_KEYS, + type CnComplianceInput, +} from '../judgment.js'; + +// ---------------------------------------------------------------------------- +// 生成器:合法的 CnComplianceInput(满足输入校验前置约束)以及可变的 CN 规则集。 +// ---------------------------------------------------------------------------- + +/** 货币金额(非负有限值)。 */ +const moneyArb: fc.Arbitrary = fc.double({ + min: 0, + max: 1_000_000, + noNaN: true, + noDefaultInfinity: true, +}); + +/** 劳务派遣用工人数信息:被派遣人数不超过用工总量、总量为正。 */ +const dispatchArb: fc.Arbitrary<{ + dispatchedHeadcount: number; + totalHeadcount: number; +}> = fc + .tuple( + fc.integer({ min: 1, max: 1000 }), + fc.integer({ min: 0, max: 1000 }), + ) + .map(([totalHeadcount, dispatchedRaw]) => ({ + totalHeadcount, + dispatchedHeadcount: Math.min(dispatchedRaw, totalHeadcount), + })); + +/** 当地标识(用于查询当地最低工资;可能命中或未命中规则集)。 */ +const localityArb: fc.Arbitrary = fc.constantFrom( + '北京', + '上海', + '深圳', + '杭州', + '未知地区', +); + +const inputArb: fc.Arbitrary = fc + .record({ + socialInsuranceBase: moneyArb, + socialInsuranceContributionRate: fc.double({ + min: 0, + max: 1, + noNaN: true, + noDefaultInfinity: true, + }), + monthlyWage: moneyArb, + serviceYears: fc.double({ + min: 0, + max: 40, + noNaN: true, + noDefaultInfinity: true, + }), + compensationScheme: fc.constantFrom<'N' | 'N+1'>('N', 'N+1'), + locality: fc.option(localityArb, { nil: undefined }), + dispatch: fc.option(dispatchArb, { nil: undefined }), + }) + .map((fields) => { + // exactOptionalPropertyTypes:可选项仅在有值时写入键,避免显式 undefined。 + const input: CnComplianceInput = { + socialInsuranceBase: fields.socialInsuranceBase, + socialInsuranceContributionRate: fields.socialInsuranceContributionRate, + monthlyWage: fields.monthlyWage, + serviceYears: fields.serviceYears, + compensationScheme: fields.compensationScheme, + }; + if (fields.locality !== undefined) { + input.locality = fields.locality; + } + if (fields.dispatch !== undefined) { + input.dispatch = fields.dispatch; + } + return input; + }); + +/** 可变的 CN 规则集:社保基数下限与部分地区最低工资取不同取值,覆盖更广输入空间。 */ +const ruleSetArb: fc.Arbitrary = fc + .record({ + lowerBound: moneyArb, + minimumWageByLocality: fc.dictionary( + fc.constantFrom('北京', '上海', '深圳', '杭州'), + fc.double({ min: 0, max: 50_000, noNaN: true, noDefaultInfinity: true }), + ), + }) + .map(({ lowerBound, minimumWageByLocality }) => ({ + region: REGION_CN, + socialInsuranceBase: { lowerBound }, + economicCompensation: { + nRule: + '按在本单位工作年限,每满一年支付一个月工资;六个月以上不满一年按一年计;不满六个月支付半个月工资(N)', + nPlusOneRule: '在 N 的基础上额外支付一个月工资作为代通知金(N+1)', + }, + dispatchRatioCap: 0.1, + minimumWage: { byLocality: minimumWageByLocality }, + })); + +describe('Property 53: 合规判定覆盖与计量标注', () => { + it('合规判定必覆盖四项,且每项测算金额必标注规则项与输入项(Req 16.1, 16.2)', () => { + fc.assert( + fc.property(inputArb, ruleSetArb, (input, ruleSet) => { + const result = assessCnCompliance(input, ruleSet); + + // --- Req 16.1:合规判定必覆盖四项判定项 --- + const judgedItems = result.judgments.map((judgment) => judgment.item); + for (const requiredItem of COMPLIANCE_ITEM_KEYS) { + expect(judgedItems).toContain(requiredItem); + } + // 四项判定项均无重复(恰覆盖四项)。 + expect(new Set(judgedItems).size).toBe(COMPLIANCE_ITEM_KEYS.length); + + // 每项判定均标注规则项(ruleReference 非空)。 + for (const judgment of result.judgments) { + expect(judgment.ruleReference.length).toBeGreaterThan(0); + expect(judgment.basis.length).toBeGreaterThan(0); + } + + // --- Req 16.2:经济补偿(N、N+1)、社保缴费金额与相关合规费用均被测算 --- + const amountLabels = result.amounts.map((amount) => amount.label); + for (const requiredLabel of [ + '经济补偿(N)', + '经济补偿(N+1)', + '社保缴费金额', + '相关合规费用合计', + ]) { + expect(amountLabels).toContain(requiredLabel); + } + + // 每项测算金额必为非负有限值,且标注其规则项与至少一个输入项。 + for (const amount of result.amounts) { + expect(Number.isFinite(amount.amount)).toBe(true); + expect(amount.amount).toBeGreaterThanOrEqual(0); + expect(amount.ruleReference.length).toBeGreaterThan(0); + expect(amount.inputs.length).toBeGreaterThan(0); + for (const ref of amount.inputs) { + expect(ref.field.length).toBeGreaterThan(0); + expect(ref.value).toBeDefined(); + } + } + }), + { numRuns: 100 }, + ); + }); +}); diff --git a/src/compliance/__tests__/judgment.property54.test.ts b/src/compliance/__tests__/judgment.property54.test.ts new file mode 100644 index 0000000..d6e76e5 --- /dev/null +++ b/src/compliance/__tests__/judgment.property54.test.ts @@ -0,0 +1,170 @@ +/** + * Property 54: 合规不满足项标注的属性化测试(Compliance_Rule_Set,Req 16.3)。 + * + * 属性陈述: + * *对任意*合规判定中存在不满足项(社保缴费基数低于法定下限、劳务派遣用工比例 + * 超过 10% 或约定薪酬低于当地最低工资标准)的评估,System *必*将该项标注为 + * 「合规不通过」,并在结果中列出对应规则项(ruleReference)与判定依据(basis)。 + * + * 生成器针对三类不满足项分别构造「确定违规」与「确定合规」两种输入,并保证每次 + * 至少注入一类违规;据此既验证违规项必被标注为合规不通过并进入 failures, + * 也验证合规项不会被误标注(failures 集合恰等于注入的违规项集合)。 + * + * Feature: outsourcing-risk-assessment, Property 54: 合规不满足项标注 + * Validates: Requirements 16.3 + */ + +import { describe, expect, it } from 'vitest'; +import fc from 'fast-check'; +import type { ComplianceRuleSet } from '../../domain/region.js'; +import { REGION_CN } from '../../domain/region.js'; +import type { CnComplianceInput, ComplianceItemKey } from '../judgment.js'; +import { assessCnCompliance } from '../judgment.js'; + +const LOCALITY = 'CN-TEST'; +const DISPATCH_CAP = 0.1; + +// ---------------------------------------------------------------------------- +// 生成器:违规标志(保证至少一类违规)+ 规则集参数 + 取值比例参数。 +// ---------------------------------------------------------------------------- + +/** 三类可判定不满足项的违规标志,至少一类为 true。 */ +const violationFlagsArb: fc.Arbitrary = fc + .tuple(fc.boolean(), fc.boolean(), fc.boolean()) + .map(([base, dispatch, wage]) => + base || dispatch || wage ? [base, dispatch, wage] : [true, dispatch, wage], + ); + +interface ScenarioParams { + readonly flags: readonly [boolean, boolean, boolean]; + /** 社保缴费基数下限(法定下限)。 */ + readonly lowerBound: number; + /** 当地最低工资标准。 */ + readonly minimumWage: number; + /** 用工总量。 */ + readonly totalHeadcount: number; + /** [0,1) 取值比例参数,用于在静态边界内派生具体取值。 */ + readonly baseFrac: number; + readonly wageFrac: number; + readonly dispatchFrac: number; + /** 合规取值的附加量。 */ + readonly baseExtra: number; + readonly wageExtra: number; + /** 经济补偿测算所需输入。 */ + readonly contributionRate: number; + readonly serviceYears: number; + readonly scheme: 'N' | 'N+1'; +} + +const scenarioArb: fc.Arbitrary = fc.record({ + flags: violationFlagsArb, + lowerBound: fc.integer({ min: 1000, max: 30000 }), + minimumWage: fc.integer({ min: 1000, max: 30000 }), + totalHeadcount: fc.integer({ min: 10, max: 1000 }), + baseFrac: fc.double({ min: 0, max: 0.9999999, noNaN: true }), + wageFrac: fc.double({ min: 0, max: 0.9999999, noNaN: true }), + dispatchFrac: fc.double({ min: 0, max: 0.9999999, noNaN: true }), + baseExtra: fc.integer({ min: 0, max: 100000 }), + wageExtra: fc.integer({ min: 0, max: 100000 }), + contributionRate: fc.double({ min: 0, max: 1, noNaN: true }), + serviceYears: fc.double({ min: 0, max: 40, noNaN: true }), + scheme: fc.constantFrom<'N' | 'N+1'>('N', 'N+1'), +}); + +/** 由参数派生规则集、判定输入与期望违规项集合。 */ +function buildScenario(p: ScenarioParams): { + ruleSet: ComplianceRuleSet; + input: CnComplianceInput; + expectedFailures: Set; +} { + const [violateBase, violateDispatch, violateWage] = p.flags; + const expectedFailures = new Set(); + + // 社保缴费基数:违规 → 严格低于下限;合规 → 不低于下限。 + const socialInsuranceBase = violateBase + ? Math.min(p.lowerBound - 1, Math.floor(p.baseFrac * p.lowerBound)) + : p.lowerBound + p.baseExtra; + if (violateBase) expectedFailures.add('社保缴费基数'); + + // 约定月薪:违规 → 严格低于当地最低工资;合规 → 不低于。 + const monthlyWage = violateWage + ? Math.min(p.minimumWage - 1, Math.floor(p.wageFrac * p.minimumWage)) + : p.minimumWage + p.wageExtra; + if (violateWage) expectedFailures.add('当地最低工资'); + + // 劳务派遣用工比例:违规 → 比例 > 10%;合规 → 比例 ≤ 10%。 + const total = p.totalHeadcount; + const capCount = Math.floor(total * DISPATCH_CAP); + let dispatchedHeadcount: number; + if (violateDispatch) { + const low = capCount + 1; // 严格超过 10% 的最小人数。 + const span = total - low + 1; + dispatchedHeadcount = low + Math.floor(p.dispatchFrac * span); + if (dispatchedHeadcount > total) dispatchedHeadcount = total; + expectedFailures.add('劳务派遣用工比例'); + } else { + dispatchedHeadcount = Math.min(capCount, Math.floor(p.dispatchFrac * (capCount + 1))); + } + + const ruleSet: ComplianceRuleSet = { + region: REGION_CN, + socialInsuranceBase: { lowerBound: p.lowerBound }, + economicCompensation: { + nRule: '按工作年限折算月数(N)', + nPlusOneRule: '在 N 基础上加一个月(N+1)', + }, + dispatchRatioCap: DISPATCH_CAP, + minimumWage: { byLocality: { [LOCALITY]: p.minimumWage } }, + }; + + const input: CnComplianceInput = { + socialInsuranceBase, + socialInsuranceContributionRate: p.contributionRate, + monthlyWage, + serviceYears: p.serviceYears, + compensationScheme: p.scheme, + locality: LOCALITY, + dispatch: { dispatchedHeadcount, totalHeadcount: total }, + }; + + return { ruleSet, input, expectedFailures }; +} + +describe('Property 54: 合规不满足项标注', () => { + it('不满足项必被标注为「合规不通过」并在 failures 中列出规则项与判定依据(Req 16.3)', () => { + fc.assert( + fc.property(scenarioArb, (params) => { + const { ruleSet, input, expectedFailures } = buildScenario(params); + const result = assessCnCompliance(input, ruleSet); + + // 1) 注入的每个违规项都被标注为「合规不通过」,且进入 failures, + // 并附带非空的规则项(ruleReference)与判定依据(basis)。 + for (const item of expectedFailures) { + const judgment = result.judgments.find((j) => j.item === item); + expect(judgment).toBeDefined(); + expect(judgment?.status).toBe('合规不通过'); + + const failure = result.failures.find((f) => f.item === item); + expect(failure).toBeDefined(); + expect(failure?.status).toBe('合规不通过'); + expect(failure?.ruleReference.length).toBeGreaterThan(0); + expect(failure?.basis.length).toBeGreaterThan(0); + } + + // 2) failures 恰等于注入的违规项集合(合规项不被误标注)。 + const actualFailures = new Set(result.failures.map((f) => f.item)); + expect(actualFailures).toEqual(expectedFailures); + + // 3) failures 与整体通过状态一致。 + expect(result.passed).toBe(expectedFailures.size === 0); + expect(result.passed).toBe(false); // 生成器保证至少一类违规。 + + // 4) failures 中所有项的状态均为「合规不通过」。 + for (const failure of result.failures) { + expect(failure.status).toBe('合规不通过'); + } + }), + { numRuns: 100 }, + ); + }); +}); diff --git a/src/compliance/__tests__/region.property.test.ts b/src/compliance/__tests__/region.property.test.ts new file mode 100644 index 0000000..f5c3065 --- /dev/null +++ b/src/compliance/__tests__/region.property.test.ts @@ -0,0 +1,56 @@ +/** + * Property 55: Region 记录与默认 的属性化测试(Compliance_Rule_Set,Req 16.4-16.5)。 + * + * 属性陈述: + * - 对任意显式指定的 Region,`resolveRegion` 记录该 Region 且 `isSystemDefault` 为 false。 + * - 当请求未指定 Region(undefined / null)时,默认采用中国大陆(CN)且 `isSystemDefault` 为 true。 + * + * Feature: outsourcing-risk-assessment, Property 55: Region 记录与默认 + * Validates: Requirements 16.4, 16.5 + */ + +import { describe, expect, it } from 'vitest'; +import fc from 'fast-check'; +import type { Region } from '../../domain/region.js'; +import { REGION_CN } from '../../domain/region.js'; +import { resolveRegion } from '../region.js'; + +// ---------------------------------------------------------------------------- +// 生成器:任意显式指定的 Region(含可能等于 CN 的代码,记录行为对取值不敏感)。 +// ---------------------------------------------------------------------------- + +const explicitRegionArb: fc.Arbitrary = fc.record({ + code: fc.string({ minLength: 1, maxLength: 8 }), + name: fc.string({ minLength: 0, maxLength: 12 }), +}); + +describe('Property 55: Region 记录与默认', () => { + it('显式指定的 Region 被原样记录且 isSystemDefault 为 false(Req 16.4)', () => { + fc.assert( + fc.property(explicitRegionArb, (region) => { + const result = resolveRegion(region); + + // 记录评估者显式指定的 Region(同一引用,取值不被篡改)。 + expect(result.region).toBe(region); + // 显式指定时不标注为系统默认值。 + expect(result.isSystemDefault).toBe(false); + }), + { numRuns: 100 }, + ); + }); + + it('未指定 Region(undefined / null)时默认 CN 且 isSystemDefault 为 true(Req 16.5)', () => { + fc.assert( + fc.property(fc.constantFrom(undefined, null), (absent) => { + const result = resolveRegion(absent); + + // 默认采用中国大陆(CN)。 + expect(result.region).toEqual(REGION_CN); + expect(result.region.code).toBe('CN'); + // 标注为系统默认值。 + expect(result.isSystemDefault).toBe(true); + }), + { numRuns: 100 }, + ); + }); +}); diff --git a/src/compliance/__tests__/ruleSet.unsupported.property.test.ts b/src/compliance/__tests__/ruleSet.unsupported.property.test.ts new file mode 100644 index 0000000..1c139b7 --- /dev/null +++ b/src/compliance/__tests__/ruleSet.unsupported.property.test.ts @@ -0,0 +1,66 @@ +/** + * Property 56: 无规则集地域拒绝合规处理 的属性化测试(Compliance_Rule_Set,Req 16.6)。 + * + * 属性陈述:对任意当前无对应规则集的 Region,`loadComplianceRuleSet` 必抛出 + * `UnsupportedRegionError`,且错误消息指示该地域「暂不支持」——据此拒绝合规判定与费用测算。 + * + * Feature: outsourcing-risk-assessment, Property 56: 无规则集地域拒绝合规处理 + * Validates: Requirements 16.6 + */ + +import { describe, expect, it } from 'vitest'; +import fc from 'fast-check'; +import type { Region } from '../../domain/region.js'; +import { UnsupportedRegionError } from '../errors.js'; +import { + createDefaultComplianceRegistry, + isRegionSupported, + loadComplianceRuleSet, +} from '../rule-set.js'; + +// ---------------------------------------------------------------------------- +// 生成器:当前无对应规则集的 Region +// ---------------------------------------------------------------------------- +// +// 默认注册表(首版仅含中国大陆 CN);匹配对地域代码大小写与首尾空白不敏感。 +// 因此排除规范化后等于 'CN' 的任何代码,即可保证生成「无规则集」的 Region。 + +const normalize = (code: string): string => code.trim().toUpperCase(); + +const unsupportedRegionArb: fc.Arbitrary = fc + .record({ + code: fc.string({ minLength: 0, maxLength: 8 }), + name: fc.string({ minLength: 0, maxLength: 12 }), + }) + .filter((region) => normalize(region.code) !== 'CN'); + +describe('Property 56: 无规则集地域拒绝合规处理', () => { + it('对任意无规则集的 Region,loadComplianceRuleSet 抛出 UnsupportedRegionError 且提示暂不支持', () => { + fc.assert( + fc.property(unsupportedRegionArb, (region) => { + const registry = createDefaultComplianceRegistry(); + + // 前置:该 Region 确实未被支持。 + expect(isRegionSupported(region, registry)).toBe(false); + + let thrown: unknown; + try { + loadComplianceRuleSet(region, registry); + } catch (error) { + thrown = error; + } + + // 必抛出 UnsupportedRegionError。 + expect(thrown).toBeInstanceOf(UnsupportedRegionError); + + const err = thrown as UnsupportedRegionError; + // 错误关联触发的地域。 + expect(err.region).toBe(region); + // 消息指示「暂不支持」(message 与面向评估者的 userMessage 一致)。 + expect(err.message).toContain('暂不支持'); + expect(err.userMessage).toContain('暂不支持'); + }), + { numRuns: 100 }, + ); + }); +}); diff --git a/src/compliance/errors.ts b/src/compliance/errors.ts new file mode 100644 index 0000000..6d19b29 --- /dev/null +++ b/src/compliance/errors.ts @@ -0,0 +1,48 @@ +/** + * 合规模块错误类型。 + * + * 错误处理遵循统一原则:输入校验前置、错误可解释、失败不破坏既有状态。 + */ + +import type { Region } from '../domain/region.js'; + +/** + * 请求的 Region 当前无对应 Compliance_Rule_Set 时抛出(Req 16.6)。 + * + * System 据此拒绝该 Region 的合规判定与费用测算、不生成合规结论, + * 并向评估者返回提示该地域暂不支持的消息(`message` / `userMessage`)。 + */ +export class UnsupportedRegionError extends Error { + /** 触发错误的地域。 */ + readonly region: Region; + /** 面向评估者的可读提示,固定包含"暂不支持"。 */ + readonly userMessage: string; + + constructor(region: Region) { + const userMessage = `地域「${region.name}(${region.code})」暂不支持合规判定与费用测算`; + super(userMessage); + this.name = 'UnsupportedRegionError'; + this.region = region; + this.userMessage = userMessage; + // 维持原型链(编译目标低于 ES2015 时的兼容保障)。 + Object.setPrototypeOf(this, UnsupportedRegionError.prototype); + } +} + +/** + * 合规判定/费用测算输入非法时抛出(输入校验前置原则)。 + * + * 例如金额为负、社保费率不在 [0, 1]、工作年限为负、劳务派遣总人数非正等。 + * 在执行任何计算前校验,确保失败不破坏既有状态、错误可解释。 + */ +export class InvalidComplianceInputError extends Error { + /** 出错的输入项字段名。 */ + readonly field: string; + + constructor(field: string, message: string) { + super(message); + this.name = 'InvalidComplianceInputError'; + this.field = field; + Object.setPrototypeOf(this, InvalidComplianceInputError.prototype); + } +} diff --git a/src/compliance/index.ts b/src/compliance/index.ts new file mode 100644 index 0000000..41cbcf5 --- /dev/null +++ b/src/compliance/index.ts @@ -0,0 +1,14 @@ +/** + * Compliance_Rule_Set 地域合规规则集模块(Req 16)。 + * + * 职责: + * - 按 Region 加载 ComplianceRuleSet({@link loadComplianceRuleSet} / {@link ComplianceRuleSetRegistry})。 + * - 记录 Assessment 所采用的 Region,未指定时默认 CN 并标注系统默认值({@link resolveRegion})。 + * - Region 无对应规则集时拒绝合规处理并提示暂不支持({@link UnsupportedRegionError})。 + * - 执行 CN 合规判定与费用测算({@link assessCnCompliance},Req 16.1-16.3)。 + */ + +export * from './errors.js'; +export * from './region.js'; +export * from './rule-set.js'; +export * from './judgment.js'; diff --git a/src/compliance/judgment.ts b/src/compliance/judgment.ts new file mode 100644 index 0000000..a71f2a9 --- /dev/null +++ b/src/compliance/judgment.ts @@ -0,0 +1,421 @@ +/** + * 中国大陆(CN)合规判定与费用测算引擎(Req 16.1, 16.2, 16.3)。 + * + * 依据按 Region 加载的 {@link ComplianceRuleSet} 对一次 Assessment: + * - 执行合规判定,判定项覆盖四项:社保缴费基数、经济补偿 N 与 N+1、 + * 劳务派遣用工比例上限 10%、当地最低工资标准(Req 16.1)。 + * - 测算经济补偿(N 与 N+1)金额、社保缴费金额与相关合规费用,并为每项金额 + * 标注其所依据的规则项与输入项(Req 16.2)。 + * - 对任一不满足项(社保缴费基数低于法定下限、劳务派遣用工比例超过 10%、 + * 约定薪酬低于当地最低工资标准)标注为「合规不通过」并给出规则项与判定依据(Req 16.3)。 + * + * 引擎与具体 Region 解耦:仅消费传入的 ComplianceRuleSet,跨境通过新增规则集扩展 + * 而不改本引擎(Req 16,地域参数化)。计算为确定性纯函数,便于属性化测试约束 + * (Property 53 覆盖与计量标注、Property 54 不满足项标注)。 + * + * 规则内容来源为《劳务派遣暂行规定》《劳动合同法》既有规则,仅用于建模规则结构, + * 不构成法律意见。 + */ + +import type { Money } from '../domain/common.js'; +import type { ComplianceRuleSet, Region } from '../domain/region.js'; +import { InvalidComplianceInputError } from './errors.js'; + +/** 浮点比较容差(用于工作年限折算月数等比较)。 */ +const EPSILON = 1e-9; + +/** 合规判定项标识(恰为 Req 16.1 要求覆盖的四项)。 */ +export type ComplianceItemKey = + | '社保缴费基数' + | '经济补偿' + | '劳务派遣用工比例' + | '当地最低工资'; + +/** Req 16.1 要求合规判定必须覆盖的四项判定项标识。 */ +export const COMPLIANCE_ITEM_KEYS: readonly ComplianceItemKey[] = [ + '社保缴费基数', + '经济补偿', + '劳务派遣用工比例', + '当地最低工资', +]; + +/** + * 单项合规判定状态。 + * - 合规通过 / 合规不通过:存在明确合规判据的项(Req 16.3)。 + * - 已测算:仅作金额测算、无通过/不通过判据的项(经济补偿)。 + * - 不适用:缺少判定所需输入(如未提供劳务派遣人数、当地无最低工资标准)。 + */ +export type ComplianceJudgmentStatus = '合规通过' | '合规不通过' | '已测算' | '不适用'; + +/** + * 判定/计量所引用的单个输入项(用于可解释标注,Req 16.2/16.3)。 + */ +export interface ComplianceInputRef { + /** 输入项中文标签。 */ + field: string; + /** 输入项取值。 */ + value: number | string; +} + +/** + * 一项合规判定结果,标注其所依据的规则项与输入项(Req 16.1, 16.3)。 + */ +export interface ComplianceJudgment { + /** 判定项标识。 */ + item: ComplianceItemKey; + /** 判定状态。 */ + status: ComplianceJudgmentStatus; + /** 所依据的规则项描述(报告据此列出对应规则项)。 */ + ruleReference: string; + /** 判定所依据的输入项。 */ + inputs: readonly ComplianceInputRef[]; + /** 判定依据/说明(报告据此列出判定依据)。 */ + basis: string; +} + +/** + * 一项测算金额,标注其所依据的规则项与输入项(Req 16.2)。 + */ +export interface ComplianceAmount { + /** 金额标签,如「经济补偿(N)」。 */ + label: string; + /** 金额(人民币元,非负)。 */ + amount: Money; + /** 所依据的规则项描述。 */ + ruleReference: string; + /** 计算所依据的输入项。 */ + inputs: readonly ComplianceInputRef[]; +} + +/** 劳务派遣用工人数信息(仅劳务派遣相关时提供)。 */ +export interface DispatchHeadcount { + /** 被派遣劳动者用工人数。 */ + dispatchedHeadcount: number; + /** 用工单位用工总量(含被派遣劳动者)。 */ + totalHeadcount: number; +} + +/** + * 合规判定与费用测算的输入项(Req 16.2/16.3 的可解释输入来源)。 + */ +export interface CnComplianceInput { + /** 实际社保缴费基数(人民币元/月)。 */ + socialInsuranceBase: Money; + /** 用人单位社保缴费费率(取值 [0, 1]),用于测算社保缴费金额。 */ + socialInsuranceContributionRate: number; + /** 约定月薪(人民币元/月),用于经济补偿测算及与当地最低工资比较。 */ + monthlyWage: Money; + /** 在本单位工作年限(年,可含小数表示不足一年的部分)。 */ + serviceYears: number; + /** 经济补偿计法:'N'(仅经济补偿)或 'N+1'(含代通知金)。 */ + compensationScheme: 'N' | 'N+1'; + /** 当地标识,用于在规则集中查询当地最低工资标准;缺省则该项不适用。 */ + locality?: string; + /** 劳务派遣用工占比信息;缺省则劳务派遣用工比例判定不适用。 */ + dispatch?: DispatchHeadcount; +} + +/** + * 合规判定与费用测算的整体结果(Req 16.1-16.3)。 + */ +export interface CnComplianceResult { + /** 适用地域。 */ + region: Region; + /** 覆盖四项的合规判定列表(Req 16.1)。 */ + judgments: readonly ComplianceJudgment[]; + /** 测算金额列表,每项标注规则项与输入项(Req 16.2)。 */ + amounts: readonly ComplianceAmount[]; + /** 整体是否合规通过:无任一「合规不通过」项时为 true(Req 16.3)。 */ + passed: boolean; + /** 合规不通过项列表(报告据此列出,Req 16.3)。 */ + failures: readonly ComplianceJudgment[]; +} + +/** 经济补偿月数测算的明细结果。 */ +export interface CompensationMonths { + /** 满整年数。 */ + fullYears: number; + /** 不足一年部分折算月数(0、0.5 或 1)。 */ + remainderMonths: number; + /** 折算总月数(N 规则月数)。 */ + total: number; +} + +/** + * 按工作年限将经济补偿折算为月数(N 规则,Req 16.1/16.2)。 + * + * 规则:每满一年支付一个月工资;六个月以上不满一年按一年计(一个月); + * 不满六个月支付半个月工资(半个月)。 + * + * @param serviceYears 在本单位工作年限(年,非负,可含小数)。 + * @returns 折算明细,其中 `total` 为 N 规则对应的月数。 + */ +export function computeCompensationMonths(serviceYears: number): CompensationMonths { + const fullYears = Math.floor(serviceYears + EPSILON); + const remainderMonths = (serviceYears - fullYears) * 12; + let remainder: number; + if (remainderMonths >= 6 - EPSILON) { + // 六个月以上不满一年按一年计。 + remainder = 1; + } else if (remainderMonths > EPSILON) { + // 不满六个月支付半个月工资。 + remainder = 0.5; + } else { + remainder = 0; + } + return { fullYears, remainderMonths: remainder, total: fullYears + remainder }; +} + +/** 校验输入项合法性(输入校验前置,失败抛 {@link InvalidComplianceInputError})。 */ +function validateInput(input: CnComplianceInput): void { + if (!Number.isFinite(input.socialInsuranceBase) || input.socialInsuranceBase < 0) { + throw new InvalidComplianceInputError( + 'socialInsuranceBase', + '社保缴费基数必须为非负数', + ); + } + if ( + !Number.isFinite(input.socialInsuranceContributionRate) || + input.socialInsuranceContributionRate < 0 || + input.socialInsuranceContributionRate > 1 + ) { + throw new InvalidComplianceInputError( + 'socialInsuranceContributionRate', + '社保缴费费率必须在 [0, 1] 区间内', + ); + } + if (!Number.isFinite(input.monthlyWage) || input.monthlyWage < 0) { + throw new InvalidComplianceInputError('monthlyWage', '约定月薪必须为非负数'); + } + if (!Number.isFinite(input.serviceYears) || input.serviceYears < 0) { + throw new InvalidComplianceInputError('serviceYears', '工作年限必须为非负数'); + } + if (input.dispatch !== undefined) { + const { dispatchedHeadcount, totalHeadcount } = input.dispatch; + if (!Number.isFinite(dispatchedHeadcount) || dispatchedHeadcount < 0) { + throw new InvalidComplianceInputError( + 'dispatch.dispatchedHeadcount', + '被派遣用工人数必须为非负数', + ); + } + if (!Number.isFinite(totalHeadcount) || totalHeadcount <= 0) { + throw new InvalidComplianceInputError( + 'dispatch.totalHeadcount', + '用工总量必须为正数', + ); + } + if (dispatchedHeadcount > totalHeadcount) { + throw new InvalidComplianceInputError( + 'dispatch.dispatchedHeadcount', + '被派遣用工人数不得超过用工总量', + ); + } + } +} + +/** 将比率格式化为百分比文本(用于判定依据说明)。 */ +function formatPercent(ratio: number): string { + return `${(ratio * 100).toFixed(2)}%`; +} + +/** 构造社保缴费基数判定项(Req 16.1, 16.3)。 */ +function judgeSocialInsuranceBase( + input: CnComplianceInput, + ruleSet: ComplianceRuleSet, +): ComplianceJudgment { + const lowerBound = ruleSet.socialInsuranceBase.lowerBound; + const passed = input.socialInsuranceBase >= lowerBound; + return { + item: '社保缴费基数', + status: passed ? '合规通过' : '合规不通过', + ruleReference: `社保缴费基数下限 ${lowerBound} 元/月`, + inputs: [{ field: '实际社保缴费基数', value: input.socialInsuranceBase }], + basis: passed + ? `实际社保缴费基数 ${input.socialInsuranceBase} 元不低于法定下限 ${lowerBound} 元` + : `实际社保缴费基数 ${input.socialInsuranceBase} 元低于法定下限 ${lowerBound} 元`, + }; +} + +/** 构造经济补偿测算项(仅测算、无通过/不通过判据,Req 16.1, 16.2)。 */ +function judgeEconomicCompensation( + input: CnComplianceInput, + ruleSet: ComplianceRuleSet, + months: CompensationMonths, +): ComplianceJudgment { + const { economicCompensation } = ruleSet; + return { + item: '经济补偿', + status: '已测算', + ruleReference: `${economicCompensation.nRule};${economicCompensation.nPlusOneRule}`, + inputs: [ + { field: '约定月薪', value: input.monthlyWage }, + { field: '工作年限(年)', value: input.serviceYears }, + { field: '经济补偿计法', value: input.compensationScheme }, + ], + basis: `工作年限 ${input.serviceYears} 年折算为 ${months.total} 个月工资;采用 ${input.compensationScheme} 计法`, + }; +} + +/** 构造劳务派遣用工比例判定项(Req 16.1, 16.3)。 */ +function judgeDispatchRatio( + input: CnComplianceInput, + ruleSet: ComplianceRuleSet, +): ComplianceJudgment { + const cap = ruleSet.dispatchRatioCap; + if (input.dispatch === undefined) { + return { + item: '劳务派遣用工比例', + status: '不适用', + ruleReference: `劳务派遣用工比例上限 ${formatPercent(cap)}`, + inputs: [], + basis: '未提供劳务派遣用工人数,劳务派遣用工比例判定不适用', + }; + } + const { dispatchedHeadcount, totalHeadcount } = input.dispatch; + const ratio = dispatchedHeadcount / totalHeadcount; + const passed = ratio <= cap + EPSILON; + return { + item: '劳务派遣用工比例', + status: passed ? '合规通过' : '合规不通过', + ruleReference: `劳务派遣用工比例上限 ${formatPercent(cap)}`, + inputs: [ + { field: '被派遣用工人数', value: dispatchedHeadcount }, + { field: '用工总量', value: totalHeadcount }, + ], + basis: passed + ? `劳务派遣用工比例 ${formatPercent(ratio)} 未超过上限 ${formatPercent(cap)}` + : `劳务派遣用工比例 ${formatPercent(ratio)} 超过上限 ${formatPercent(cap)}`, + }; +} + +/** 构造当地最低工资判定项(Req 16.1, 16.3)。 */ +function judgeMinimumWage( + input: CnComplianceInput, + ruleSet: ComplianceRuleSet, +): ComplianceJudgment { + const { locality } = input; + const minimumWage = + locality !== undefined ? ruleSet.minimumWage.byLocality[locality] : undefined; + if (locality === undefined || minimumWage === undefined) { + return { + item: '当地最低工资', + status: '不适用', + ruleReference: '当地最低工资标准', + inputs: + locality !== undefined ? [{ field: '当地标识', value: locality }] : [], + basis: + locality === undefined + ? '未提供当地标识,当地最低工资判定不适用' + : `规则集中无当地标识「${locality}」的最低工资标准,判定不适用`, + }; + } + const passed = input.monthlyWage >= minimumWage; + return { + item: '当地最低工资', + status: passed ? '合规通过' : '合规不通过', + ruleReference: `当地(${locality})最低工资标准 ${minimumWage} 元/月`, + inputs: [ + { field: '约定月薪', value: input.monthlyWage }, + { field: '当地标识', value: locality }, + ], + basis: passed + ? `约定月薪 ${input.monthlyWage} 元不低于当地最低工资标准 ${minimumWage} 元` + : `约定月薪 ${input.monthlyWage} 元低于当地最低工资标准 ${minimumWage} 元`, + }; +} + +/** 构造经济补偿/社保/合规费用的测算金额(Req 16.2)。 */ +function measureAmounts( + input: CnComplianceInput, + ruleSet: ComplianceRuleSet, + months: CompensationMonths, +): ComplianceAmount[] { + const { economicCompensation } = ruleSet; + const compensationN = months.total * input.monthlyWage; + const compensationNPlusOne = compensationN + input.monthlyWage; + const socialInsuranceAmount = + input.socialInsuranceBase * input.socialInsuranceContributionRate; + const applicableCompensation = + input.compensationScheme === 'N+1' ? compensationNPlusOne : compensationN; + const totalComplianceCost = applicableCompensation + socialInsuranceAmount; + + const compensationInputs: readonly ComplianceInputRef[] = [ + { field: '约定月薪', value: input.monthlyWage }, + { field: '工作年限(年)', value: input.serviceYears }, + ]; + + return [ + { + label: '经济补偿(N)', + amount: compensationN, + ruleReference: economicCompensation.nRule, + inputs: compensationInputs, + }, + { + label: '经济补偿(N+1)', + amount: compensationNPlusOne, + ruleReference: economicCompensation.nPlusOneRule, + inputs: compensationInputs, + }, + { + label: '社保缴费金额', + amount: socialInsuranceAmount, + ruleReference: `社保缴费基数下限 ${ruleSet.socialInsuranceBase.lowerBound} 元/月`, + inputs: [ + { field: '实际社保缴费基数', value: input.socialInsuranceBase }, + { field: '社保缴费费率', value: input.socialInsuranceContributionRate }, + ], + }, + { + label: '相关合规费用合计', + amount: totalComplianceCost, + ruleReference: `经济补偿(${input.compensationScheme})与社保缴费金额合计`, + inputs: [ + { field: `经济补偿(${input.compensationScheme})`, value: applicableCompensation }, + { field: '社保缴费金额', value: socialInsuranceAmount }, + ], + }, + ]; +} + +/** + * 执行中国大陆(CN)合规判定与费用测算(Req 16.1, 16.2, 16.3)。 + * + * 判定项恒覆盖四项(社保缴费基数、经济补偿、劳务派遣用工比例、当地最低工资); + * 测算经济补偿(N 与 N+1)、社保缴费金额与相关合规费用,每项标注规则项与输入项; + * 任一不满足项标注为「合规不通过」并随结果一并返回(含规则项与判定依据)。 + * + * @param input 合规判定/费用测算输入。 + * @param ruleSet 按 Region 加载的合规规则集(由调用方经 + * {@link loadComplianceRuleSet} 提供,本引擎不耦合具体 Region)。 + * @returns 合规判定与费用测算结果。 + * @throws {InvalidComplianceInputError} 当输入项非法时(输入校验前置)。 + */ +export function assessCnCompliance( + input: CnComplianceInput, + ruleSet: ComplianceRuleSet, +): CnComplianceResult { + validateInput(input); + + const months = computeCompensationMonths(input.serviceYears); + + const judgments: ComplianceJudgment[] = [ + judgeSocialInsuranceBase(input, ruleSet), + judgeEconomicCompensation(input, ruleSet, months), + judgeDispatchRatio(input, ruleSet), + judgeMinimumWage(input, ruleSet), + ]; + + const amounts = measureAmounts(input, ruleSet, months); + const failures = judgments.filter( + (judgment) => judgment.status === '合规不通过', + ); + + return { + region: ruleSet.region, + judgments, + amounts, + passed: failures.length === 0, + failures, + }; +} diff --git a/src/compliance/region.ts b/src/compliance/region.ts new file mode 100644 index 0000000..381e787 --- /dev/null +++ b/src/compliance/region.ts @@ -0,0 +1,36 @@ +/** + * Region 解析:记录 Assessment 所采用的 Region,未指定时默认 CN(Req 16.4-16.5)。 + */ + +import type { Region } from '../domain/region.js'; +import { REGION_CN } from '../domain/region.js'; + +/** + * Region 解析结果(Req 16.4-16.5)。 + * + * 记录最终采用的 Region,并标注其是否由系统默认值填充(请求未指定 Region)。 + */ +export interface RegionResolution { + /** 本次 Assessment 最终采用的 Region。 */ + region: Region; + /** 是否为系统默认值:true 表示请求未指定 Region 而采用了中国大陆(CN)。 */ + isSystemDefault: boolean; +} + +/** + * 解析创建 Assessment 请求所采用的 Region(Req 16.4-16.5)。 + * + * - 请求显式指定 Region 时,采用该 Region,`isSystemDefault` 为 false。 + * - 请求未指定 Region(undefined / null)时,采用中国大陆(CN)并标注为系统默认值。 + * + * 本函数仅负责"记录与默认",不校验该 Region 是否存在对应规则集; + * 规则集缺失的拒绝由 {@link UnsupportedRegionError} 在加载规则集时处理(Req 16.6)。 + * + * @param requested 请求指定的 Region;未指定时传入 undefined 或 null。 + */ +export function resolveRegion(requested?: Region | null): RegionResolution { + if (requested == null) { + return { region: REGION_CN, isSystemDefault: true }; + } + return { region: requested, isSystemDefault: false }; +} diff --git a/src/compliance/rule-set.ts b/src/compliance/rule-set.ts new file mode 100644 index 0000000..c61949f --- /dev/null +++ b/src/compliance/rule-set.ts @@ -0,0 +1,121 @@ +/** + * 按 Region 加载 Compliance_Rule_Set 的注册表与默认规则集(Req 16.1, 16.6)。 + * + * 规则集按 Region 加载;跨境通过向注册表新增 ComplianceRuleSet 扩展, + * 而不改判定/费用引擎(地域参数化合规规则集,Req 16)。 + * Region 无对应规则集时拒绝合规处理并提示暂不支持(Req 16.6)。 + */ + +import type { ComplianceRuleSet, Region } from '../domain/region.js'; +import { REGION_CN } from '../domain/region.js'; +import { UnsupportedRegionError } from './errors.js'; + +/** + * 中国大陆(CN)合规规则集(Req 16.1)。 + * + * 来源为《劳务派遣暂行规定》《劳动合同法》既有规则,仅用于建模规则结构、不作法律意见。 + * 社保缴费基数下限与当地最低工资按地区取值的细化在后续合规判定/费用测算任务中填充。 + */ +export const CN_COMPLIANCE_RULE_SET: ComplianceRuleSet = { + region: REGION_CN, + socialInsuranceBase: { + // 按地区取值,首版以 0 占位,后续任务按地区填充下限。 + lowerBound: 0, + }, + economicCompensation: { + nRule: + '按在本单位工作年限,每满一年支付一个月工资;六个月以上不满一年按一年计;不满六个月支付半个月工资(N)', + nPlusOneRule: '在 N 的基础上额外支付一个月工资作为代通知金(N+1)', + }, + // 劳务派遣用工比例上限 10%。 + dispatchRatioCap: 0.1, + minimumWage: { + // 按地区/城市标识映射月最低工资,首版为空、后续任务按地区填充。 + byLocality: {}, + }, +}; + +/** 规范化地域代码用于匹配(大小写与首尾空白不敏感)。 */ +function normalizeCode(code: string): string { + return code.trim().toUpperCase(); +} + +/** + * 地域合规规则集注册表(Req 16)。 + * + * 以 Region 代码为键登记 ComplianceRuleSet;新增地域只需注册新规则集, + * 无需改动调用方(判定/费用引擎)。 + */ +export class ComplianceRuleSetRegistry { + private readonly ruleSets = new Map(); + + /** + * @param initial 初始登记的规则集集合。 + */ + constructor(initial: readonly ComplianceRuleSet[] = []) { + for (const ruleSet of initial) { + this.register(ruleSet); + } + } + + /** + * 登记(或覆盖)某地域的合规规则集。 + */ + register(ruleSet: ComplianceRuleSet): void { + this.ruleSets.set(normalizeCode(ruleSet.region.code), ruleSet); + } + + /** + * 判断某 Region 是否存在对应规则集(Req 16.6 的前置判断)。 + */ + has(region: Region): boolean { + return this.ruleSets.has(normalizeCode(region.code)); + } + + /** + * 加载某 Region 的合规规则集。 + * + * @throws {UnsupportedRegionError} 当该 Region 无对应规则集时(Req 16.6)。 + */ + load(region: Region): ComplianceRuleSet { + const ruleSet = this.ruleSets.get(normalizeCode(region.code)); + if (ruleSet === undefined) { + throw new UnsupportedRegionError(region); + } + return ruleSet; + } +} + +/** + * 创建默认注册表(首版仅含中国大陆 CN 规则集,Req 16.1)。 + */ +export function createDefaultComplianceRegistry(): ComplianceRuleSetRegistry { + return new ComplianceRuleSetRegistry([CN_COMPLIANCE_RULE_SET]); +} + +/** 进程级默认注册表实例(首版仅含 CN)。 */ +export const defaultComplianceRegistry: ComplianceRuleSetRegistry = + createDefaultComplianceRegistry(); + +/** + * 判断某 Region 当前是否被支持(存在对应规则集,Req 16.6)。 + */ +export function isRegionSupported( + region: Region, + registry: ComplianceRuleSetRegistry = defaultComplianceRegistry, +): boolean { + return registry.has(region); +} + +/** + * 按 Region 加载合规规则集(Req 16.1, 16.6)。 + * + * @throws {UnsupportedRegionError} 当该 Region 无对应规则集时—— + * 据此拒绝合规判定与费用测算并提示暂不支持(Req 16.6)。 + */ +export function loadComplianceRuleSet( + region: Region, + registry: ComplianceRuleSetRegistry = defaultComplianceRegistry, +): ComplianceRuleSet { + return registry.load(region); +} diff --git a/src/config/__tests__/instantiateRiskModel.property7.test.ts b/src/config/__tests__/instantiateRiskModel.property7.test.ts new file mode 100644 index 0000000..4ab294d --- /dev/null +++ b/src/config/__tests__/instantiateRiskModel.property7.test.ts @@ -0,0 +1,301 @@ +/** + * Property 7: 模板实例化结构保持 的属性化测试(Config_Center,Req 2.3)。 + * + * 属性陈述:对任意合法模板,实例化为 Risk_Model 后必完整保留其全部 + * Dimension、Indicator、权重、Scoring_Rule、Redline、追问话术(askPrompt) + * 及各 Dimension 与 Indicator 的启用/停用状态,无丢失、无篡改。 + * + * 本测试生成"合法模板"——同级启用 Dimension 权重之和、每个 Dimension 下同级 + * 启用 Indicator 权重之和均等于 100%,每个 Indicator 的 Scoring_Rule 覆盖 + * Risk_Level 1 至 5,且必填组成项齐备——使其通过 instantiateRiskModel 的 + * 校验门槛,从而专注验证"实例化的结构保持性"。 + * + * 断言两层含义: + * 1. 结构保持:实例化结果逐项深相等于模板配置(id/name/businessType、 + * 全部维度与指标及其权重/启停/评分规则/话术、全部红线)。 + * 2. 无篡改且无共享可变引用:实例化结果与输入模板不共享引用,且对结果的 + * 任意深层改动均不回写影响原模板。 + * + * Feature: outsourcing-risk-assessment, Property 7: 模板实例化结构保持 + * Validates: Requirements 2.3 + */ + +import { describe, expect, it } from 'vitest'; +import fc from 'fast-check'; +import { + BUSINESS_TYPE_VALUES, + RISK_LEVEL_VALUES, + type RiskLevel, +} from '../../domain/common.js'; +import type { + Dimension, + Indicator, + Redline, + RiskModelConfig, + ScoringRule, + Template, +} from '../../domain/model.js'; +import { instantiateRiskModel } from '../instantiateRiskModel.js'; + +// ---------------------------------------------------------------------------- +// 生成器:构造"合法模板",保证通过 instantiateRiskModel 的必填项与权重和校验。 +// ---------------------------------------------------------------------------- + +const businessTypeArb = fc.constantFrom(...BUSINESS_TYPE_VALUES); + +/** + * 将 100 划分为 `parts` 个各 ≥1 的整数,且其和恰为 100。 + * 用于生成"同级启用项权重之和等于 100%"的合法权重向量。 + */ +function partitionArb(parts: number): fc.Arbitrary { + if (parts <= 1) { + return fc.constant([100]); + } + return fc + .uniqueArray(fc.integer({ min: 1, max: 99 }), { + minLength: parts - 1, + maxLength: parts - 1, + }) + .map((cuts) => { + const sorted = [...cuts].sort((a, b) => a - b); + const weights: number[] = []; + let prev = 0; + for (const cut of sorted) { + weights.push(cut - prev); + prev = cut; + } + weights.push(100 - prev); + return weights; + }); +} + +/** 覆盖 Risk_Level 1 至 5 全部级别的评分规则(满足必填校验,Req 11.3)。 */ +const scoringRulesArb: fc.Arbitrary = fc.tuple( + ...RISK_LEVEL_VALUES.map((level: RiskLevel) => + fc.record({ + level: fc.constant(level), + label: fc.string({ minLength: 0, maxLength: 6 }), + description: fc.string({ minLength: 0, maxLength: 12 }), + }), + ), +); + +/** 构造单个指标,指定其稳定标识、启停状态与权重。 */ +function makeIndicator( + id: string, + enabled: boolean, + weight: number, +): fc.Arbitrary { + return fc + .record({ + name: fc.string({ minLength: 0, maxLength: 8 }), + scoringRules: scoringRulesArb, + evidenceRequired: fc.string({ minLength: 0, maxLength: 8 }), + askPrompt: fc.string({ minLength: 0, maxLength: 12 }), + }) + .map((r) => ({ + id, + name: r.name, + weight, + enabled, + scoringRules: r.scoringRules, + evidenceRequired: r.evidenceRequired, + askPrompt: r.askPrompt, + })); +} + +/** + * 生成某维度下的指标集合:启用指标权重之和恒为 100%(满足校验), + * 另附若干停用指标(权重任意,不计入权重和,用于验证停用状态保持)。 + */ +const indicatorsArb: fc.Arbitrary = fc + .record({ + enabledCount: fc.integer({ min: 1, max: 3 }), + disabledCount: fc.integer({ min: 0, max: 2 }), + }) + .chain(({ enabledCount, disabledCount }) => + partitionArb(enabledCount).chain((enabledWeights) => { + const enabledArbs = enabledWeights.map((w, idx) => + makeIndicator(`i-en-${idx}`, true, w), + ); + const disabledArbs = Array.from({ length: disabledCount }, (_unused, idx) => + fc.nat({ max: 100 }).chain((w) => makeIndicator(`i-dis-${idx}`, false, w)), + ); + return fc.tuple(...enabledArbs, ...disabledArbs); + }), + ); + +/** 构造单个维度,指定其稳定标识、启停状态与权重。 */ +function makeDimension( + id: string, + enabled: boolean, + weight: number, +): fc.Arbitrary { + return fc + .record({ + name: fc.string({ minLength: 0, maxLength: 8 }), + indicators: indicatorsArb, + }) + .map((r) => ({ + id, + name: r.name, + weight, + enabled, + indicators: r.indicators, + })); +} + +/** + * 生成维度集合:启用维度权重之和恒为 100%(满足校验),另附若干停用维度 + * (权重任意,不计入权重和)。每个维度自身亦为合法结构。 + */ +const dimensionsArb: fc.Arbitrary = fc + .record({ + enabledCount: fc.integer({ min: 1, max: 3 }), + disabledCount: fc.integer({ min: 0, max: 2 }), + }) + .chain(({ enabledCount, disabledCount }) => + partitionArb(enabledCount).chain((enabledWeights) => { + const enabledArbs = enabledWeights.map((w, idx) => + makeDimension(`d-en-${idx}`, true, w), + ); + const disabledArbs = Array.from({ length: disabledCount }, (_unused, idx) => + fc.nat({ max: 100 }).chain((w) => makeDimension(`d-dis-${idx}`, false, w)), + ); + return fc.tuple(...enabledArbs, ...disabledArbs); + }), + ); + +const redlineArb = (id: string): fc.Arbitrary => + fc.record({ + id: fc.constant(id), + triggerCondition: fc.string({ minLength: 0, maxLength: 10 }), + consequence: fc.string({ minLength: 0, maxLength: 10 }), + enabled: fc.boolean(), + }); + +const redlinesArb: fc.Arbitrary = fc + .uniqueArray(fc.constantFrom('r1', 'r2', 'r3'), { minLength: 0, maxLength: 3 }) + .chain((ids) => fc.tuple(...ids.map((id) => redlineArb(id)))); + +const riskModelConfigArb: fc.Arbitrary = fc.record({ + name: fc.string({ minLength: 0, maxLength: 10 }), + businessType: businessTypeArb, + dimensions: dimensionsArb, + redlines: redlinesArb, +}); + +const validTemplateArb: fc.Arbitrary