diff --git a/data/samples/delivery-trial/srm_delivery_record.csv b/data/samples/delivery-trial/srm_delivery_record.csv index f06cb67..73b8be8 100644 --- a/data/samples/delivery-trial/srm_delivery_record.csv +++ b/data/samples/delivery-trial/srm_delivery_record.csv @@ -14,6 +14,6 @@ ERP 收货单号,ERP 订单号,ERP 订单行号,供应商,物料,工厂,需求 5000090012,4500013006,20,示例工业品商贸有限公司,EX-300002,P100,2026-10-20,2026-10-20,2026-10-21,50,-20,,盒,-500.00,未检,,,退货,, 5000090013,4500013007,10,示例包装制品有限公司,EX-200001,一厂,2026-10-22,2026-10-22,2026-10-22,2000,2000,,只,4800.00,未检,,,,, ,4500013007,20,示例包装制品有限公司,EX-200002,P100,2026-10-22,2026-10-22,2026-10-22,200,200,,卷,1700.00,未检,,,,, -5000090015,4500013008,10,示例包装制品有限公司,示例瓦楞纸箱,P100,2026-10-25,2026-10-25,2026-10-24,1000,1000,,只,2400.00,未检,,,,, +5000090015,4500013008,10,示例包装制品有限公司,示例瓦楞纸箱,P100,2026-10-25,2026-10-25,2026-10-24,1000,1000,,只,2400.00,未检,,,,2026-09,否 5000090016,4500013009,10,示例电子材料有限公司,EX-100004,P100,2026-10-26,2026-10-26,2026-10-26,2000,2000,2000,个,2500.00,合格 ,,,,, 5000090017,4500013009,20,示例电子材料有限公司,ex-100004,P100,2026-10-26,2026-10-26,2026-10-26,2000,2000,2000,个,2500.00,合格,,,,, diff --git a/docs/design/03-processes-and-automation.md b/docs/design/03-processes-and-automation.md index bd881cb..23ae4b2 100644 --- a/docs/design/03-processes-and-automation.md +++ b/docs/design/03-processes-and-automation.md @@ -143,7 +143,7 @@ sequenceDiagram | 3 开通门户 | 品类经理、采购主管提出(门户邀请);组织管理员操作(v0.6) | 组织管理员按邀请邮箱在成员页邀请为 `external`,对方接受后运行「开通门户」(见 [04 §3.3](./04-security-and-portal.md#33-门户账号的开通与回收));准入邀请在开通后由供应商提交申请,所以这一步在步骤 1 之前;v0.1–v0.5 跳过 | | 4 补充资料 | 供应商 | 门户内补齐公司资料(门户可改的字段见 [04 §4.2](./04-security-and-portal.md#42-字段级权限fls))、银行账户、证书;品类经理可「退回补充」(申请→`returned`,必须写退回意见 `return_reason`,通知申请联系人,v0.6 U6)。受邀申请由门户用户提交、属主是门户账号,品类经理和采购主管经共享规则「处理准入申请」(`srm_supplier_application` 的编辑规则,关联了供应商的申请)才能受理、退回;`srm_application_submit` 提交后通知发出邀请的人(门户邀请的 `created_by`)。供应商补齐后在申请上点「补充完成,重新提交」(screen flow `srm_application_resubmit`,v0.6):写补充说明 `supplement_note`,申请回到 `doc_review`,通知初审人(没有初审人时通知最后修改申请的人,即退回申请的人);只有申请所属供应商的门户用户能提交(flow 按门户名册检查)。应付会计经「核验银行账户」、SQE 经「核验证书」两个 screen flow 完成核验 | | 5 现场审核 | `risk_class == 'high'` | SQE 建供应商审核记录(`audit_type = admission`,关联本次申请),审核后录入总分、最低审核块得分、结论与问题项(结论按 `audit_score_thresholds` 校验,02 §4.2)。审核完成或改了结论时,flow `srm_audit_admission_apply` 把结论写回申请:`audit_result` 记结论,通过或有条件通过时 `audit_passed = true`,否则为 false,并通知初审人;已批准或已驳回的申请不再写。校验规则 `audit_before_review`:`risk_class == 'high' && audit_passed != true` 时申请不得进入 `admission_review`(FR-204)。申请上的勾选框只由 flow 写,品类经理改不了 | -| 6 准入评审 | 申请→`admission_review` | flow `srm_admission_approval`:第 1 级 `field` 审批人 = `reviewer`(品类经理);第 2 级 `position` = `srm_procurement_director`;`material_class == 'class_a'` 或 `risk_class == 'high'` 时第 2 级改为采购总监、`srm_quality_manager` 并行会签(`per_group`:两个岗位各一人通过,同岗位多人时任一人即可;任一人驳回即驳回)。初审人就是申请的新建人或把申请推进到准入评审的人时,品类经理一级上提给采购总监、与第 2 级合并(标准申请由采购总监审批,A 类或高风险由采购总监 + 质量经理会签),申请上 `review_escalated` 记为 true;不要求另一位品类经理,免得只有一位品类经理的品类卡住(平台没有「不可自审」选项,objectstack#25446;同合同、处置单的做法)。驳回 → 申请回 `doc_review` 并通知 | +| 6 准入评审 | 申请→`admission_review` | flow `srm_admission_approval`:第 1 级 `field` 审批人 = `reviewer`(品类经理);第 2 级 `position` = `srm_procurement_director`;`material_class == 'class_a'` 或 `risk_class == 'high'` 时第 2 级改为采购总监、`srm_quality_manager` 并行会签(`per_group`:两个岗位各一人通过,同岗位多人时任一人即可;任一人驳回即驳回)。初审人就是申请的新建人或把申请推进到准入评审的人时,品类经理一级上提给采购总监、与第 2 级合并(标准申请由采购总监审批,A 类或高风险由采购总监 + 质量经理会签),申请上 `review_escalated` 记为 true;不要求另一位品类经理,免得只有一位品类经理的品类卡住(平台没有「不可自审」选项,objectstack#25446,18.3.0 已提供 `excludeSubmitter`,改用见 hotsrm#96;同合同、处置单的做法)。驳回 → 申请回 `doc_review` 并通知 | | 7 批准后 | 审批通过 | 申请→`approved`,写入 `cycle_days`;供应商 `qualifying → qualified`;按申请品类建 `srm_supplier_category`(申请的 `audit_result` 为有条件通过时为 `trial`,否则 `qualified`,WI-01 6.6.2);通知申请联系人(v0.6 U6)。ERP 建档与 `erp_vendor_code` 回写为预留集成点(06 §1) | | 8 未通过 | 终止 | 申请→`rejected`;供应商 `qualifying → potential`(`reapply` 的旧档回 `eliminated`);v0.6 起 `srm_admission_rejection` 先通知申请联系人「未通过」(U6),再回收门户账号:清掉该供应商全部联系人的 `portal_user`、撤销门户邀请,有门户账号时通知组织管理员撤任命、移出组织(GL-10 6.6.1 c)。只有潜在、淘汰的供应商能申请(`supplier_admissible`),所以驳回总会让供应商退回,已合格供应商不受影响 | @@ -224,7 +224,7 @@ flowchart TD 从第 1 级重新审批(第 2 级退回时账户信息改过,采购要再确认);第 1 级通过时清掉上一轮的财务审批人。 18.0.0 时第 2 级不能退回(平台把第 2 个审批节点的提交人记成第 1 级审批人,objectstack#24817),18.1.0 修复后加回(2026-10-26)。 - **职责分离(BR-11)**:发起人不能审批本人的变更单,两级审批人不能是同一人。平台没有「提交人不得审批」开关 - (objectstack#25446),岗位又可能兼任,所以每级通过后 flow 读审批记录比对,撞人时这次通过不算数,通知审批人 + (objectstack#25446;18.3.0 已提供 `excludeSubmitter`,改用见 hotsrm#96),岗位又可能兼任,所以每级通过后 flow 读审批记录比对,撞人时这次通过不算数,通知审批人 并重新开一轮同级审批。审批人在节点开启时按岗位解析:只有一位采购主管且他就是发起人时,要再任命一位。 ERP 侧的银行账户由财务按 SRM 的审批记录维护(ERP 集成为预留集成点)。 @@ -288,7 +288,7 @@ ERP 侧的银行账户由财务按 SRM 的审批记录维护(ERP 集成为预 - **撤回**:提交人在审批中或退回修改期间撤回,审批结论为 `recalled`,`srm_contract_recalled` 把合同改回 `draft`,改完可以 再次提交。 - **职责分离(BR-11)**:起草人、提交人不能审批本合同,各级审批人不能是同一人;做法同变更单(§5):每级通过后读审批记录 - 比对,撞人时这次通过不算数,通知审批人并重新开一轮同级审批(objectstack#25446)。 + 比对,撞人时这次通过不算数,通知审批人并重新开一轮同级审批(objectstack#25446;18.3.0 已提供 `excludeSubmitter`,改用见 hotsrm#96)。 ## 7. P5 订单协同与 P6 送货收货(预留:ERP 集成后的门户镜像,现阶段不做) @@ -491,7 +491,7 @@ GL-02 附录 A 按本表编写,制度里的岗位称谓经 `governance/org.yam 冻结层级统一为上表,减少一个判断维度。 职责分离:同一张单据的发起人不应出现在审批人中。平台审批服务目前**没有**内置 -「提交人不得审批本人单据」的开关(已提需求 objectstack#25446),本设计用两层保证:岗位 +「提交人不得审批本人单据」的开关(已提需求 objectstack#25446;18.3.0 已提供 `excludeSubmitter`,改用见 hotsrm#96),本设计用两层保证:岗位 设置上,发起岗与审批岗不由同一人兼任;在 flow 中,审批人等于发起人时改用上一级岗位 (`expression` 审批人),并用校验规则拦截提交人 = 审批人。详见 [04 §6](./04-security-and-portal.md#6-职责分离sod)。 diff --git a/docs/design/04-security-and-portal.md b/docs/design/04-security-and-portal.md index 81bb4cd..58bab79 100644 --- a/docs/design/04-security-and-portal.md +++ b/docs/design/04-security-and-portal.md @@ -355,7 +355,7 @@ export const PortalSupplier = definePermissionSet({ | 交叉写 | 用 A 的身份修改 B 的记录、把自己的单据改挂到 B 名下、在 B 名下新建联系人或证书 | 全部拒绝 | | 冒用名册 | 用门户身份(含门户管理员)或任何内部岗位写联系人的 `portal_user`、改联系人归属或在职标志 | 拒绝或写入被丢弃,授权不变;只有 flow 能写 | | 越过回复范围 | 用 A 的身份改本公司整改单的非回复字段(SQE 意见、索赔、问题描述、逾期标志)、把状态改成效果验证 / 关闭 / 撤销或退回、改轮到 SQE 的整改单 | 拒绝或写入被丢弃,值不变(v0.6 U5) | -| 越权 screen flow | 用 A 的身份对 B 的整改单「确认索赔」、对 B 的考评单「申诉」,分别经记录页按钮和直接触发 flow;联系人停用后对本公司考评单申诉 | 按钮答「不存在」,直接触发被 flow 的名册检查拒绝,记录不变;恢复在职后本公司的申诉照常(v0.6 U5) | +| 越权 screen flow | 用 A 的身份对 B 的整改单「确认索赔」、对 B 的考评单「申诉」,分别经记录页按钮和直接触发 flow;联系人停用后对本公司考评单申诉 | 按钮答「不存在」;直接触发 18.3.0 起同样答「不存在」(入口先以启动人身份读记录,objectstack#29050),flow 的名册检查保留作第二道防线,记录不变;恢复在职后本公司的申诉照常(v0.6 U5) | | 开标后改价 | 开标后用中标供应商的身份修改本公司报价明细的单价或中标字段 | 拒绝(v0.7 寻源上线时加入) | | 自解资质异常 | 用门户身份把本公司过期证书的 `status` 改为有效、新建证书时自带「已核验」、写 `verified`;修改有效期或类型 | 改状态、自带核验被拒绝或写入被丢弃;修改内容放行但 `verified` 复位,资质异常在 SQE 核验前不解除(U4) | | 自助写路径 | 用 A 的身份改 B 的证书、联系人、变更单,在 B 名下建银行账户;伪造 `recordId` 启动「提交变更申请」「补充完成,重新提交」;门户身份启动「核验证书」 | 全部拒绝,B 的记录不变(U4) | diff --git a/docs/design/05-experience-and-analytics.md b/docs/design/05-experience-and-analytics.md index 7d211bb..67c9a78 100644 --- a/docs/design/05-experience-and-analytics.md +++ b/docs/design/05-experience-and-analytics.md @@ -162,7 +162,7 @@ ## 5. 通知 -平台的 `notify` 节点只能发给组织内用户(objectstack#25444):v0.6 前供应商侧的通知发给 +平台的 `notify` 节点只能发给组织内用户(objectstack#25444;18.3.0 起可按记录的邮箱字段发,是否用于没开通门户的联系人见 hotsrm#98):v0.6 前供应商侧的通知发给 内部经办人(品类经理 / SQE),由其转告;v0.6 起门户用户是组织成员(组织角色 external),供应商侧的通知直接发给 他们(U6)。收件人规则: diff --git a/docs/design/06-integration-and-ai.md b/docs/design/06-integration-and-ai.md index c865ed0..a240ae4 100644 --- a/docs/design/06-integration-and-ai.md +++ b/docs/design/06-integration-and-ai.md @@ -105,7 +105,7 @@ | 服务 | 用途 | 接入点 | 状态 | |---|---|---|---| -| 邮件 / 短信 | 通知 | 平台通知通道(`notify` 现只能发给组织内用户,objectstack#25444) | v0.1 | +| 邮件 / 短信 | 通知 | 平台通知通道(`notify` 现只能发给组织内用户,objectstack#25444;18.3.0 起可按记录的邮箱字段发,是否用于没开通门户的联系人见 hotsrm#98) | v0.1 | | 企业信息服务(工商、司法、经营异常) | 登记时核对名称与信用代码;每周风险扫描。只适用于中国供应商,境外供应商先人工核验 | `http` 节点,按批调用 | 客户驱动 | | 电子签章 | 合同、质量协议、廉洁协议在线签署 | `http` 发起 + `api` 回调 | 客户驱动 | | 对公账户验证 | 银行账户真实性核验(四要素 / 小额打款) | `http` | 客户驱动 | diff --git a/docs/design/07-delivery-plan.md b/docs/design/07-delivery-plan.md index d86e798..71044bc 100644 --- a/docs/design/07-delivery-plan.md +++ b/docs/design/07-delivery-plan.md @@ -214,7 +214,7 @@ test/ # 仓库根目录 | 字段谓词(`requiredWhen` 等) | ⚠️ | 条件必填 | 不能经引用字段读关联记录(ADR-0137 D2) | 2026-10-23 确认;规范见 docs/engineering §1 | | `controlled_by_parent` 子对象 | ✅ ⚠️ | 证书、合同行、报价明细 | 写子记录要求对父记录有编辑权 → 父记录授编辑权 + 字段级权限全部只读 | 2026-10-23 确认 | | flow:record_change / schedule / time_relative / api / screen | ✅ ⚠️ | 全部自动化 | 定时触发可能延迟或漏跑 → 追赶式扫描(03 §2.3) | 2026-10-23;平台不提供延迟保证(PF1) | -| 审批节点 | ✅ ⚠️ | 全部审批 | 超时按日历小时计算 → 工作日口径的时限存为业务字段,由扫描判断;flow 里的 `record` 是发起时的快照,退回修改后要按改过的值路由时先重新读记录(合同按金额分档) | 2026-10-23 确认;2026-10-26 18.1.0 复核 | +| 审批节点 | ✅ ⚠️ | 全部审批 | 超时按日历小时计算 → 工作日口径的时限存为业务字段,由扫描判断;flow 里的 `record` 是发起时的快照,退回修改后要按改过的值路由时先重新读记录(合同按金额分档) | 2026-10-23 确认;2026-10-26 18.1.0 复核;2026-11-03 18.3.0 复核 | | 公开表单(匿名提交) | ⚠️ | 供应商自助注册 | 不收附件:需要上传的,供应商注册账户、登录后再上传(2026-10-24 决定,PF4 不再是依赖);防刷由前置网关限流保证(04 §8) | 2026-10-23;FR-201 降为 P2 | | 岗位、权限集、OWD、字段级权限 | ✅ | 内部角色、门户 | `role` 为保留字(ADR-0090 D3);`org_external` 为保留岗位名(18.1.0) | 2026-10-23 确认;2026-10-26 18.1.0 复核 | | 组织角色 `external` | ✅ | 门户用户 | 只读自己的身份行,看不到成员目录;不放进业务单元;OWD 对其一律视为仅本人,数据只来自岗位 / 权限集上的授权(含 RLS)和共享(04 §3.3) | 2026-10-26 随 18.1.0 发布(objectstack#25233) | @@ -225,14 +225,14 @@ test/ # 仓库根目录 | 唯一约束 | ✅ | 信用代码、`country` + `registration_no` | 空值安全的组合唯一(ADR-0120) | 2026-10-23 确认 | | 报表(含矩阵) | ✅ | 比价矩阵(v0.7) | — | — | | 仪表盘 | ✅ ⚠️ | 各类看板 | 求和 / 平均只能落在存储数值字段;多值字段不能做维度 | 2026-10-23 确认 | -| 平台导入(表格) | ✅ ⚠️ | 物料、存量供应商、交付记录 | 按外部编号匹配,可重复执行;只在新建时写入的列(如供应商初始状态)用字段默认值和 `readonlyWhen` 表达(PF8);一次导入只能一组匹配字段,境内 / 境外供应商分两遍(PF9);同一批里的跨行规则拦不住(PF10) | 2026-10-23 样例表格在 CI 跑两遍通过;模拟「脏」台账试导结论见[导入说明 §6](../implementation/legacy-supplier-import.md#6-试导结果与清洗清单)(PF11、PF12);2026-10-24 交付记录月度表格两遍通过,「脏」表格试导见[交付记录导入说明 §5](../implementation/delivery-import.md#5-试导结果与清洗清单) | +| 平台导入(表格) | ✅ ⚠️ | 物料、存量供应商、交付记录 | 按外部编号匹配,可重复执行;同一文件里匹配键相同的行一律不写(18.3.0,PF16);只在新建时写入的列(如供应商初始状态)用字段默认值和 `readonlyWhen` 表达(PF8);境内 / 境外供应商分两遍(PF9)。PF8、PF9 的平台能力随 18.3.0 发布,改用见 hotsrm#97 | 2026-10-23 样例表格在 CI 跑两遍通过;模拟「脏」台账试导结论见[导入说明 §6](../implementation/legacy-supplier-import.md#6-试导结果与清洗清单)(PF11、PF12);2026-10-24 交付记录月度表格两遍通过,「脏」表格试导见[交付记录导入说明 §5](../implementation/delivery-import.md#5-试导结果与清洗清单) | | 批量 API `upsert`、`http` 节点、webhook | ✅ ⚠️ | ERP 集成(预留) | 业务字段记同步状态 + 追赶式重推(06 §1.1) | ERP 立项时验证 | -| 通知(`notify`) | ✅ ⚠️ | 全部通知 | 只能发给组织内用户;门户用户是组织成员(external),供应商侧通知经门户账号发站内信和邮件,用中英文邮件模板按收件人语言渲染(v0.6 U6);没开通门户的联系人仍由经办人转告(PF3);邮件里拼不出带部署地址的完整链接(PF20) | 2026-10-23;2026-11-02 U6 实测 | +| 通知(`notify`) | ✅ ⚠️ | 全部通知 | 门户用户是组织成员(external),供应商侧通知经门户账号发站内信和邮件,用中英文邮件模板按收件人语言渲染(v0.6 U6);没开通门户的联系人仍由经办人转告(PF3,18.3.0 起平台能按记录的邮箱字段发,是否采用见 hotsrm#98);邮件里的完整链接 18.3.0 起可用 `{{baseUrl}}` 拼(PF20,hotsrm#98) | 2026-10-23;2026-11-02 U6 实测;2026-11-03 18.3.0 | | 字段历史、审批历史 | ✅ | 审计 | — | 2026-10-23 确认 | | AI 治理(工具调用审计、回答级权限) | ✅(企业版) | AI 用例的审计 | 需企业版许可 | — | | 应用包代码(钩子、插件、函数) | ⚠️ | 不依赖 | 平台支持 QuickJS 沙箱内的事务型钩子(ADR-0034/0102/0119),可被 `OS_DISABLE_AUTHORED_HOOKS` 关闭;本设计按 ADR-0001 不依赖 | 2026-10-23 更正(原文「托管云不执行代码」有误) | | 岗位定义 | ✅ | 内部岗位与两个静态门户岗位 | 岗位是环境级元数据,由管理员新建,flow 不能创建;门户改用成员键后不再需要逐家建岗位(ADR-0010) | 2026-10-23;2026-11-02 | -| 审批人与发起人相同的控制 | ⚠️ | 职责分离 | `expression` 审批人改派 + 岗位分离(04 §6);平台开关主干已修、18.1.0 未带上(PF5) | 2026-10-23 | +| 审批人与发起人相同的控制 | ⚠️ | 职责分离 | `expression` 审批人改派 + 岗位分离(04 §6);平台开关 `excludeSubmitter` 随 18.3.0 发布(PF5),改用见 hotsrm#96 | 2026-10-23;2026-11-03 | ### 6.2 v0.6 前的实测项 @@ -251,45 +251,58 @@ test/ # 仓库根目录 ### 6.3 已提交的平台 issue -「状态」列是 2026-10-26 升级到 18.1.0 时(#76)的核对结果:「18.1.0 已修」的临时做法已撤掉;「主干已修」指修复在 18.1.0 -发版之后合入、要等下一版,临时做法保留,升级时再撤。平台把部分 issue 改由车队账号重新登记,表里写现在的编号,原编号在括号里。 +「状态」列是 2026-11-03 升级到 18.3.0 时的核对结果(上一次是 2026-10-26 升级到 18.1.0,#76):「18.3.0 已修,已撤」的临时 +做法在升级 PR 里撤掉;「18.3.0 已发布,见 hotsrm#…」指平台能力已到、但改用会改变流程或数据,另开 issue 做。平台把部分 issue 改由 +车队账号重新登记,表里写现在的编号,原编号在括号里。 -| # | issue | 影响 | 不修复时的做法 | 状态(2026-10-26,18.1.0) | +| # | issue | 影响 | 不修复时的做法 | 状态(2026-11-03,18.3.0) | |---|---|---|---|---| | PF1 | objectstack#25570(原 #25442)定时 flow 的延迟与漏跑保证 | 提醒、考评、扫描 | 追赶式扫描 | 平台不纳入计划;追赶式扫描为长期做法 | | PF2 | objectstack#25234(原 #25443)运行时创建岗位 → 平台改以声明式成员键回应 | 门户开通 | 管理员手工建岗位 | 18.1.0 已发布 `ObjectSchema.membershipKeys`;U2 实测通过,2026-11-02 改用(ADR-0010) | -| PF3 | objectstack#25571(原 #25444)通知组织外联系人 | 没开通门户的供应商联系人的邮件通知 | 开通门户的联系人是组织成员,直接发(v0.6 U6);没开通的由内部经办人转告 | 主干已修,18.1.0 未带上;门户上线后影响只剩没开通门户的联系人 | -| PF4 | objectstack#25572(原 #25445)公开表单附件 | 不再依赖(2026-10-24 决定公开表单不收附件) | 附件在供应商注册账户、登录后上传 | 主干已修;决定不变 | -| PF5 | objectstack#25573(原 #25446)禁止自审开关 | 职责分离 | `expression` 改派;信息变更单每级通过后读审批记录比对,撞人时重开同级审批(03 §5) | 主干已修,18.1.0 未带上 | +| PF3 | objectstack#25571(原 #25444)通知组织外联系人 | 没开通门户的供应商联系人的邮件通知 | 开通门户的联系人是组织成员,直接发(v0.6 U6);没开通的由内部经办人转告 | 18.3.0 已发布(`notify` 按记录的邮箱字段发);是否给没开通门户的联系人发邮件要先定,见 hotsrm#98 | +| PF4 | objectstack#25572(原 #25445)公开表单附件 | 不再依赖(2026-10-24 决定公开表单不收附件) | 附件在供应商注册账户、登录后上传 | 平台已修;决定不变 | +| PF5 | objectstack#25573(原 #25446)禁止自审开关 | 职责分离 | `expression` 改派;信息变更单每级通过后读审批记录比对,撞人时重开同级审批(03 §5) | 18.3.0 已发布(审批节点 `excludeSubmitter`),改用见 hotsrm#96 | | PF6 | objectstack#25233 门户用户可列出全组织用户 | 门户隔离 | v0.6 阻塞;见 D8 | 18.1.0 已修:组织角色 `external`(04 §3.3);V1 已通过(2026-11-02) | | PF7 | objectstack#25574(原 #25447)ADR-0138 D5(多组织环境下公开表单绑定到指定组织) | 多法人客户的自助注册 | 邀请注册;多法人按 ADR-0006 单组织时不受影响 | 平台不纳入计划 | -| PF8 | objectstack#25577(原 #25475)导入时「只在新建时写入」的列 | 存量导入不回退状态 | `status` 默认值按 `legacy_imported` 计算、`legacy_imported` 用 `readonlyWhen` 锁定(02 §4.1) | 主干已修,18.1.0 未带上 | -| PF9 | objectstack#25578(原 #25476)导入按多组匹配字段依次匹配 | 境内 / 境外供应商同表导入 | 同一张表导两遍,各自跳过匹配字段为空的行 | 主干已修,18.1.0 未带上 | -| PF10 | objectstack#25579(原 #25477)条件唯一(每家至多一位主联系人)在批量写入中生效 | 联系人导入 | 汇总字段 + 校验规则守卫单条写入;「主联系人重复」视图兜底同批写入 | 未修 | -| PF11 | objectstack#25491 导入映射的 `transform`(转大写 / 小写)被忽略 | 信用代码、邮箱归一 | 导入前清洗([导入说明 §6.2](../implementation/legacy-supplier-import.md#62-清洗清单)) | 主干已修,18.1.0 未带上 | -| PF12 | objectstack#25582(原 #25493)导入接受 `YYYY.MM.DD` / `YYYYMMDD`、选项别名与大小写、每行报全部错误 | 存量台账导入 | 导入前清洗 | 部分:18.1.0 已接受 `YYYY.MM.DD` / `YYYYMMDD` 和不分大小写的选项值(`CN`、`ISO9001`、`Y`),清洗清单已改;选项别名、每行报全部错误未修 | +| PF8 | objectstack#25577(原 #25475)导入时「只在新建时写入」的列 | 存量导入不回退状态 | `status` 默认值按 `legacy_imported` 计算、`legacy_imported` 用 `readonlyWhen` 锁定(02 §4.1) | 18.3.0 已发布(导入列 `applyOn: 'create'`),改用见 hotsrm#97 | +| PF9 | objectstack#25578(原 #25476)导入按多组匹配字段依次匹配 | 境内 / 境外供应商同表导入 | 同一张表导两遍,各自跳过匹配字段为空的行 | 18.3.0 已发布(`matchFields` 按顺序的多组匹配键),改用见 hotsrm#97 | +| PF10 | objectstack#25579(原 #25477)条件唯一(每家至多一位主联系人)在批量写入中生效 | 联系人导入 | 汇总字段 + 校验规则守卫单条写入;「主联系人重复」视图兜底同批写入 | 18.3.0 已修:同一批里先写入的行计入汇总,第二位主联系人被校验规则拒绝;「主联系人重复」视图保留作兜底 | +| PF11 | objectstack#25491 导入映射的 `transform`(转大写 / 小写)被忽略 | 信用代码、邮箱归一 | 导入前清洗([导入说明 §6.2](../implementation/legacy-supplier-import.md#62-清洗清单)) | 18.3.0 已修(映射的 `transform` 生效);导入说明改用见 hotsrm#97 | +| PF12 | objectstack#25582(原 #25493)导入接受 `YYYY.MM.DD` / `YYYYMMDD`、选项别名与大小写、每行报全部错误 | 存量台账导入 | 导入前清洗 | 部分:18.1.0 已接受 `YYYY.MM.DD` / `YYYYMMDD` 和不分大小写的选项值;18.3.0 起失败行在 `errors` 里报全部错误;选项别名未修 | | PF13 | objectstack#24817 多级审批的后续节点以原发起人为提交人 | 信息变更单、合同、处置单第 2 级起退回修改 | 第 2 级起只能通过或驳回 | 18.1.0 已修;各级加回退回修改,重新提交后从第一级重审(03 §5、§12) | -| PF14 | objectstack#25683 i18n 检查不把字段的 `inlineHelpText` 当作默认语言词条 | 带帮助文字的字段(绩效指标的满分值、零分值) | zh-CN 翻译包只放这两条帮助文字,引用源码同一份常量;修复后删除 | 主干已修,18.1.0 未带上 | +| PF14 | objectstack#25683 i18n 检查不把字段的 `inlineHelpText` 当作默认语言词条 | 带帮助文字的字段(绩效指标的满分值、零分值) | zh-CN 翻译包只放这两条帮助文字,引用源码同一份常量;修复后删除 | 18.3.0 已修,已撤(zh-CN 翻译包删除,帮助文字写回字段) | | PF15 | objectstack#14500 flow 没有汇总能力:CEL 不能对列表求和,`get_record` 只返回行(已关的「汇总 / 分组节点」卡,hotsrm 的用法作为数据点记在评论里) | 考评生成的 Σ不良数、Σ实收数、启用指标权重合计 | `loop` + `assignment` 逐行累加;200 家 × 30 行约 8.6 秒,性能可接受 | 平台不纳入计划 | -| PF16 | objectstack#26133 导入 upsert 时同一文件里匹配键相同的两行都新建,之后再导报 `AMBIGUOUS_MATCH` | 证书等没有唯一约束的对象重复建档 | 清洗时同一张证书只留一行([导入说明 §6.2](../implementation/legacy-supplier-import.md#62-清洗清单)) | 2026-10-26 新提 | +| PF16 | objectstack#26133 导入 upsert 时同一文件里匹配键相同的两行都新建,之后再导报 `AMBIGUOUS_MATCH` | 证书等没有唯一约束的对象重复建档 | 清洗时同一张证书只留一行([导入说明 §6.2](../implementation/legacy-supplier-import.md#62-清洗清单)) | 18.3.0 已修:同一文件里匹配键相同的行都不写,报 `DUPLICATE_MATCH_KEY`;清洗清单照旧要求一张证书一行 | | PF17 | objectstack#25251 自动化不得以系统身份写岗位任命、权限集授予、邀请和成员(裁定 B);邀请只经组织接口(V2、V3) | 门户开通、回收 | 组织管理员在成员页邀请、移出组织;任命与门户管理员授权由 `runAs: 'user'` 的「开通门户」flow 在组织管理员操作时写;处置 flow 不撤任命,改为通知组织管理员(04 §3.3) | 平台裁定,长期做法(2026-11-02 实测登记,不另提 issue) | -| PF18 | objectstack#29050 直接触发 flow(REST trigger)不按启动人的权限读 `recordId`,按钮入口(#16370)会答「不存在」 | 门户能启动的系统身份 screen flow(确认索赔、申诉、提交变更申请、准入补充后重新提交) | 每条 flow 进门自己查名册(`portal_user` = 本人、`active == true`,与成员键同条件),元数据测试守住(04 §3.7) | 2026-11-02 新提 | +| PF18 | objectstack#29050 直接触发 flow(REST trigger)不按启动人的权限读 `recordId`,按钮入口(#16370)会答「不存在」 | 门户能启动的系统身份 screen flow(确认索赔、申诉、提交变更申请、准入补充后重新提交) | 每条 flow 进门自己查名册(`portal_user` = 本人、`active == true`,与成员键同条件),元数据测试守住(04 §3.7) | 18.3.0 已修:所有按名字启动 flow 的入口先以启动人身份读 `recordId`,读不到答 404。flow 内的名册检查保留作第二道防线(还要拦读得到记录的内部员工),测试改为断言入口拒绝 | | PF19 | 平台既定行为(objectstack#3407 起):数据层剔除对只读字段的写入,flow 节点只在步骤结果里给警告(`droppedFields`),写入照样算成功;`runAs: 'user'` 的 flow 因此写不进字段级只读的 `portal_user` | 门户开通(名册写入) | 「开通门户」以 `runAs: 'user'` 任命门户岗位,名册由系统身份的子 flow `srm_portal_roster_write` 写;平台拒绝直接启动系统身份的 autolaunched flow,子 flow 只经开通 flow 进入(04 §3.3);`test/portal-invitation.test.ts` 的探针盯着平台行为 | 平台既定行为,长期做法(2026-11-02 U3 实测,总监裁定不另提 issue) | -| PF20 | objectstack#29095 邮件模板的渲染上下文只有 `appName`,没有部署地址(`OS_BASE_URL`),flow 发出的邮件拼不出能点开的完整链接 | 供应商侧通知邮件(05 §5) | 邮件正文写「登录门户查看」和单号,站内信带单据链接;模板数据给出相对路径 `path`,客户可在模板覆盖层写上自己的门户域名 | 2026-11-02 U6 新提 | -| PF21 | objectstack#29171(功能缺口,待维护者裁定)引用的目标记录对当前用户不可读时,lookup 只显示「受限记录」(18.1.0 显示原始 ID,objectui#13335 已改),不显示名称 | 门户页面上指向品类、物料、内部员工的关联字段(准入申请的品类、整改单的物料和负责 SQE) | 引用对象上加 flow 写入的只读文本快照字段(`category_names`、`material_name`、`sqe_name` 等),门户只读快照(hotsrm#95,裁定线程 2026-11-02 选 B);平台做了以后快照字段保留不删 | 2026-11-02 出口验收新提 | +| PF20 | objectstack#29095 邮件模板的渲染上下文只有 `appName`,没有部署地址(`OS_BASE_URL`),flow 发出的邮件拼不出能点开的完整链接 | 供应商侧通知邮件(05 §5) | 邮件正文写「登录门户查看」和单号,站内信带单据链接;模板数据给出相对路径 `path`,客户可在模板覆盖层写上自己的门户域名 | 18.3.0 已发布(模板变量 `{{baseUrl}}`),改用见 hotsrm#98 | +| PF21 | objectstack#29171(功能缺口,待维护者裁定)引用的目标记录对当前用户不可读时,lookup 只显示「受限记录」(18.1.0 显示原始 ID,objectui#13335 已改),不显示名称 | 门户页面上指向品类、物料、内部员工的关联字段(准入申请的品类、整改单的物料和负责 SQE) | 引用对象上加 flow 写入的只读文本快照字段(`category_names`、`material_name`、`sqe_name` 等),门户只读快照(hotsrm#95,裁定线程 2026-11-02 选 B);平台做了以后快照字段保留不删 | 18.3.0 已交付(lookup / user 字段的 `referenceNameVisible`);裁定线程 2026-11-03 改判:hotsrm#95 改用它,不建快照字段 | -其他代码和文档里指向平台的临时做法,2026-10-26 核对结果: +其他代码和文档里指向平台的临时做法,2026-11-03 核对结果: -| issue | 临时做法 | 状态(18.1.0) | +| issue | 临时做法 | 状态(18.3.0) | |---|---|---| -| objectstack#25584 → objectui#13436 新建表单按默认值套用 `readonlyWhen` | 处置单、银行账户、变更单的 `readonlyWhen` 先判断记录已保存(有 `id`) | 主干已修,18.1.0 的控制台未带上;保留 | +| objectstack#25584 → objectui#13436 新建表单按默认值套用 `readonlyWhen` | 处置单、银行账户、变更单的 `readonlyWhen` 先判断记录已保存(有 `id`) | 18.3.0 已修,已撤:银行账户、变更单的条件去掉 `id` 判断;处置单的「只在新建时填写」本义就是「已保存」,保留 | | objectstack#25470 → #25575 「可改不可删」 | 按岗位授 `edit` 的共享规则 | 平台裁定的长期写法,不是临时做法;保留 | -| objectstack#25652 岗位、权限集标签没有翻译词条 | 岗位、权限集标签只有中文 | 主干已修,18.1.0 的翻译包还没有 `positions` / `permissions`;保留 | -| objectstack#25672 非管理员在人员选择框里输入文字搜索报 403 | 不输入,直接从下拉列表点选([本地运行说明](../implementation/local-demo.md)) | 主干已修,18.1.0 实测仍 403;保留 | -| objectstack#25675 应用包里的 `api` flow 只能写死密钥 | 逾期扫描只用定时触发,不加 api 心跳 | 主干已修(`config.secretRef`),18.1.0 未带上;保留 | +| objectstack#25652 岗位、权限集标签没有翻译词条 | 岗位、权限集标签只有中文 | 18.3.0 已修,已撤:翻译包补上 `positions` / `permissions` 英文 | +| objectstack#25672 非管理员在人员选择框里输入文字搜索报 403 | 不输入,直接从下拉列表点选([本地运行说明](../implementation/local-demo.md));门户隐藏 `risk_level` 会让门户的供应商搜索整体被拒,先放开读 | 18.3.0 已修,已撤:门户重新隐藏 `risk_level`,搜索照常(`test/portal-isolation.test.ts`);品类经理、采购员按文字搜索人员返回 200(2026-11-03 本地复测),本地运行说明里的提示已删 | +| objectstack#25675 应用包里的 `api` flow 只能写死密钥 | 逾期扫描只用定时触发,不加 api 心跳 | 18.3.0 已发布(`config.secretRef`),加心跳见 hotsrm#98 | | objectstack#25681 repeater 不能声明子字段 | 审核检查表、8D `steps` 不用 repeater | 平台裁定:对象上的 repeater / composite 就是无类型 JSON,有类型的列表用子对象;8D 不设分步记录、不增对象(2026-10-26 裁定,[02 §4.6](./02-domain-model.md#46-质量v04)) | -| objectui#13470 报表的内联多语言标签让报表页白屏 | 交付报表标签只写中文 | 主干已修,18.1.0 的控制台未带上;保留 | -| objectui#13471 日历把 lookup 标题渲染成「[object Object]」 | 审核日历用审核编号作标题 | 主干已修,18.1.0 的控制台未带上;保留 | +| objectui#13470 报表的内联多语言标签让报表页白屏 | 交付报表标签只写中文 | 18.3.0 已修,已撤:报表进翻译包(`reports` 组,objectstack#25788),英文写在翻译包里 | +| objectui#13471 日历把 lookup 标题渲染成「[object Object]」 | 审核日历用审核编号作标题 | 18.3.0 已修,已撤:标题改回供应商 | + +18.3.0 带来的行为变化(2026-11-03 升级时发现,已按平台的新规则改): + +- `modifyAllRecords` 压过同一权限集上的字段级只读(lint 规则 `security-superuser-field-mask-inert`)。系统管理员删考评单原来靠它, + 升级后管理员能改分数;改为 `allowEdit` + `allowDelete` 加两条只对考评单的行级策略(删除、编辑),字段级权限全部只读照旧生效。 + 考评单由系统生成、没有所有人,删除不能靠所有权或写深度。 +- 记录上的文件字段只能引用调用人自己打开得了的文件(objectstack#25692):上传人、文件挂着的记录可读,或系统身份写入。界面上传 + 不受影响;测试里造文件时要写上传人(`test/support/files.ts`)。 +- 以系统身份运行的 flow 从 trigger 入口带 `recordId` 启动时必须同时给 `objectName`(PF18 的修复),否则 400;测试改走 + `test/support/flows.ts`。 +- 同一文件里匹配键相同的行都不写(PF16):两份试导样例的结论随之改变,见[导入说明 §6](../implementation/legacy-supplier-import.md#6-试导结果与清洗清单) + 和[交付记录导入说明 §5](../implementation/delivery-import.md#5-试导结果与清洗清单)。 --- diff --git a/docs/implementation/delivery-import.md b/docs/implementation/delivery-import.md index ccf8f34..bfcaf76 100644 --- a/docs/implementation/delivery-import.md +++ b/docs/implementation/delivery-import.md @@ -65,18 +65,18 @@ ERP 里事后更正了检验结论或数量,把那个月重新导出再导一 ### 5.1 试导 2026-10-24 用一份模拟的「脏」月度表格试导([`data/samples/delivery-trial/`](../../data/samples/delivery-trial/srm_delivery_record.csv), -18 行,全部虚构),逐行结果由 [test/delivery-record.test.ts](../../test/delivery-record.test.ts) 钉住;2026-10-26 升级到平台 18.1.0 后复测。结论: +18 行,全部虚构),逐行结果由 [test/delivery-record.test.ts](../../test/delivery-record.test.ts) 钉住;2026-10-26 升级到平台 18.1.0、2026-11-03 升级到 18.3.0 后复测。结论: | # | 表格里的问题 | 平台的结果 | 判断 | |---|---|---|---| | D1 | 日期写成 `2026.10.08`、`20261008` | 18.1.0 起按日期导入(此前整行 `invalid_date`) | 正确;不再需要清洗 | | D2 | 供应商写简称、工厂写「一厂」、物料号不存在 | `reference_not_found` | 正确拦截 | -| D3 | 物料号小写 `ex-100004` | `reference_not_found`(查找区分大小写) | 平台限制,同 objectstack#25491;先清洗 | +| D3 | 物料号小写 `ex-100004` | `reference_not_found`(查找区分大小写) | 先清洗;18.3.0 起可用映射的「转大写」(objectstack#25491,hotsrm#97) | | D4 | 物料列写的是物料名称 | 按名称找到并导入 | 正确(名称重复时报 ambiguous) | | D5 | 检验结论写 `OK` | `invalid_option` | 选项没有别名,objectstack#25582;先清洗。带尾随空格的「合格 」能导入 | | D6 | 不合格没写严重度、不良数;让步接收没写不良数 | `required` | 正确拦截 | | D7 | 不良数大于实收数 | 校验规则报错 | 正确拦截 | -| D8 | 同一张表里重复的行 | 第二行 `UNIQUE_VIOLATION`,第一行正常导入 | 正确拦截 | +| D8 | 同一张表里重复的行 | 18.3.0 起两行都 `DUPLICATE_MATCH_KEY`、都不写(此前第二行 `UNIQUE_VIOLATION`、第一行导入) | 正确拦截;去掉重复行后重导 | | D9 | 没有实到日期、没有收货单号 | `required` | 正确拦截 | | D10 | 退货行(负数量、负金额) | `min_value` | 正确拦截;退货不在交付记录里,导出时排除 | | D11 | 数量带千分位 `1,200` | 按 1200 导入 | 正确 | diff --git a/docs/implementation/legacy-supplier-import.md b/docs/implementation/legacy-supplier-import.md index 6ef1d38..0136e67 100644 --- a/docs/implementation/legacy-supplier-import.md +++ b/docs/implementation/legacy-supplier-import.md @@ -19,7 +19,7 @@ 供应商表导两遍:第一遍按信用代码匹配境内供应商,境外行信用代码为空被跳过;第二遍按 「国家 / 地区 + 注册号」匹配境外供应商,境内行注册号为空被跳过([ADR-0007](../adr/0007-china-first-with-overseas-seams.md))。 -平台一次导入只能用一组匹配字段,已提需求 objectstack#25476;支持后改为一遍。 +18.3.0 起平台支持按顺序的多组匹配字段(objectstack#25578),改为一遍见 hotsrm#97。 每一步先点「试运行」看预览,没有错误行再正式导入。导入可以撤销。导入前先按 §6.2 清洗表格; 「空值」设置里加上「长期」「无」「/」,台账里写「长期」的有效期按没有到期日导入。 @@ -64,9 +64,8 @@ 品类经理在 **供应商 → 存量待复核** 视图里逐家核对档案、联系人和证书,补传证书扫描件, 确认后点 **确认复核**,供应商移出该视图。 -**主联系人重复** 视图列出主联系人多于一位的供应商。单行导入时校验规则会拒绝第二位主联系人, -但同一张表里同一家供应商写了两位「是」时规则拦不住(平台限制,objectstack#25477), -在这里把多余的改为「否」。 +**主联系人重复** 视图列出主联系人多于一位的供应商。校验规则拒绝第二位主联系人,18.3.0 起同一张表里 +同一家写了两位「是」时第二行也被拒(objectstack#25579),所以这个视图正常是空的,留作兜底;有数据时把多余的改为「否」。 ## 5. 常见错误 @@ -88,27 +87,29 @@ 2026-10-23 用一份模拟的「脏」台账试导([`data/samples/legacy-trial/`](../../data/samples/legacy-trial/srm_supplier.csv), 13 家供应商、12 位联系人、11 张证书,全部虚构),逐行结果由 -[test/import-trial.test.ts](../../test/import-trial.test.ts) 钉住;2026-10-26 升级到平台 18.1.0 后复测,变化写在「判断」列。结论: +[test/import-trial.test.ts](../../test/import-trial.test.ts) 钉住;2026-10-26 升级到平台 18.1.0、2026-11-03 升级到 18.3.0 后复测, +变化写在「判断」列。18.3.0 起重复行两行都不写(T1),所以试导先按原样导一次供应商,再按 §6.2 第 5 条去掉重复行重导, +联系人和证书在清洗后的供应商上导入。结论: | # | 台账里的问题 | 平台的结果 | 判断 | |---|---|---|---| -| T1 | 同一家供应商写了两行(名称带尾随空格、境外重复行) | 第二行 `UNIQUE_VIOLATION`,第一行正常建档 | 正确拦截;报错文字不说是哪家重复,按行号对照 | +| T1 | 同一家供应商写了两行(名称带尾随空格、境外重复行) | 18.3.0 起两行都 `DUPLICATE_MATCH_KEY`、都不写,报错写明是哪几行(此前第二行 `UNIQUE_VIOLATION`、第一行建档) | 正确拦截;去掉重复行重导(§6.2 第 5 条) | | T2 | 日期写成 `2011.07.01`、`20090615` | 18.1.0 起按日期导入(此前整行 `invalid_date`) | 正确;不再需要清洗 | | T3 | 不存在的日期 `2018-02-30` | `invalid_date` | 正确 | | T4 | 国家写「中国大陆」「CN」,类型写「物流」,资质写「ISO9001」「ISO 9001:2015」,存量导入写「Y」 | 18.1.0 起选项值不分大小写也能认(`CN`、`ISO9001`、`Y` 导入);「中国大陆」「物流」「ISO 9001:2015」仍 `invalid_option` | 选项没有别名,objectstack#25582;别名先清洗 | -| T5 | 信用代码小写、带连字符、17 位 | 校验规则 / 长度报错 | 正确拦截;映射里的「转大写」被平台忽略(objectstack#25491),只能先清洗 | -| T6 | 境内供应商缺信用代码、境外缺注册号 | 两遍都 `BLANK_MATCH_KEY` 跳过,**不报错** | 平台限制,objectstack#25476;导入后核对数量(§6.2 第 6 条) | -| T7 | 一行有多处错误 | 只报第一处,改完再导才看到下一处 | 平台限制,objectstack#25582;清洗时按 §6.2 一次查全 | +| T5 | 信用代码小写、带连字符、17 位 | 校验规则 / 长度报错 | 正确拦截;18.3.0 起映射里的「转大写」生效(objectstack#25491),改用见 hotsrm#97,在此之前先清洗 | +| T6 | 境内供应商缺信用代码、境外缺注册号 | 两遍都 `BLANK_MATCH_KEY` 跳过,**不报错** | 导入后核对数量(§6.2 第 6 条);18.3.0 起可一遍导入(objectstack#25578,hotsrm#97) | +| T7 | 一行有多处错误 | 18.3.0 起失败行的 `errors` 列出全部错误(此前只报第一处) | 正确(objectstack#25582) | | T8 | 有效期写「长期」 | 加空值设置后导入为没有到期日,扫描永不置为过期 | 正确 | -| T9 | 同一家两位主联系人在同一张表 | 两位都导入 | 「主联系人重复」视图兜底,objectstack#25477 | -| T10 | 同一人的邮箱大小写不同写了两行 | 建成两个联系人(匹配区分大小写) | 平台限制,objectstack#25491;清洗时统一小写 | +| T9 | 同一家两位主联系人在同一张表 | 18.3.0 起第二位 `rule_violation`(此前两位都导入) | 正确拦截(objectstack#25579);「主联系人重复」视图留作兜底 | +| T10 | 同一人的邮箱大小写不同写了两行 | 建成两个联系人(匹配区分大小写),18.3.0 起第二位作为第二主联系人被拒 | 清洗时统一小写;18.3.0 起可用映射的「转小写」(objectstack#25491,hotsrm#97) | | T11 | 一个单元格写两个邮箱、联系人没有邮箱 | `invalid_email` / `required` | 正确拦截 | | T12 | 联系人、证书表里的供应商全称与供应商表不一致(括号全半角、少了地名) | `reference_not_found` | 正确拦截;供应商行导入失败时,它的联系人和证书也全部失败 | | T13 | 「道路运输经营许可证」等资质清单外的证书 | `invalid_option` | 选「其他」,证书编号照填 | | T14 | 有效期至早于生效日期、存量证书缺编号 | 校验规则报错 | 正确拦截 | -| T15 | 同一张证书写了两行(「ISO 9001」「ISO9001」) | 18.1.0 起两行都能认出资质类型,**两行都新建**,之后再导这张证书报 `AMBIGUOUS_MATCH` | 平台缺陷,objectstack#26133;清洗时同一张证书只留一行(§6.2 第 5 条),导入后删掉重复的一条 | +| T15 | 同一张证书写了两行(「ISO 9001」「ISO9001」) | 18.3.0 起两行都 `DUPLICATE_MATCH_KEY`、都不写(18.1.0 时两行都新建,objectstack#26133) | 正确拦截;清洗时同一张证书只留一行(§6.2 第 5 条) | -除 T15 外,没有发现导入把错误数据写进系统的情况:其余问题行要么被拒、要么被跳过。T6、T9、T10、T15 是需要人工核对的四处。 +没有发现导入把错误数据写进系统的情况:问题行要么被拒、要么被跳过。T6(被跳过、不报错)是需要人工核对的一处。 ### 6.2 清洗清单 @@ -120,7 +121,7 @@ 3. **信用代码**转大写、去掉空格和连字符,检查是 18 位;境外供应商把信用代码列留空、填注册号。 4. **邮箱**统一小写,一格一个;没有邮箱的联系人先不导。 5. **供应商全称**在三张表里完全一致(括号统一用全角),同一家只留一行;同一张证书(同一供应商、资质类型、证书编号) - 也只留一行,否则会建出两条(objectstack#26133)。 + 也只留一行,否则重复的几行都不写(18.3.0 起报 `DUPLICATE_MATCH_KEY`)。 6. 导入后核对:供应商表的行数 = 两遍报告里「新建 + 更新」之和 + 报错行数;差额就是两遍都被跳过的行 (缺信用代码或注册号),补齐后重导。 -7. 每家只保留一位主联系人;导入后看一眼「主联系人重复」视图。 +7. 每家只保留一位主联系人(第二位会被拒);导入后看一眼「主联系人重复」视图。 diff --git a/docs/implementation/local-demo.md b/docs/implementation/local-demo.md index 98ce46e..9bf7f77 100644 --- a/docs/implementation/local-demo.md +++ b/docs/implementation/local-demo.md @@ -83,5 +83,4 @@ Windows PowerShell:`$env:HOTSRM_DEMO_PASSWORD="<自定密码>"; pnpm setup:loc | 现象 | 处理 | 跟踪 | |---|---|---| -| 非管理员在人员选择框里输入文字搜索会报错 | 不输入,直接从下拉列表点选 | objectstack#25672 | | 审批通知标题是英文模板 “Approval requested / A decision on …” | 已知,验收时忽略 | objectstack#23978 | diff --git a/objectstack.manifest.json b/objectstack.manifest.json index 975f6ab..fbaac33 100644 --- a/objectstack.manifest.json +++ b/objectstack.manifest.json @@ -1,9 +1,9 @@ { "$schema": "https://schemas.objectstack.dev/template-manifest.json", "name": "hotsrm", - "specVersion": "^18.1.0", + "specVersion": "^18.3.0", "engines": { - "protocol": "^18.1.0" + "protocol": "^18.3.0" }, "manifestId": "app.objectstack.hotsrm", "displayName": "hotsrm", diff --git a/package.json b/package.json index b14a136..976719d 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,13 @@ "verify": "pnpm validate && pnpm lint && pnpm typecheck && pnpm i18n:check && pnpm test && pnpm build" }, "dependencies": { - "@objectstack/runtime": "18.1.0", - "@objectstack/spec": "18.1.0" + "@objectstack/runtime": "18.3.0", + "@objectstack/spec": "18.3.0" }, "devDependencies": { - "@objectstack/cli": "18.1.0", - "@objectstack/skills": "18.1.0", - "@objectstack/verify": "18.1.0", + "@objectstack/cli": "18.3.0", + "@objectstack/skills": "18.3.0", + "@objectstack/verify": "18.3.0", "typescript": "^5.3.0", "vitest": "~4.0.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 790728e..2c46a9f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,35 +5,35 @@ settings: excludeLinksFromLockfile: false overrides: - '@objectstack/core': 18.1.0 - '@objectstack/driver-memory': 18.1.0 - '@objectstack/driver-mongodb': 18.1.0 - '@objectstack/driver-sql': 18.1.0 - '@objectstack/driver-sqlite-wasm': 18.1.0 - '@objectstack/objectql': 18.1.0 - '@objectstack/observability': 18.1.0 - '@objectstack/runtime': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/driver-memory': 18.3.0 + '@objectstack/driver-mongodb': 18.3.0 + '@objectstack/driver-sql': 18.3.0 + '@objectstack/driver-sqlite-wasm': 18.3.0 + '@objectstack/objectql': 18.3.0 + '@objectstack/observability': 18.3.0 + '@objectstack/runtime': 18.3.0 importers: .: dependencies: '@objectstack/runtime': - specifier: 18.1.0 - version: 18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + specifier: 18.3.0 + version: 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) '@objectstack/spec': - specifier: 18.1.0 - version: 18.1.0 + specifier: 18.3.0 + version: 18.3.0 devDependencies: '@objectstack/cli': - specifier: 18.1.0 - version: 18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + specifier: 18.3.0 + version: 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) '@objectstack/skills': - specifier: 18.1.0 - version: 18.1.0 + specifier: 18.3.0 + version: 18.3.0 '@objectstack/verify': - specifier: 18.1.0 - version: 18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@objectstack/driver-mongodb@18.1.0)(@objectstack/driver-sqlite-wasm@18.1.0)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + specifier: 18.3.0 + version: 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@objectstack/driver-mongodb@18.3.0)(@objectstack/driver-sqlite-wasm@18.3.0)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) typescript: specifier: ^5.3.0 version: 5.9.3 @@ -316,6 +316,9 @@ packages: peerDependencies: hono: ^4 || ^5.0.0-0 + '@isaacs/nonrecursive-glob-match@1.0.3': + resolution: {integrity: sha512-SI7kyKHq4XXDRozw+r9GlRZkia1ZkFrpLW98r2IC2Bpr8YkFGE3fvjzmxCG0x78A++69HmDNrDvSMYBZmHGpKA==} + '@jitl/quickjs-ffi-types@0.32.0': resolution: {integrity: sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg==} @@ -379,48 +382,50 @@ packages: '@nodable/entities@3.1.0': resolution: {integrity: sha512-LsS/DjHr+uDM647Gru/cA8+J3a3HfhttwCKLyuoyN7yXTFCxKBtSgMQBvrW9yNPa2/zDRUNuGPaH5QUFoa4arQ==} - '@objectstack/account@18.1.0': - resolution: {integrity: sha512-WNMglqHxcdu9Ht2kF9IEOcQ8LaYBE9p5uwyl9a+9fnZ7GxQqv0h7sYTsBGBek/1rEYVFJiC9JBOeGJyJQJK3Ow==} + '@objectstack/account@18.3.0': + resolution: {integrity: sha512-dwVJs8dTYbAJJi3FgG7itxyiNzHhRbqvt9uj2HBHdqo0kA4fFZ3uddQq38GPL6/xp3vmC7pH/uyddB9fcg1iNg==} engines: {node: '>=22.0.0'} - '@objectstack/cli@18.1.0': - resolution: {integrity: sha512-CaWt2tDYJTKR5RvMvWfpeVe5aFEQHXkDts/wV6bZmsR6veh2byKiKjubGRJkepvbj/FnjlgsHEuMf6zIW+sLdQ==} + '@objectstack/cli@18.3.0': + resolution: {integrity: sha512-Ew4iqbvkGI/9/mUrc0Ke4K0KTi1hSItPpG0R3WmWapBVL0YReYKXpWY5SmPXnrRB2I2BTtYE1N5H6PcVKibhZg==} engines: {node: '>=22.0.0'} hasBin: true peerDependencies: - '@objectstack/driver-turso': ^18.1.0 + '@objectstack/driver-turso': ^18.3.0 peerDependenciesMeta: '@objectstack/driver-turso': optional: true - '@objectstack/client@18.1.0': - resolution: {integrity: sha512-6Ih4vPl03Edr6CZkBKCdqS+6rgGwBuj3MZFr+yAh8RlS42YirjsR737+EucgQrHqHAPKYCUSZLNvA8xm+ADyag==} + '@objectstack/client@18.3.0': + resolution: {integrity: sha512-0g52Ou+eOYqHndJeZfn1e7Qa2ARLBp4GwlpJimEoi+t360IAWVmvnqXQd0uBlI3+rTm05MUBI19kCFa2F+wDNQ==} engines: {node: '>=22.0.0'} - '@objectstack/cloud-connection@18.1.0': - resolution: {integrity: sha512-OmoTLKjN1J2G3TrD2+U4kijjnHr7l+9oEHTH0RL9ncLuFZAE+e9OvJQY+2AfLBOah1Ykf56GcUkFVgiJxsjCnQ==} + '@objectstack/cloud-connection@18.3.0': + resolution: {integrity: sha512-bFKTomkCLuhW30Eyhph06fMgLHfTLEBVv4vDlyanDXOni93fY0PoytPrvqXxMtDZ6M9qX+r9K+MyZn/fYy1X2A==} engines: {node: '>=22.0.0'} - '@objectstack/connector-imap@18.1.0': - resolution: {integrity: sha512-PYgITDeSecgnHCMLSQ2zLy6rSsO2wjqz11sy33601m+uxhLiuYrEi18XikT9XcYzz7EB1xizz8SwVDcFjjRhKQ==} + '@objectstack/connector-imap@18.3.0': + resolution: {integrity: sha512-iNTLrXBOgdwgj580AJjRNBtQz8TyQ6wvEbE3KWY5UYG+ncFBjdW3yDxWRkD/g6iHT9Z3gAw5PEdcunlaflq9sw==} + engines: {node: '>=22.0.0'} - '@objectstack/console@18.1.0': - resolution: {integrity: sha512-LubT9vt6tAQmYykaqbh2dTyuEgWlXowpY+Jecm3yO1bgtzy2hlIByNMQmvJ/O2XlvVUzq2ArqPObFsHeaQdSeQ==} + '@objectstack/console@18.3.0': + resolution: {integrity: sha512-Bdjc4DrW3hf2cLixGrnJQAYMWnpdgJR7Ba4aTjl4Jv5GIe98JiYd0kXLHk97TjWZPW7hhe39RCnqIF3RRD9VPw==} + engines: {node: '>=22.0.0'} - '@objectstack/core@18.1.0': - resolution: {integrity: sha512-VeKMMDB3ZMowtDRrfzUjLKdo/biErfLn13Ewh/I+HJVETs7eVrcOyyN9TjXkohf8SQdDhYny1QrxaiUBUxi16w==} + '@objectstack/core@18.3.0': + resolution: {integrity: sha512-Fi03+dEpM5+uHrmjkQhDNJHP2pePAWrARtI4myWmGsoYOc47xOAzGzX6qc+V9vT5QeFVykUIz/6TihWzNvU50Q==} engines: {node: '>=22.0.0'} - '@objectstack/driver-memory@18.1.0': - resolution: {integrity: sha512-TdLzG7+iBmmfIY2Z/jd9LbKOeqzxAhFDBlOaPUcS3PGkg/y7GwO17HuQDqPC5RcIo4QVSxv/9TCXhMBWWygBCQ==} + '@objectstack/driver-memory@18.3.0': + resolution: {integrity: sha512-5K6KShBwPC3NveJul4JZP768h5sxyRBzGlAnMO6PB/uqZLbdP0zo6Jop+erelQrTRrzVHfC85XMJ/VarE9MVoA==} engines: {node: '>=22.0.0'} - '@objectstack/driver-mongodb@18.1.0': - resolution: {integrity: sha512-13n6BWwwSlC/OCfBl+A4EM1Z0F9P9VniH2tvI0cOwz+MNPfObT6Wa9A63VXAg5/MY0OPYSKuimJz61pcnzjsDA==} + '@objectstack/driver-mongodb@18.3.0': + resolution: {integrity: sha512-kAi5U5FqgE4EE87UAdB/fZc6WbzvpI6h4WnbRZ3RrfTQESh5enZ5n31EaZzTLODtUL3CYLkhcuNzOGXL88m9kQ==} engines: {node: '>=22.0.0'} - '@objectstack/driver-sql@18.1.0': - resolution: {integrity: sha512-4X1sYv5Bn6u1BUZKlQ2BRO0VKKUjS8uEX0lGKzOqcqDE5yJFl7e1wtU/4gt6WcKhbsQCmDC4/h0K9uarU8Xbmw==} + '@objectstack/driver-sql@18.3.0': + resolution: {integrity: sha512-FPD2OcKMB7d2i25mh9lFP4B4+aSEhCA0KWLwZ+Z5tvsiik3/Rb0SNlV6Lso1HbWBZGRbSmA8cB8AiLVK2Ljhjw==} engines: {node: '>=22.0.0'} peerDependencies: mysql2: ^3.0.0 @@ -434,117 +439,126 @@ packages: tedious: optional: true - '@objectstack/driver-sqlite-wasm@18.1.0': - resolution: {integrity: sha512-BKACgB6wsFjysKy4gZ+Oe8RmZcLNK8VKhm+QLjsfLQbF0C4Bgqil7ZEEbS/5OxJpVd+pEPP8v2JAK/KcBnm+TQ==} + '@objectstack/driver-sqlite-wasm@18.3.0': + resolution: {integrity: sha512-EsSIP74fyFdU/vQb5fTIyCKFGEnd8XGKmcWGPgQScPXsCFCcC7B7AAyda1t6h+p5z3Q/RwJP13/tJix5T2hhUw==} engines: {node: '>=22.0.0'} - '@objectstack/formula@18.1.0': - resolution: {integrity: sha512-3zyGz5t87F5pOcgt/1oyezcDvwOTUbQrbROZiI7t3AMmTl/mm774QcDhT4h/kFRBTUeQx0T7+M8pqTKJ1iFrSA==} + '@objectstack/formula@18.3.0': + resolution: {integrity: sha512-DyBj+DlqjYnOMNxtHffZSBfpDRb41cEfnq/byD+g3UUDM4GCeQHBGCa2ROS5rLDi0vrlVe5pddHcVANtP4NvLQ==} + engines: {node: '>=22.0.0'} - '@objectstack/lint@18.1.0': - resolution: {integrity: sha512-m9wSzH1+YhVZgB3gf07BIStWzZyLQkMgreAeIEKjc4jM2Wi8HPWzanibfNb2MBlph/M/E5w55yJNGlm+w6mOnw==} + '@objectstack/lint@18.3.0': + resolution: {integrity: sha512-pML07D403ABjxagPW0d9SAsmmVKRzasWkctX0UGG9oN5ybaRegm6/reQuRbCF5POT/r5JpJ78ItEInfjaqYAKQ==} engines: {node: '>=22.0.0'} - '@objectstack/mcp@18.1.0': - resolution: {integrity: sha512-Uu9LfFAKpYwEnXpNQLyZFeuB1wR6sPpSSwKru6wEpgMQQ9gT5QiPpeEzBak2vsWKc+Awf9w5hPnUC09gbQ9QbA==} + '@objectstack/mcp@18.3.0': + resolution: {integrity: sha512-K7cvLNlVoZ4OAnME50YnJy4P6Sl/LJkiiWA/kQuGLzxlkUBWpXFdBDfhbEXynvFwpMkBfrz6Z638R46/MTBLNA==} engines: {node: '>=22.0.0'} - '@objectstack/metadata-core@18.1.0': - resolution: {integrity: sha512-5rllDa/+dEgGB6/KFOplSF9OsvEW3gflWVxqJvVCKYoMhpkLDJspy8h6Sx8zDy54yPCi1/YliwNBZgY0EAnhQQ==} + '@objectstack/metadata-core@18.3.0': + resolution: {integrity: sha512-IcWsdZ360kZR3jatcK8OA0D7VTlGEZtj2KDMMlAUn/k3sx0DoxBCWVRPxzEKsRZ7tZ4/fqeoqGWpZYhrs6AWGA==} engines: {node: '>=22.0.0'} - '@objectstack/metadata-fs@18.1.0': - resolution: {integrity: sha512-2WvSOQdOUlB9C0VV+1ublW7iSUmzZDShb5zAAcUST9JKOjj9R9TkXVYINQIzBNzGcsw9qrgesUFhlb8IULyOEA==} + '@objectstack/metadata-fs@18.3.0': + resolution: {integrity: sha512-/teSIW1eA/YhRBe9hpnHLOZhPlSy/b1nE+1QUZuatI/SjZQSJSoYPi83Wu+oQ45yhk7VIXnFfDT5kodw1v9OBA==} engines: {node: '>=22.0.0'} - '@objectstack/metadata-protocol@18.1.0': - resolution: {integrity: sha512-z8Wh8lHAZ4zzYBKliqI/o80Wmbr4K4JV+FFnrXznt4CFu9qlRS4UtdD+2Ie3gKrIT1nhgI7A2j0bXPFi6ggz6Q==} + '@objectstack/metadata-protocol@18.3.0': + resolution: {integrity: sha512-Fug+O+de4842MYhH7eroBZlLE40OoMvXXNAupqlps99GA9iuzU8Do+5ZiCl1vwanhfHZYd632/O/KoPMa3iERQ==} engines: {node: '>=22.0.0'} - '@objectstack/metadata@18.1.0': - resolution: {integrity: sha512-JzbjTqAsepnXGZ7WrbaUKnF5xpQPYBH8KmfG/oYqm3lOiQ9JGZoXcMEcZHQNnOsj1Bf5wNchVBb52Pv5/PwZ/A==} + '@objectstack/metadata@18.3.0': + resolution: {integrity: sha512-pepkdkaId6jDFwcs0y/Fd0uXcPc0+mJRzjwAkhvgjmAviXGaJ8pPXBaO+LGn7egLqTjT9w8zgbKVjrAAqUUPvA==} engines: {node: '>=22.0.0'} - '@objectstack/objectql@18.1.0': - resolution: {integrity: sha512-Ld5hJSQFojc3oGTdbj2OvoApHxa3K5pqbTgd7PzNiUqQH8V4iirPEe4Cnh22i3VjHpWB9DEA3YJHaYkUrQLo+g==} + '@objectstack/objectql@18.3.0': + resolution: {integrity: sha512-sPthcFW5/Hk60dzo7SaS8AkB2c1VusxvofMz88bA0wG0kT55trffVcfOpofdo+zdG3HlrY/sIh0GpNQEjdqVXw==} engines: {node: '>=22.0.0'} - '@objectstack/observability@18.1.0': - resolution: {integrity: sha512-iwEXlrY0Y4pzxlUKN84A8PjioxQyFQ5PaOghgMpkLh/V5XorUT+W15AIK+BQD0sVmxB2iMZNkcDg8WqNr53/ZA==} + '@objectstack/observability@18.3.0': + resolution: {integrity: sha512-8KvQGECHfX0S5KwD1KqyI8J3PqnRM90wyP28jMt7furWUSyvFNWMNmG5YsOMOhn1qiGcPXPiN/iCXPO2DlpDSA==} engines: {node: '>=22.0.0'} - '@objectstack/platform-objects@18.1.0': - resolution: {integrity: sha512-roQPYsEbiCACebkHtpw70ojvtHl75PCvGpCSCvofz/i7pzlpyPW1uoAxKT3IEwsNjDK7TNmNEe/ghmRGXQMKPA==} + '@objectstack/platform-objects@18.3.0': + resolution: {integrity: sha512-CA4c4yz9H+4KlkKLq+BtShznGa1e45guOvZpfTz1JXFhn5kNcf9rrGPRpn0gGw1y4+X/uDUZv3gdSnAcTXi7TQ==} engines: {node: '>=22.0.0'} - '@objectstack/plugin-approvals@18.1.0': - resolution: {integrity: sha512-Z/sD7GxD45PJblGZLs+6wTjckIdEmInsoK+1WWx4E9OtrPlak6Ys887SqnSd+erxBhgdBUVjIGiopCjK5df++w==} + '@objectstack/plugin-approvals@18.3.0': + resolution: {integrity: sha512-dKe20+rmA8Q0XziTuNIcmilb6rCbT8439K6oJrOaeiZLWyr/G8uFe+OabrkdbbT7AyJOiVT3xpyrtMqrrR0YHg==} + engines: {node: '>=22.0.0'} - '@objectstack/plugin-audit@18.1.0': - resolution: {integrity: sha512-dumz3qmSL4HapjZumv56YfrfOzZ4cB6j7OQh4TSfFurOt6Pz1GqRhLYIPty3205InLjVeEK7T/MAn2JBwyFUZg==} + '@objectstack/plugin-audit@18.3.0': + resolution: {integrity: sha512-ZMWRyAe4TZ6sLV607yT1pO09Ssmd5y3lcm9FHjb0wzo+QGUD6cM27tsGPvrNSTs/2FrFsNBQ7Gdu/Hbcw+C/GQ==} engines: {node: '>=22.0.0'} - '@objectstack/plugin-auth@18.1.0': - resolution: {integrity: sha512-83VSCTlBPwZQ5tIU18F2/U+DbPU6j6V97RLsRSKF21UL3VYRA4/OLkrcZgTzEGVfETkz3r3HJflhYSq4Ed+4ww==} + '@objectstack/plugin-auth@18.3.0': + resolution: {integrity: sha512-co5k4Ec316/LxWfyXFdx8yMz8VxXBE/ty7shECH+i0uT4RpGJMqTUh8ezZb1op55/NsYmJ9DAvV+k+HnGs685g==} engines: {node: '>=22.0.0'} - '@objectstack/plugin-email@18.1.0': - resolution: {integrity: sha512-Az562+c7GWfYmRqLtBKQq7cDC7FG7/CI/MhtwXo3yN3RKfPlZLugqjw+PboSNPyZQnFBUUwo8F3e4tIk8uZFAQ==} + '@objectstack/plugin-email@18.3.0': + resolution: {integrity: sha512-i/+ptaThvlKZGjD4UsdwEURQ0jVA1SqlnLFzue3bvdzimrdl0FM385lce+QQOpbBZsKumj40QJ/9Cyzgg7SbVg==} + engines: {node: '>=22.0.0'} - '@objectstack/plugin-hono-server@18.1.0': - resolution: {integrity: sha512-WgBeu5oUtrfQSRZZWMv6aWq3/bgpVSkQFqgrc5bpgE9HsR33X0mb/6JeTfu+gf10MCC9WXNUY5Q1fgbzHrnp7Q==} + '@objectstack/plugin-hono-server@18.3.0': + resolution: {integrity: sha512-XNLD6h9Xz5V7gfaQOLuRB1suO26piHkvh68cVHFJbi7mnWqySeWoPpu0leB7khkylPMuABDN2F2VHJEK5FWhvw==} engines: {node: '>=22.0.0'} - '@objectstack/plugin-pinyin-search@18.1.0': - resolution: {integrity: sha512-/FXlBJyQ2fnuXWFMSGSuW9YLvdXRGDGfWGwsWhpPY9czCQ3oOUrQX8qSYecMpUoKEJsT3XIZHSaU/fDoKb6IYg==} + '@objectstack/plugin-pinyin-search@18.3.0': + resolution: {integrity: sha512-EcT53qg0ppP4K550gMnSVggjp0NJJrOoY9I3xoqGg7hkR9jEFYytR1ieJNbEP32yOjzPy9tjrBXoGSnA4pJzzQ==} + engines: {node: '>=22.0.0'} - '@objectstack/plugin-security@18.1.0': - resolution: {integrity: sha512-V+tGsH6a+8CZY9yUZ6gfq4vH8DvPNNd9YxqJBRtgwlMWb3iFxrh4Y8ACXoMvyuehX3vwt1wldUrpgr1WNbIUxQ==} + '@objectstack/plugin-security@18.3.0': + resolution: {integrity: sha512-3zZ9RHAIPDnt4d3HPiCxhgGCTPJu8tAMG4/c/Oe/8hVo7UFzDSOyp7ZFOphQCBYD54/6jSwb3+Ibhpe3qXsVwQ==} engines: {node: '>=22.0.0'} - '@objectstack/plugin-sharing@18.1.0': - resolution: {integrity: sha512-cZI/nmnZ9PcXkdNKBtuM4/Kw9aXG473yL4d+pWqxcLfqG2deG+naT+PnZ6J6mzAXXhYL1MkaBozHikxepYMNKA==} + '@objectstack/plugin-sharing@18.3.0': + resolution: {integrity: sha512-V662bOBJm0UabcZU4XO8AWrOzESu9f2gEtEQSahKrZ9f1lZzQH9tIRwQOLFrvIaqxTPUCNLfHWYWRk/yvf/7jg==} + engines: {node: '>=22.0.0'} - '@objectstack/plugin-webhooks@18.1.0': - resolution: {integrity: sha512-x3p8ic4NDJME03REovFC8lUsxM3VtojJnU39ef4YzLRDlcKOuMsjhQIzYWyWVvMqNGXuw40nvls8abr+cDLGqw==} + '@objectstack/plugin-webhooks@18.3.0': + resolution: {integrity: sha512-7FU1UctZJgW3Q3YtcEpwjYllp28/N41fuIXSzigb0sSfUQlDBKVrhWVM1CRdJ75h1mOVqG6JHFn/HcNpfncw3Q==} + engines: {node: '>=22.0.0'} - '@objectstack/rest@18.1.0': - resolution: {integrity: sha512-/ArsVYM0ua/WS6xCnWsY0ztrs6hfjYTff3Cw/ff3RUjg5ixcmdyFRiqWL0BVAjdEaWPpYDJbpsUX1HjNVqHCZw==} + '@objectstack/rest@18.3.0': + resolution: {integrity: sha512-G2iioVPReic1OWtJWjdnLuxxrc1f03QiPkkS2VLNBIfjsmRo/o+jMRWysyfARg5a5akwVsL/LOKpfaf4SJ1HdQ==} engines: {node: '>=22.0.0'} - '@objectstack/runtime@18.1.0': - resolution: {integrity: sha512-EDlekEZg+Al5KDYLv1kgP7URu3RLDBa1HQPu2+XCzOoe310pVe0/cKiwE0tINHk/3aemAMIxidSVpzSUXJ8dHg==} + '@objectstack/runtime@18.3.0': + resolution: {integrity: sha512-qln6H+dPedT39E7fVmYf6GCcdL9ZRmzqLbjijRB/mED90qtiqW3PDdTjgw85LLU3KV3P7cVLnxqbEkE7pgLwDQ==} engines: {node: '>=22.0.0'} peerDependencies: - '@objectstack/driver-turso': ^18.1.0 + '@objectstack/driver-turso': ^18.3.0 peerDependenciesMeta: '@objectstack/driver-turso': optional: true - '@objectstack/sdui-parser@18.1.0': - resolution: {integrity: sha512-8Bi5GDDMM862SjXb8tSS+eQUubSKkXUp6aI680Iwjj2GYcB+P9sA2N2Qu8nXyfnBEuaNrixdV7cfr0dDIB1NrA==} + '@objectstack/sdui-parser@18.3.0': + resolution: {integrity: sha512-KM/RSCB3hXjzo0fMOuu3np+YpArPijUqOweEg37yH44qaR2kmpJDMCu42+L7NwpqgPKH0a2ApPD650ooXtKzmw==} + engines: {node: '>=22.0.0'} - '@objectstack/service-analytics@18.1.0': - resolution: {integrity: sha512-7AKN2pP/MSrLqhDQWalup6C9pct0/SPudbIKTHPFRgZQRSSM2ClB8HuoPLFNXOJpkZRdPIpvBq2NnNRUfuf5Rg==} + '@objectstack/service-analytics@18.3.0': + resolution: {integrity: sha512-daxhMcO9oCIorDxVdCFBycwgNSRtNWNDshbsnZ3KYnI2sA1YNT4w4mnJfsq5RWO7qR0aHNaLOfA1tyEkrtdCpg==} engines: {node: '>=22.0.0'} - '@objectstack/service-automation@18.1.0': - resolution: {integrity: sha512-u7jq/Coq3X5JaZiQn0bt0aiqoZvOfjlDQe2/izb0FAvGhXXGqt2KAbcCPkCIpzvKySEahZ3+hHis04DtfmlmjA==} + '@objectstack/service-automation@18.3.0': + resolution: {integrity: sha512-5DkB28yfdmt5jTj5ViZFcy3MYiyRYPHKEEBC392paETu0WzRF8RlQPqsshmScsaZYTKxt6NVyTYE2Tig2nGClw==} engines: {node: '>=22.0.0'} - '@objectstack/service-cache@18.1.0': - resolution: {integrity: sha512-/SOyAJuYDI03weHG5K9BsCAbaZ9qtILs6r4o1RpUhmDrqGcFYxzZ6f9VZ+4IZRbStCtOAUmE+FoWqOcx+uq3SA==} + '@objectstack/service-cache@18.3.0': + resolution: {integrity: sha512-bpk8RyMIhx5nGsjLzgIHjPI5FbKoujaDpprh5sMs9p6XuBRf1zK0gj8wb+e8kJNxXBw+814F3mnWLvBPdwJO6Q==} engines: {node: '>=22.0.0'} - '@objectstack/service-cluster@18.1.0': - resolution: {integrity: sha512-Oov/6iTVcejdgKa963Ud7S48Tdb2iF/L82aBpQndCFe4UEYNxtD6HZWEs08Di7IL7gLhDEkXxbywvXSrCAlNlw==} + '@objectstack/service-cluster@18.3.0': + resolution: {integrity: sha512-9lXWpMwONZsueq2dfTukV2pFJbvoCfmAmz2/PsmKiNntq1vsBcrrPYZNsW/w8JYZHxdygLzT6awQ9j+DOAY7WA==} + engines: {node: '>=22.0.0'} - '@objectstack/service-datasource@18.1.0': - resolution: {integrity: sha512-dYvJsc24cTJ31mX5YWxnm92a+35x58EHQYfwp+cPKsmNpjHeMmOvHPk/D4Zos7l+AkyFhEAbjgGwOOmRIlaQqQ==} + '@objectstack/service-datasource@18.3.0': + resolution: {integrity: sha512-nZ3AsvBUQ+gkCll390l2kfpSkBh4lfCS2Q7p3tTPQah5igrLiyQlaGFghV1rNj400FzNz9jrjohZzlaSxlub1g==} + engines: {node: '>=22.0.0'} peerDependencies: - '@objectstack/driver-mongodb': 18.1.0 - '@objectstack/driver-sqlite-wasm': 18.1.0 - '@objectstack/driver-turso': ^18.1.0 + '@objectstack/driver-mongodb': 18.3.0 + '@objectstack/driver-sqlite-wasm': 18.3.0 + '@objectstack/driver-turso': ^18.3.0 peerDependenciesMeta: '@objectstack/driver-mongodb': optional: true @@ -553,43 +567,44 @@ packages: '@objectstack/driver-turso': optional: true - '@objectstack/service-i18n@18.1.0': - resolution: {integrity: sha512-RNfKjfnb5CmQ5pVtejkihjvUl4lM/P5snPbeOT3gSDKhK51W4KxpbqEKo8NtaDgeQEANkZZgKWRmE/X9iEjFkQ==} + '@objectstack/service-i18n@18.3.0': + resolution: {integrity: sha512-v2MTWZlxYplW/JCxzT7gtZJUBWr6DhWFloIXyDSbbnfT+ABTsWP4+S5AnPawjZnzCOm4bxkEyXgBeFdEasOKVQ==} engines: {node: '>=22.0.0'} - '@objectstack/service-job@18.1.0': - resolution: {integrity: sha512-qvG8NegO4kuB1CLqeBdYH4+nZ9YViS7OrBqKgUh54IvOMFZag23rsuJ5Y3xzJfXc1y6CEaSNF1HbE//5eSiZ1w==} + '@objectstack/service-job@18.3.0': + resolution: {integrity: sha512-arHAzYeuTuEPg3YTBUDGvE1fOha/qPWl57D5wfofp92N31MX6CKIYn4G/tJHqXD2pNfZamgDXi8HBLZYQl2zdA==} engines: {node: '>=22.0.0'} - '@objectstack/service-mailbox@18.1.0': - resolution: {integrity: sha512-bZk434GbA7c+J56l31qJ8EEGXsdbP79RMTdIhnGuhqGm4rCiWDp5VIBXbGHSYLwVXWyqW5A4qmNHLOLsUqihdQ==} + '@objectstack/service-mailbox@18.3.0': + resolution: {integrity: sha512-f6MiW1ZQ8FBA4VS2P6FljPRV8UIpCr0UzSBqR4HKtcnC+wkM8g32Igz0VjFIoubx3i/xByadOFgkdgylgQ/EHg==} engines: {node: '>=22.0.0'} - '@objectstack/service-messaging@18.1.0': - resolution: {integrity: sha512-yPAHmuxgFN78SBFB1/lvxOu7QcFG7XgNrRZt3Xpg/FP40f3HoEQ5LxD7/UCSIWkF6NpQqhESdiXVU0gcy/hctw==} + '@objectstack/service-messaging@18.3.0': + resolution: {integrity: sha512-dQsIE00iPG9sOOlEgOMiEq0Wk1LS/Vdk3eGQqeryQqvINXjfWmjoRmCRoam3I55VpIcdtPRvSanfFfg1U7gHJA==} engines: {node: '>=22.0.0'} - '@objectstack/service-package@18.1.0': - resolution: {integrity: sha512-8ZphV/dnYAoLN1szweT+vTXub75dBpFbX5J4voNlFn4VCtktgDKji8/oI/Nlt4fMUDNRz5WpvRxYZ3cKs7VdCg==} + '@objectstack/service-package@18.3.0': + resolution: {integrity: sha512-rqfC7uoYuDGauDjLQxWAXpIReOzrGLR1ui1ai7S5r8SAxwQx+rLSZCkoFbpfz49Qh6Q5qk/0L0xjdG3U9utkRA==} engines: {node: '>=22.0.0'} - '@objectstack/service-queue@18.1.0': - resolution: {integrity: sha512-U+286WDVDgFegOwn1Kb5cuK7ZdRDLAIfKxDNiitbl+c7vExKWwpFIHBCk8nJmljE3mMbwBCS2DE05EhI795T6w==} + '@objectstack/service-queue@18.3.0': + resolution: {integrity: sha512-2HGMOGh3bheyipachiJP7aSIqNr6FsVxt7cl25Sk5Wx0tDNiBGsIBi3VR35qSHPhqR6zgsYX5rDW4DC4IiVOdg==} engines: {node: '>=22.0.0'} - '@objectstack/service-realtime@18.1.0': - resolution: {integrity: sha512-zg7CC8NZyIM788308tHUlK6yQjL3s+ZBXn5WV2938qiVy/oatq6/ZBLvRfpTKNKuJ+a9Be+TwqfJ8CdknYj+Cg==} + '@objectstack/service-realtime@18.3.0': + resolution: {integrity: sha512-iigEo3MapVtiIGYIfJpYmf9V392UGy32yXaqMT5ADJK8kopRjKcwdYmRGaFnEStrRYEcDIel8UrpNJtx2XCN3Q==} engines: {node: '>=22.0.0'} - '@objectstack/service-settings@18.1.0': - resolution: {integrity: sha512-rgdCIwkBCQtjCRnlW/DNZGawqo7huapjMI1knZ18mmSLe/zG0fjBKaGop1qiGkbtOZPUJWD4uxtUotisixe+YA==} + '@objectstack/service-settings@18.3.0': + resolution: {integrity: sha512-RfbC34a5fKuJzQ6Ad7hNmQV1lVkvJNdLYA/BiCA3hVYptF9Q/s6lmGseOhisG8ZO1nOTNB4+Yno/iOWhUqIOgQ==} engines: {node: '>=22.0.0'} - '@objectstack/service-sms@18.1.0': - resolution: {integrity: sha512-Qtmk4/zFPJwCvW0qO9MbIhxdE9AiyYFRjBM22EbAgKfizVp/8iRWDo/5Sl4oeFm3DdVKLS4OxkR4p92G/a8vAw==} + '@objectstack/service-sms@18.3.0': + resolution: {integrity: sha512-XIRO5dzDQnvS/fb+PlGmcz2UMJtOy9VMMIDQ7tcdtnU0Dxw4YTNNrlTpKGMP4pIZxKLZ7vN13QfB7DI1fqqjiw==} + engines: {node: '>=22.0.0'} - '@objectstack/service-storage@18.1.0': - resolution: {integrity: sha512-nUaCFOl93TmAirIQkRwv1fwYl5jgdCtWvU+NuUPSoDu2V92/er4JLjIXC08Q5TIwN3dcpw+JO/0q6nLDvRir7w==} + '@objectstack/service-storage@18.3.0': + resolution: {integrity: sha512-alvlxmvALHcaHDaRDrISawRZn9jZgxkgR+brLJZlLTxhENlkhkIFmp5RcizkF6lkT2vYs//GB/nUq9hu5wyXJg==} engines: {node: '>=22.0.0'} peerDependencies: '@aws-sdk/client-s3': ^3.0.0 @@ -600,15 +615,16 @@ packages: '@aws-sdk/s3-request-presigner': optional: true - '@objectstack/setup@18.1.0': - resolution: {integrity: sha512-1WHZyQ6Gpq2a1AhUxplpThTF37InFQdZpz/n9GCA8zBAU8eHuVBzEHxT3x1rANxkrgAaz0bydFtcKAGLxsj/6g==} + '@objectstack/setup@18.3.0': + resolution: {integrity: sha512-d2Q/pz6NOmuFyn3vgAT5x5wm53IBFOjKJRLQXTZ2eDUd6QWosSsM8hfjlfs12QAOMYsspZ8nRA3Hi6O8vzUfXQ==} engines: {node: '>=22.0.0'} - '@objectstack/skills@18.1.0': - resolution: {integrity: sha512-br4VYpoWQvZK9Q5Zy5wypgd99BhEmcdXoKJWkr+91bnACwuQHsnc994TScmfPx43XEbiTvVZFp21RBY0Sjx26g==} + '@objectstack/skills@18.3.0': + resolution: {integrity: sha512-rmCpAScPYSz8sZ9R2+dSgteREeMaVXrxdsY+fALj/sFr1O0poyajRs0jN97uTH/wkCXNT2D5MKgbG97vB4XlEg==} + engines: {node: '>=22.0.0'} - '@objectstack/spec@18.1.0': - resolution: {integrity: sha512-Ib4iTFELKyHG+9kfXSyOxMkne1NFF1suo/o/xHVnLv9FQGDUCv7oPOSklBKHOYM3IBiMmxhXfSp1pbkFa5ksAg==} + '@objectstack/spec@18.3.0': + resolution: {integrity: sha512-8gKxvolZBHgFhcAuWiTMDO43FjrGx9YjqEBaRE91TOKNJShc9etbH795GDaER3zL7XxZRgDn3vwXy00kvaw8ig==} engines: {node: '>=22.0.0'} peerDependencies: ai: ^7.0.0 @@ -616,23 +632,24 @@ packages: ai: optional: true - '@objectstack/trigger-api@18.1.0': - resolution: {integrity: sha512-8NU/WTB62daZHDetLgFMtPGaCTQjkCpX6OJb231kzQmr6Wt5Saq90JPcQOjWDNUr4hsvcnU082KzURsluUyZCQ==} + '@objectstack/trigger-api@18.3.0': + resolution: {integrity: sha512-brdNTTH0mzajinhm2cN36MqfDMjcO2AHigfYe3DasekTma6mps86R78WY3NTstVSnrjm99EOrE92PPiVObPZJA==} + engines: {node: '>=22.0.0'} - '@objectstack/trigger-record-change@18.1.0': - resolution: {integrity: sha512-6lmzLBk8s9nxU6mwNLraFjcXqBJukyOLlJPShKLKD1tKFQ6nE/1g3dqTtWjJhIIuo7Q/7mHoFrZ424dCW68+yQ==} + '@objectstack/trigger-record-change@18.3.0': + resolution: {integrity: sha512-qMR/zoLxiwPENiCYo+Ab//+T6r4aBHVwOZNarEfS2mCPK/eumUf2UL3VAgjCc7K6LDUSOaPhdgPHKsEsRLgZ2Q==} engines: {node: '>=22.0.0'} - '@objectstack/trigger-schedule@18.1.0': - resolution: {integrity: sha512-euvfGFqSU5IhJ9MNU33p2q0q5QnSd1/NKSChC/CuWGfAjAd36EHhv5C3WePPrr+CYzVe3lBY33W+pDrU8iWImw==} + '@objectstack/trigger-schedule@18.3.0': + resolution: {integrity: sha512-3SKT3aQ1/YCf6aPilSAnj+0T1t1nOupsgyO6PmkvU0o7hMpEsuIY1LJytkvKdkdNcqHxSbmSbt7UPVcukBBw8w==} engines: {node: '>=22.0.0'} - '@objectstack/types@18.1.0': - resolution: {integrity: sha512-bM/rI6kLE1cvjWE8KMe2AWwNHE5XAYQC3WuaAyyxV7BXH8D+XukDx9h9O26GBg+z3hhz/gdYhhxelCN127BzgQ==} + '@objectstack/types@18.3.0': + resolution: {integrity: sha512-U69HnEgh9u81bXhz7XZJvgEc/NceY4CRPmWDT2ZBQTetHZsnDQb71Tr2srPV+PAjgW2oKEdD2jN5GyxzxHTVww==} engines: {node: '>=22.0.0'} - '@objectstack/verify@18.1.0': - resolution: {integrity: sha512-sLnGomkPd/4/uQRdDojVhEE1fu/Tv/B76IQWu1ZENOf5kG+QrF7OKS30qBX88Km3Hr0jpFPwDxYuBdgUBi5f8Q==} + '@objectstack/verify@18.3.0': + resolution: {integrity: sha512-0FLmG5uV2QJPQ+6RqiRC8upClByWNI0Q8cn73LcxA9wJsID3IEjlLQiIOBqLSx7gYUdEoJKR1hWULL8BruP8OA==} engines: {node: '>=22.0.0'} '@oclif/core@5.1.9': @@ -767,9 +784,6 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@ts-morph/common@0.29.0': - resolution: {integrity: sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1075,9 +1089,6 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} - code-block-writer@13.0.3: - resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1143,8 +1154,8 @@ packages: resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} engines: {node: '>= 10'} - create-objectstack@18.1.0: - resolution: {integrity: sha512-zJTLiiMZkmqBy4VYU3hDVPWTOrmqkKxRQzinxrkH2fBAYMg8+RbC5nBeG+ei0bQEt4AyWWEjiFeZrkfqMc3bAQ==} + create-objectstack@18.3.0: + resolution: {integrity: sha512-fhfEvZemX5dK1zyqRcXbKidcoTIdzXdEG8xlWGicmSCsnTD4h4pxv8nqhoQp+nKUoiELBzr5ZMndtGj3MH9upA==} engines: {node: '>=22.0.0'} hasBin: true @@ -1412,8 +1423,8 @@ packages: getopts@2.3.0: resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==} - glob@13.0.6: - resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + glob@14.0.3: + resolution: {integrity: sha512-A1vjIwC3qpdCso45Iw127gnp7sPhqgY8lUQhDrArIs+GbmfaaVPFuoyy7WyLT9EPZCRZ7j7aLdwUhQmHWesEow==} engines: {node: 18 || 20 || >=22} glob@7.2.3: @@ -1783,6 +1794,10 @@ packages: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} + minimatch@11.0.4: + resolution: {integrity: sha512-/UJ1dEoVQTjPNDh6SGlUo+ueLMmoHTas4q+8yaAR/5t6nLAIiBDJtoYUh20IYleix7ehiQkgczb84PE3uzHkxg==} + engines: {node: 20 || >=22} + minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} @@ -1909,9 +1924,6 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - path-expression-matcher@1.6.2: resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==} engines: {node: '>=14.0.0'} @@ -1927,8 +1939,8 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-scurry@2.0.2: - resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + path-scurry@3.0.0: + resolution: {integrity: sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==} engines: {node: 18 || 20 || >=22} path-to-regexp@8.4.2: @@ -2278,9 +2290,6 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-morph@28.0.0: - resolution: {integrity: sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==} - tsx@4.23.15: resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==} engines: {node: '>=18.0.0'} @@ -2343,6 +2352,10 @@ packages: deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true + validate-npm-package-name@7.0.2: + resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} + engines: {node: ^20.17.0 || >=22.9.0} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -2708,6 +2721,8 @@ snapshots: dependencies: hono: 4.13.13 + '@isaacs/nonrecursive-glob-match@1.0.3': {} + '@jitl/quickjs-ffi-types@0.32.0': {} '@jitl/quickjs-wasmfile-debug-asyncify@0.32.0': @@ -2777,73 +2792,73 @@ snapshots: '@nodable/entities@3.1.0': {} - '@objectstack/account@18.1.0': + '@objectstack/account@18.3.0': dependencies: - '@objectstack/platform-objects': 18.1.0 - '@objectstack/spec': 18.1.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/spec': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/cli@18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1))': - dependencies: - '@objectstack/account': 18.1.0 - '@objectstack/client': 18.1.0 - '@objectstack/cloud-connection': 18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) - '@objectstack/console': 18.1.0 - '@objectstack/core': 18.1.0 - '@objectstack/driver-memory': 18.1.0 - '@objectstack/driver-mongodb': 18.1.0 - '@objectstack/driver-sql': 18.1.0 - '@objectstack/driver-sqlite-wasm': 18.1.0(better-sqlite3@13.0.3) - '@objectstack/formula': 18.1.0 - '@objectstack/lint': 18.1.0 - '@objectstack/mcp': 18.1.0 - '@objectstack/metadata': 18.1.0 - '@objectstack/metadata-core': 18.1.0 - '@objectstack/metadata-protocol': 18.1.0 - '@objectstack/objectql': 18.1.0 - '@objectstack/observability': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/plugin-approvals': 18.1.0 - '@objectstack/plugin-audit': 18.1.0 - '@objectstack/plugin-auth': 18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) - '@objectstack/plugin-email': 18.1.0 - '@objectstack/plugin-hono-server': 18.1.0 - '@objectstack/plugin-pinyin-search': 18.1.0 - '@objectstack/plugin-security': 18.1.0 - '@objectstack/plugin-sharing': 18.1.0 - '@objectstack/plugin-webhooks': 18.1.0 - '@objectstack/rest': 18.1.0 - '@objectstack/runtime': 18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) - '@objectstack/service-analytics': 18.1.0 - '@objectstack/service-automation': 18.1.0 - '@objectstack/service-cache': 18.1.0 - '@objectstack/service-datasource': 18.1.0(@objectstack/driver-mongodb@18.1.0)(@objectstack/driver-sqlite-wasm@18.1.0) - '@objectstack/service-job': 18.1.0 - '@objectstack/service-mailbox': 18.1.0 - '@objectstack/service-messaging': 18.1.0 - '@objectstack/service-package': 18.1.0 - '@objectstack/service-queue': 18.1.0 - '@objectstack/service-realtime': 18.1.0 - '@objectstack/service-settings': 18.1.0 - '@objectstack/service-sms': 18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) - '@objectstack/service-storage': 18.1.0 - '@objectstack/setup': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/trigger-api': 18.1.0 - '@objectstack/trigger-record-change': 18.1.0 - '@objectstack/trigger-schedule': 18.1.0 - '@objectstack/types': 18.1.0 - '@objectstack/verify': 18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@objectstack/driver-mongodb@18.1.0)(@objectstack/driver-sqlite-wasm@18.1.0)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + '@objectstack/cli@18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1))': + dependencies: + '@objectstack/account': 18.3.0 + '@objectstack/client': 18.3.0 + '@objectstack/cloud-connection': 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + '@objectstack/console': 18.3.0 + '@objectstack/core': 18.3.0 + '@objectstack/driver-memory': 18.3.0 + '@objectstack/driver-mongodb': 18.3.0 + '@objectstack/driver-sql': 18.3.0 + '@objectstack/driver-sqlite-wasm': 18.3.0(better-sqlite3@13.0.3) + '@objectstack/formula': 18.3.0 + '@objectstack/lint': 18.3.0 + '@objectstack/mcp': 18.3.0 + '@objectstack/metadata': 18.3.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/metadata-protocol': 18.3.0 + '@objectstack/objectql': 18.3.0 + '@objectstack/observability': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/plugin-approvals': 18.3.0 + '@objectstack/plugin-audit': 18.3.0 + '@objectstack/plugin-auth': 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + '@objectstack/plugin-email': 18.3.0 + '@objectstack/plugin-hono-server': 18.3.0 + '@objectstack/plugin-pinyin-search': 18.3.0 + '@objectstack/plugin-security': 18.3.0 + '@objectstack/plugin-sharing': 18.3.0 + '@objectstack/plugin-webhooks': 18.3.0 + '@objectstack/rest': 18.3.0 + '@objectstack/runtime': 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + '@objectstack/service-analytics': 18.3.0 + '@objectstack/service-automation': 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + '@objectstack/service-cache': 18.3.0 + '@objectstack/service-datasource': 18.3.0(@objectstack/driver-mongodb@18.3.0)(@objectstack/driver-sqlite-wasm@18.3.0) + '@objectstack/service-job': 18.3.0 + '@objectstack/service-mailbox': 18.3.0 + '@objectstack/service-messaging': 18.3.0 + '@objectstack/service-package': 18.3.0 + '@objectstack/service-queue': 18.3.0 + '@objectstack/service-realtime': 18.3.0 + '@objectstack/service-settings': 18.3.0 + '@objectstack/service-sms': 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + '@objectstack/service-storage': 18.3.0 + '@objectstack/setup': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/trigger-api': 18.3.0 + '@objectstack/trigger-record-change': 18.3.0 + '@objectstack/trigger-schedule': 18.3.0 + '@objectstack/types': 18.3.0 + '@objectstack/verify': 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@objectstack/driver-mongodb@18.3.0)(@objectstack/driver-sqlite-wasm@18.3.0)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) '@oclif/core': 5.1.9 bundle-require: 5.1.0(esbuild@0.28.2) chalk: 6.0.1 chokidar: 5.0.0 - create-objectstack: 18.1.0 + create-objectstack: 18.3.0 dotenv-flow: 4.1.0 esbuild: 0.28.2 - ts-morph: 28.0.0 tsx: 4.23.15 + typescript: 6.0.3 yaml: 2.9.1 zod: 4.6.7 optionalDependencies: @@ -2892,22 +2907,23 @@ snapshots: - vitest - vue - '@objectstack/client@18.1.0': + '@objectstack/client@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/spec': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/spec': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/cloud-connection@18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1))': + '@objectstack/cloud-connection@18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1))': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/metadata-core': 18.1.0 - '@objectstack/metadata-protocol': 18.1.0 - '@objectstack/objectql': 18.1.0 - '@objectstack/runtime': 18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/metadata-protocol': 18.3.0 + '@objectstack/objectql': 18.3.0 + '@objectstack/observability': 18.3.0 + '@objectstack/runtime': 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 transitivePeerDependencies: - '@aws-sdk/credential-providers' - '@better-auth/utils' @@ -2951,32 +2967,32 @@ snapshots: - vitest - vue - '@objectstack/connector-imap@18.1.0': {} + '@objectstack/connector-imap@18.3.0': {} - '@objectstack/console@18.1.0': {} + '@objectstack/console@18.3.0': {} - '@objectstack/core@18.1.0': + '@objectstack/core@18.3.0': dependencies: - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 zod: 4.6.7 transitivePeerDependencies: - ai - '@objectstack/driver-memory@18.1.0': + '@objectstack/driver-memory@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 mingo: 7.2.5 transitivePeerDependencies: - ai - '@objectstack/driver-mongodb@18.1.0': + '@objectstack/driver-mongodb@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 mongodb: 7.7.0 nanoid: 6.0.2 transitivePeerDependencies: @@ -2989,12 +3005,12 @@ snapshots: - snappy - socks - '@objectstack/driver-sql@18.1.0': + '@objectstack/driver-sql@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/observability': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/observability': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 knex: 3.3.0(better-sqlite3@13.0.3) nanoid: 6.0.2 optionalDependencies: @@ -3008,11 +3024,11 @@ snapshots: - sqlite3 - supports-color - '@objectstack/driver-sqlite-wasm@18.1.0(better-sqlite3@13.0.3)': + '@objectstack/driver-sqlite-wasm@18.3.0(better-sqlite3@13.0.3)': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/driver-sql': 18.1.0 - '@objectstack/spec': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/driver-sql': 18.3.0 + '@objectstack/spec': 18.3.0 knex: 3.3.0(better-sqlite3@13.0.3) nanoid: 6.0.2 sql.js: 1.15.1 @@ -3029,18 +3045,18 @@ snapshots: - supports-color - tedious - '@objectstack/formula@18.1.0': + '@objectstack/formula@18.3.0': dependencies: '@marcbachmann/cel-js': 8.0.0 - '@objectstack/spec': 18.1.0 + '@objectstack/spec': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/lint@18.1.0': + '@objectstack/lint@18.3.0': dependencies: - '@objectstack/formula': 18.1.0 - '@objectstack/sdui-parser': 18.1.0 - '@objectstack/spec': 18.1.0 + '@objectstack/formula': 18.3.0 + '@objectstack/sdui-parser': 18.3.0 + '@objectstack/spec': 18.3.0 ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) sucrase: 3.35.1 @@ -3048,124 +3064,124 @@ snapshots: transitivePeerDependencies: - ai - '@objectstack/mcp@18.1.0': + '@objectstack/mcp@18.3.0': dependencies: '@modelcontextprotocol/sdk': 1.32.1(zod@4.6.7) - '@objectstack/core': 18.1.0 - '@objectstack/formula': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/formula': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 zod: 4.6.7 transitivePeerDependencies: - '@cfworker/json-schema' - ai - supports-color - '@objectstack/metadata-core@18.1.0': + '@objectstack/metadata-core@18.3.0': dependencies: - '@objectstack/spec': 18.1.0 + '@objectstack/spec': 18.3.0 zod: 4.6.7 transitivePeerDependencies: - ai - '@objectstack/metadata-fs@18.1.0': + '@objectstack/metadata-fs@18.3.0': dependencies: - '@objectstack/metadata-core': 18.1.0 + '@objectstack/metadata-core': 18.3.0 chokidar: 5.0.0 transitivePeerDependencies: - ai - '@objectstack/metadata-protocol@18.1.0': + '@objectstack/metadata-protocol@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/formula': 18.1.0 - '@objectstack/lint': 18.1.0 - '@objectstack/metadata': 18.1.0 - '@objectstack/metadata-core': 18.1.0 - '@objectstack/sdui-parser': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/formula': 18.3.0 + '@objectstack/lint': 18.3.0 + '@objectstack/metadata': 18.3.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/sdui-parser': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 zod: 4.6.7 transitivePeerDependencies: - ai - '@objectstack/metadata@18.1.0': + '@objectstack/metadata@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/metadata-core': 18.1.0 - '@objectstack/metadata-fs': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/metadata-fs': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 chokidar: 5.0.0 - glob: 13.0.6 + glob: 14.0.3 js-yaml: 5.5.0 zod: 4.6.7 transitivePeerDependencies: - ai - '@objectstack/objectql@18.1.0': + '@objectstack/objectql@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/formula': 18.1.0 - '@objectstack/metadata': 18.1.0 - '@objectstack/metadata-core': 18.1.0 - '@objectstack/metadata-protocol': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/formula': 18.3.0 + '@objectstack/metadata': 18.3.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/metadata-protocol': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) zod: 4.6.7 transitivePeerDependencies: - ai - '@objectstack/observability@18.1.0': + '@objectstack/observability@18.3.0': dependencies: - '@objectstack/spec': 18.1.0 + '@objectstack/spec': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/platform-objects@18.1.0': + '@objectstack/platform-objects@18.3.0': dependencies: - '@objectstack/metadata-core': 18.1.0 - '@objectstack/spec': 18.1.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/spec': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/plugin-approvals@18.1.0': + '@objectstack/plugin-approvals@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/formula': 18.1.0 - '@objectstack/metadata-core': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/formula': 18.3.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/plugin-audit@18.1.0': + '@objectstack/plugin-audit@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/metadata-core': 18.1.0 - '@objectstack/objectql': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/objectql': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/plugin-auth@18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1))': + '@objectstack/plugin-auth@18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1))': dependencies: '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5) '@better-auth/oauth-provider': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-auth@1.7.12(mongodb@7.7.0)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)))(better-call@1.4.4(zod@4.6.7)) '@better-auth/scim': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(better-auth@1.7.12(mongodb@7.7.0)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)))(better-call@1.4.4(zod@4.6.7)) '@better-auth/sso': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.5))(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-auth@1.7.12(mongodb@7.7.0)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)))(better-call@1.4.4(zod@4.6.7)) '@noble/hashes': 2.4.0 - '@objectstack/core': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/rest': 18.1.0 - '@objectstack/service-messaging': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/rest': 18.3.0 + '@objectstack/service-messaging': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 better-auth: 1.7.12(mongodb@7.7.0)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) jose: 6.2.13 transitivePeerDependencies: @@ -3195,109 +3211,111 @@ snapshots: - vitest - vue - '@objectstack/plugin-email@18.1.0': + '@objectstack/plugin-email@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/formula': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/spec': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/formula': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/spec': 18.3.0 nodemailer: 10.2.1 transitivePeerDependencies: - ai - '@objectstack/plugin-hono-server@18.1.0': + '@objectstack/plugin-hono-server@18.3.0': dependencies: '@hono/node-server': 2.1.5(hono@4.13.13) - '@objectstack/core': 18.1.0 - '@objectstack/observability': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/observability': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 hono: 4.13.13 transitivePeerDependencies: - ai - '@objectstack/plugin-pinyin-search@18.1.0': + '@objectstack/plugin-pinyin-search@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/objectql': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/objectql': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 pinyin-pro: 3.29.5 transitivePeerDependencies: - ai - '@objectstack/plugin-security@18.1.0': + '@objectstack/plugin-security@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/formula': 18.1.0 - '@objectstack/metadata-core': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/formula': 18.3.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/plugin-sharing@18.1.0': + '@objectstack/plugin-sharing@18.3.0': dependencies: '@noble/hashes': 2.4.0 - '@objectstack/core': 18.1.0 - '@objectstack/formula': 18.1.0 - '@objectstack/metadata-core': 18.1.0 - '@objectstack/objectql': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/formula': 18.3.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/objectql': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/plugin-webhooks@18.1.0': + '@objectstack/plugin-webhooks@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/service-messaging': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/service-messaging': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/rest@18.1.0': + '@objectstack/rest@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/metadata': 18.1.0 - '@objectstack/metadata-core': 18.1.0 - '@objectstack/observability': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/service-package': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/metadata': 18.3.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/observability': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/service-package': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 exceljs: 4.4.0 zod: 4.6.7 transitivePeerDependencies: - ai - '@objectstack/runtime@18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1))': - dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/driver-memory': 18.1.0 - '@objectstack/driver-sql': 18.1.0 - '@objectstack/driver-sqlite-wasm': 18.1.0(better-sqlite3@13.0.3) - '@objectstack/formula': 18.1.0 - '@objectstack/metadata': 18.1.0 - '@objectstack/metadata-core': 18.1.0 - '@objectstack/metadata-protocol': 18.1.0 - '@objectstack/objectql': 18.1.0 - '@objectstack/observability': 18.1.0 - '@objectstack/plugin-auth': 18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) - '@objectstack/plugin-security': 18.1.0 - '@objectstack/rest': 18.1.0 - '@objectstack/service-cluster': 18.1.0 - '@objectstack/service-datasource': 18.1.0(@objectstack/driver-mongodb@18.1.0)(@objectstack/driver-sqlite-wasm@18.1.0) - '@objectstack/service-i18n': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/runtime@18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1))': + dependencies: + '@objectstack/core': 18.3.0 + '@objectstack/driver-memory': 18.3.0 + '@objectstack/driver-sql': 18.3.0 + '@objectstack/driver-sqlite-wasm': 18.3.0(better-sqlite3@13.0.3) + '@objectstack/formula': 18.3.0 + '@objectstack/lint': 18.3.0 + '@objectstack/metadata': 18.3.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/metadata-protocol': 18.3.0 + '@objectstack/objectql': 18.3.0 + '@objectstack/observability': 18.3.0 + '@objectstack/plugin-auth': 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + '@objectstack/plugin-security': 18.3.0 + '@objectstack/rest': 18.3.0 + '@objectstack/service-cluster': 18.3.0 + '@objectstack/service-datasource': 18.3.0(@objectstack/driver-mongodb@18.3.0)(@objectstack/driver-sqlite-wasm@18.3.0) + '@objectstack/service-i18n': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 quickjs-emscripten: 0.32.0 zod: 4.6.7 optionalDependencies: - '@objectstack/driver-mongodb': 18.1.0 + '@objectstack/driver-mongodb': 18.3.0 transitivePeerDependencies: - '@aws-sdk/credential-providers' - '@better-auth/utils' @@ -3340,55 +3358,80 @@ snapshots: - vitest - vue - '@objectstack/sdui-parser@18.1.0': {} + '@objectstack/sdui-parser@18.3.0': {} - '@objectstack/service-analytics@18.1.0': + '@objectstack/service-analytics@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/service-automation@18.1.0': + '@objectstack/service-automation@18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1))': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/formula': 18.1.0 - '@objectstack/metadata-core': 18.1.0 - '@objectstack/metadata-protocol': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/formula': 18.3.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/metadata-protocol': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/plugin-auth': 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 transitivePeerDependencies: + - '@better-auth/utils' + - '@better-fetch/fetch' + - '@lynx-js/react' + - '@opentelemetry/api' + - '@prisma/client' + - '@sveltejs/kit' + - '@tanstack/react-start' + - '@tanstack/solid-start' - ai + - better-call + - drizzle-kit + - drizzle-orm + - kysely + - mongodb + - mysql2 + - nanostores + - next + - pg + - prisma + - react + - react-dom + - solid-js + - svelte + - vitest + - vue - '@objectstack/service-cache@18.1.0': + '@objectstack/service-cache@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/observability': 18.1.0 - '@objectstack/spec': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/observability': 18.3.0 + '@objectstack/spec': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/service-cluster@18.1.0': + '@objectstack/service-cluster@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/spec': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/spec': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/service-datasource@18.1.0(@objectstack/driver-mongodb@18.1.0)(@objectstack/driver-sqlite-wasm@18.1.0)': + '@objectstack/service-datasource@18.3.0(@objectstack/driver-mongodb@18.3.0)(@objectstack/driver-sqlite-wasm@18.3.0)': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/driver-memory': 18.1.0 - '@objectstack/driver-sql': 18.1.0 - '@objectstack/metadata-core': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/driver-memory': 18.3.0 + '@objectstack/driver-sql': 18.3.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 pg-connection-string: 2.14.1 optionalDependencies: - '@objectstack/driver-mongodb': 18.1.0 - '@objectstack/driver-sqlite-wasm': 18.1.0(better-sqlite3@13.0.3) + '@objectstack/driver-mongodb': 18.3.0 + '@objectstack/driver-sqlite-wasm': 18.3.0(better-sqlite3@13.0.3) transitivePeerDependencies: - ai - mariadb @@ -3401,82 +3444,82 @@ snapshots: - supports-color - tedious - '@objectstack/service-i18n@18.1.0': + '@objectstack/service-i18n@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/service-job@18.1.0': + '@objectstack/service-job@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/spec': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/spec': 18.3.0 croner: 10.0.1 transitivePeerDependencies: - ai - '@objectstack/service-mailbox@18.1.0': + '@objectstack/service-mailbox@18.3.0': dependencies: - '@objectstack/connector-imap': 18.1.0 - '@objectstack/core': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/connector-imap': 18.3.0 + '@objectstack/core': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 postal-mime: 4.1.4 transitivePeerDependencies: - ai - '@objectstack/service-messaging@18.1.0': + '@objectstack/service-messaging@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/service-package@18.1.0': + '@objectstack/service-package@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/metadata-core': 18.1.0 - '@objectstack/spec': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/spec': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/service-queue@18.1.0': + '@objectstack/service-queue@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/spec': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/spec': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/service-realtime@18.1.0': + '@objectstack/service-realtime@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/spec': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/spec': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/service-settings@18.1.0': + '@objectstack/service-settings@18.3.0': dependencies: '@noble/ciphers': 2.4.0 - '@objectstack/core': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/service-sms@18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1))': + '@objectstack/service-sms@18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1))': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/plugin-auth': 18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) - '@objectstack/spec': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/plugin-auth': 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + '@objectstack/spec': 18.3.0 transitivePeerDependencies: - '@better-auth/utils' - '@better-fetch/fetch' @@ -3504,96 +3547,96 @@ snapshots: - vitest - vue - '@objectstack/service-storage@18.1.0': + '@objectstack/service-storage@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/observability': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/observability': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/setup@18.1.0': + '@objectstack/setup@18.3.0': dependencies: - '@objectstack/platform-objects': 18.1.0 - '@objectstack/spec': 18.1.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/spec': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/skills@18.1.0': {} + '@objectstack/skills@18.3.0': {} - '@objectstack/spec@18.1.0': + '@objectstack/spec@18.3.0': dependencies: pg-connection-string: 2.14.1 zod: 4.6.7 - '@objectstack/trigger-api@18.1.0': + '@objectstack/trigger-api@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/spec': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/spec': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/trigger-record-change@18.1.0': + '@objectstack/trigger-record-change@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/spec': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/spec': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/trigger-schedule@18.1.0': + '@objectstack/trigger-schedule@18.3.0': dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/metadata-core': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/core': 18.3.0 + '@objectstack/metadata-core': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/types': 18.3.0 croner: 10.0.1 transitivePeerDependencies: - ai - '@objectstack/types@18.1.0': + '@objectstack/types@18.3.0': dependencies: - '@objectstack/spec': 18.1.0 + '@objectstack/spec': 18.3.0 transitivePeerDependencies: - ai - '@objectstack/verify@18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@objectstack/driver-mongodb@18.1.0)(@objectstack/driver-sqlite-wasm@18.1.0)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1))': - dependencies: - '@objectstack/core': 18.1.0 - '@objectstack/mcp': 18.1.0 - '@objectstack/metadata': 18.1.0 - '@objectstack/objectql': 18.1.0 - '@objectstack/platform-objects': 18.1.0 - '@objectstack/plugin-approvals': 18.1.0 - '@objectstack/plugin-audit': 18.1.0 - '@objectstack/plugin-auth': 18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) - '@objectstack/plugin-email': 18.1.0 - '@objectstack/plugin-hono-server': 18.1.0 - '@objectstack/plugin-pinyin-search': 18.1.0 - '@objectstack/plugin-security': 18.1.0 - '@objectstack/plugin-sharing': 18.1.0 - '@objectstack/plugin-webhooks': 18.1.0 - '@objectstack/rest': 18.1.0 - '@objectstack/runtime': 18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) - '@objectstack/service-analytics': 18.1.0 - '@objectstack/service-automation': 18.1.0 - '@objectstack/service-cache': 18.1.0 - '@objectstack/service-datasource': 18.1.0(@objectstack/driver-mongodb@18.1.0)(@objectstack/driver-sqlite-wasm@18.1.0) - '@objectstack/service-job': 18.1.0 - '@objectstack/service-mailbox': 18.1.0 - '@objectstack/service-messaging': 18.1.0 - '@objectstack/service-package': 18.1.0 - '@objectstack/service-queue': 18.1.0 - '@objectstack/service-realtime': 18.1.0 - '@objectstack/service-settings': 18.1.0 - '@objectstack/service-sms': 18.1.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) - '@objectstack/service-storage': 18.1.0 - '@objectstack/spec': 18.1.0 - '@objectstack/trigger-api': 18.1.0 - '@objectstack/trigger-record-change': 18.1.0 - '@objectstack/trigger-schedule': 18.1.0 - '@objectstack/types': 18.1.0 + '@objectstack/verify@18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(@objectstack/driver-mongodb@18.3.0)(@objectstack/driver-sqlite-wasm@18.3.0)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1))': + dependencies: + '@objectstack/core': 18.3.0 + '@objectstack/mcp': 18.3.0 + '@objectstack/metadata': 18.3.0 + '@objectstack/objectql': 18.3.0 + '@objectstack/platform-objects': 18.3.0 + '@objectstack/plugin-approvals': 18.3.0 + '@objectstack/plugin-audit': 18.3.0 + '@objectstack/plugin-auth': 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + '@objectstack/plugin-email': 18.3.0 + '@objectstack/plugin-hono-server': 18.3.0 + '@objectstack/plugin-pinyin-search': 18.3.0 + '@objectstack/plugin-security': 18.3.0 + '@objectstack/plugin-sharing': 18.3.0 + '@objectstack/plugin-webhooks': 18.3.0 + '@objectstack/rest': 18.3.0 + '@objectstack/runtime': 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(better-sqlite3@13.0.3)(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + '@objectstack/service-analytics': 18.3.0 + '@objectstack/service-automation': 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + '@objectstack/service-cache': 18.3.0 + '@objectstack/service-datasource': 18.3.0(@objectstack/driver-mongodb@18.3.0)(@objectstack/driver-sqlite-wasm@18.3.0) + '@objectstack/service-job': 18.3.0 + '@objectstack/service-mailbox': 18.3.0 + '@objectstack/service-messaging': 18.3.0 + '@objectstack/service-package': 18.3.0 + '@objectstack/service-queue': 18.3.0 + '@objectstack/service-realtime': 18.3.0 + '@objectstack/service-settings': 18.3.0 + '@objectstack/service-sms': 18.3.0(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.7))(kysely@0.29.7)(mongodb@7.7.0)(nanostores@1.5.5)(vitest@4.0.18(tsx@4.23.15)(yaml@2.9.1)) + '@objectstack/service-storage': 18.3.0 + '@objectstack/spec': 18.3.0 + '@objectstack/trigger-api': 18.3.0 + '@objectstack/trigger-record-change': 18.3.0 + '@objectstack/trigger-schedule': 18.3.0 + '@objectstack/types': 18.3.0 transitivePeerDependencies: - '@aws-sdk/client-s3' - '@aws-sdk/credential-providers' @@ -3740,12 +3783,6 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@ts-morph/common@0.29.0': - dependencies: - minimatch: 10.2.6 - path-browserify: 1.0.1 - tinyglobby: 0.2.17 - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -4045,8 +4082,6 @@ snapshots: cli-spinners@2.9.2: {} - code-block-writer@13.0.3: {} - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -4094,10 +4129,11 @@ snapshots: crc-32: 1.2.2 readable-stream: 3.6.2 - create-objectstack@18.1.0: + create-objectstack@18.3.0: dependencies: chalk: 6.0.1 commander: 15.0.0 + validate-npm-package-name: 7.0.2 croner@10.0.1: {} @@ -4481,11 +4517,11 @@ snapshots: getopts@2.3.0: {} - glob@13.0.6: + glob@14.0.3: dependencies: - minimatch: 10.2.6 + minimatch: 11.0.4 minipass: 7.1.3 - path-scurry: 2.0.2 + path-scurry: 3.0.0 glob@7.2.3: dependencies: @@ -4802,6 +4838,11 @@ snapshots: dependencies: brace-expansion: 5.0.12 + minimatch@11.0.4: + dependencies: + '@isaacs/nonrecursive-glob-match': 1.0.3 + brace-expansion: 5.0.12 + minimatch@3.1.5: dependencies: brace-expansion: 1.1.21 @@ -4892,8 +4933,6 @@ snapshots: parseurl@1.3.3: {} - path-browserify@1.0.1: {} - path-expression-matcher@1.6.2: {} path-is-absolute@1.0.1: {} @@ -4902,7 +4941,7 @@ snapshots: path-parse@1.0.7: {} - path-scurry@2.0.2: + path-scurry@3.0.0: dependencies: lru-cache: 11.5.3 minipass: 7.1.3 @@ -5342,11 +5381,6 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-morph@28.0.0: - dependencies: - '@ts-morph/common': 0.29.0 - code-block-writer: 13.0.3 - tsx@4.23.15: dependencies: esbuild: 0.28.2 @@ -5434,6 +5468,8 @@ snapshots: uuid@8.3.2: {} + validate-npm-package-name@7.0.2: {} + vary@1.1.2: {} vite@7.3.7(tsx@4.23.15)(yaml@2.9.1): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 61cf4ce..438757d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -20,14 +20,14 @@ peerDependencyRules: '@better-auth/scim>@better-auth/utils': '0.5.0' '@better-auth/sso>@better-auth/utils': '0.5.0' -# The CLI depends on these by range; hold them at the version package.json pins (18.1 upgrade checklist). +# The CLI depends on these by range; hold them at the version package.json pins (upgrade checklist). # Move them together with the @objectstack/* pins at every upgrade. overrides: - '@objectstack/core': '18.1.0' - '@objectstack/driver-memory': '18.1.0' - '@objectstack/driver-mongodb': '18.1.0' - '@objectstack/driver-sql': '18.1.0' - '@objectstack/driver-sqlite-wasm': '18.1.0' - '@objectstack/objectql': '18.1.0' - '@objectstack/observability': '18.1.0' - '@objectstack/runtime': '18.1.0' + '@objectstack/core': '18.3.0' + '@objectstack/driver-memory': '18.3.0' + '@objectstack/driver-mongodb': '18.3.0' + '@objectstack/driver-sql': '18.3.0' + '@objectstack/driver-sqlite-wasm': '18.3.0' + '@objectstack/objectql': '18.3.0' + '@objectstack/observability': '18.3.0' + '@objectstack/runtime': '18.3.0' diff --git a/src/core/analytics/delivery.report.ts b/src/core/analytics/delivery.report.ts index 1ac7bfa..56240ad 100644 --- a/src/core/analytics/delivery.report.ts +++ b/src/core/analytics/delivery.report.ts @@ -1,8 +1,7 @@ import { defineReport } from '@objectstack/spec/ui'; // 按供应商、期间出 OTD、批次合格率、PPM(05 §4.1;v0.4 出口条件)。考评单(v0.5)按同一口径取数。 -// 报表在翻译包里没有词条组。标签暂用中文字符串:控制台把内联的多语言写法 { 'zh-CN', en } 当文本直接渲染, -// 报表页白屏(objectui#13470);平台修好后改回内联多语言写法,补上英文(ADR-0007)。 +// 标签、说明的英文在翻译包的 reports 组(18.3.0 起报表可翻译,objectstack#25788)。 export const DeliveryScorecardReport = defineReport({ name: 'srm_delivery_scorecard', label: '供应商交付与质量(按期间)', diff --git a/src/core/flows/admission/approval.flow.ts b/src/core/flows/admission/approval.flow.ts index 15fd7cf..cc7d001 100644 --- a/src/core/flows/admission/approval.flow.ts +++ b/src/core/flows/admission/approval.flow.ts @@ -7,7 +7,7 @@ import type * as Automation from '@objectstack/spec/automation'; // 任一级驳回,申请退回「资料审核」。 // 职责分离(07 §2.1 第 4 步,03 §12):初审人就是申请的新建人或把申请推进到准入评审的人时,品类经理一级 // 上提给采购总监、与第 2 级合并(同合同、处置单的做法),申请上 review_escalated 记为 true。不要求「另一位 -// 品类经理」:只有一位品类经理的品类会卡住。平台审批服务没有「提交人不得审批」的开关(objectstack#25446)。 +// 品类经理」:只有一位品类经理的品类会卡住。平台审批服务没有「提交人不得审批」的开关(objectstack#25446;18.3.0 已提供 excludeSubmitter,改用见 hotsrm#96)。 // 批准后:申请→准入通过,供应商→合格,按申请品类建供货品类资格:现场审核结论为有条件通过时为「试供」, // 否则为「合格」(03 §3 第 7 步,WI-01 6.6.2)。 diff --git a/src/core/flows/admission/resubmit.flow.ts b/src/core/flows/admission/resubmit.flow.ts index 2430180..aaf8abb 100644 --- a/src/core/flows/admission/resubmit.flow.ts +++ b/src/core/flows/admission/resubmit.flow.ts @@ -6,7 +6,8 @@ import type * as Automation from '@objectstack/spec/automation'; // 通知初审人(还没指定时通知退回申请的人)。申请本身的品类、初审人等由内部维护,门户不直接改。 // // 以系统身份运行:门户对准入申请只有读共享(ADR-0010 两把锁)。flow 进门判断启动人持门户岗位、并且是这张申请所属供应商 -// 在职联系人上登记的门户账号(与成员键同一份名册),伪造别家申请的 recordId 时名册对不上,直接结束(直接触发不检查记录可读,objectstack#29050;谁可以启动见 objectstack#25481)。 +// 在职联系人上登记的门户账号(与成员键同一份名册),伪造别家申请的 recordId 时名册对不上,直接结束。18.3.0 起启动入口先以启动人身份读 recordId(objectstack#29050),别家的申请到不了这里,名册检查保留作第二道防线; +// 谁可以启动见 objectstack#25481。 const uid = `current_user != null ? current_user.id : ''`; const allowed = `current_user != null && 'srm_portal_user' in current_user.positions && application != null && membership != null`; const returned = `application.status == 'returned'`; diff --git a/src/core/flows/change-request/change-approval.flow.ts b/src/core/flows/change-request/change-approval.flow.ts index 0b53b9f..ca9a52e 100644 --- a/src/core/flows/change-request/change-approval.flow.ts +++ b/src/core/flows/change-request/change-approval.flow.ts @@ -7,7 +7,7 @@ import type * as Automation from '@objectstack/spec/automation'; // 通过后经 srm_change_apply 写回档案;驳回不改主记录,把驳回意见写进 reject_reason 并通知发起人。 // // 职责分离:发起人不能审批本人的变更单,两级审批人不能是同一人。平台审批服务还没有「提交人不得 -// 审批本人单据」的开关(objectstack#25446),岗位又可能兼任,所以每级通过后由 flow 读审批记录比对: +// 审批本人单据」的开关(objectstack#25446;18.3.0 已提供 excludeSubmitter,改用见 hotsrm#96),岗位又可能兼任,所以每级通过后由 flow 读审批记录比对: // 撞人时这次通过不算数,通知审批人后重新开一轮同级审批(临时做法,平台支持后改为节点配置)。 const snapshotOf = (v: string, keys: string[]) => ({ dialect: 'cel' as const, diff --git a/src/core/flows/change-request/change-submit.flow.ts b/src/core/flows/change-request/change-submit.flow.ts index 7367416..4b26a49 100644 --- a/src/core/flows/change-request/change-submit.flow.ts +++ b/src/core/flows/change-request/change-submit.flow.ts @@ -8,8 +8,8 @@ import type * as Automation from '@objectstack/spec/automation'; // 以系统身份运行:门户对变更单只有读共享(ADR-0010 两把锁),新建要靠 flow。所以 flow 进门自己判断「是不是这家供应商的 // 门户管理员」——持 srm_portal_admin 岗位,并且是这家供应商在职联系人上登记的门户账号(与成员键同一份名册)。供应商取自 // 启动记录,但不信任它:伪造别家的 recordId 时名册对不上,直接结束。银行账户、联系人的归属由变更单上的校验规则再把一道关。 -// 临时做法:直接触发 flow(REST trigger)不像按钮那样先按启动人的权限读一遍 recordId(objectstack#29050),系统身份的 flow -// 会读到别家的记录,所以名册检查留在 flow 里,平台补上后再评估。谁可以启动另见 objectstack#25481。 +// 18.3.0 起所有启动入口(按钮、直接触发)都先以启动人身份读 recordId,读不到答「不存在」(objectstack#29050),别家的 +// 供应商到不了这里;名册检查保留作第二道防线,并拦下读得到记录的内部员工和本公司的普通门户用户。谁可以启动另见 objectstack#25481。 const uid = `current_user != null ? current_user.id : ''`; const allowed = `current_user != null && 'srm_portal_admin' in current_user.positions && supplier != null && membership != null`; const c = (source: string) => ({ dialect: 'cel' as const, source }); diff --git a/src/core/flows/contract/approval.flow.ts b/src/core/flows/contract/approval.flow.ts index dcd5c01..12c6edc 100644 --- a/src/core/flows/contract/approval.flow.ts +++ b/src/core/flows/contract/approval.flow.ts @@ -16,7 +16,7 @@ const atLeast = (tier: number) => ({ dialect: 'cel' as const, source: `${amount} const below = (tier: number) => ({ dialect: 'cel' as const, source: `${amount} < ${tier}.0` }); // 职责分离:起草人和提交人不能审批本合同,各级审批人不能是同一人。平台审批服务还没有「提交人不得 -// 审批本人单据」的开关(objectstack#25446),岗位又可能兼任,所以每级通过后由 flow 读审批记录比对: +// 审批本人单据」的开关(objectstack#25446;18.3.0 已提供 excludeSubmitter,改用见 hotsrm#96),岗位又可能兼任,所以每级通过后由 flow 读审批记录比对: // 撞人时这次通过不算数,通知审批人后重新开一轮同级审批(临时做法,同变更单、处置单)。 // 起草人或提交人本身是品类经理时,品类经理一级改由上一级(采购总监)审批(03 §12、GL-03 附录 A)。 const cel$ = (source: string) => ({ dialect: 'cel' as const, source }); diff --git a/src/core/flows/disposition/disposition-approval.flow.ts b/src/core/flows/disposition/disposition-approval.flow.ts index 1872dbe..4e95be6 100644 --- a/src/core/flows/disposition/disposition-approval.flow.ts +++ b/src/core/flows/disposition/disposition-approval.flow.ts @@ -9,7 +9,7 @@ import type * as Automation from '@objectstack/spec/automation'; // 执行;驳回不改供应商,把驳回意见写进 reject_reason 并通知发起人。 // // 职责分离:发起人不能审批本人的处置单,各级审批人不能是同一人。平台审批服务还没有「提交人不得 -// 审批本人单据」的开关(objectstack#25446),岗位又可能兼任,所以每级通过后由 flow 读审批记录比对: +// 审批本人单据」的开关(objectstack#25446;18.3.0 已提供 excludeSubmitter,改用见 hotsrm#96),岗位又可能兼任,所以每级通过后由 flow 读审批记录比对: // 撞人时这次通过不算数,通知审批人后重新开一轮同级审批(临时做法,平台支持后改为节点配置)。 const cel$ = (source: string) => ({ dialect: 'cel' as const, source }); diff --git a/src/core/flows/performance/publish.flow.ts b/src/core/flows/performance/publish.flow.ts index 965a47b..e85045e 100644 --- a/src/core/flows/performance/publish.flow.ts +++ b/src/core/flows/performance/publish.flow.ts @@ -12,7 +12,7 @@ import { SCORE_DAYS, gradeOf } from './params.js'; // 驳回即「退回重评」:回到人工评分,审定意见写入 review_comment,截止日重新起算,通知评分人改分。只有一级审批, // 所以不用平台的「退回修改」(考评单没有发起人可以重新提交)。 // -// 职责分离(同 BR-11):审定人不能是本考评单的评分人。平台还没有「提交人不得审批」开关(objectstack#25446), +// 职责分离(同 BR-11):审定人不能是本考评单的评分人。平台还没有「提交人不得审批」开关(objectstack#25446;18.3.0 已提供 excludeSubmitter,改用见 hotsrm#96), // 所以通过后由 flow 读审批记录比对,撞人时这次通过不算数,通知后重新开一轮审定(同 srm_disposition_approval)。 // // 临时:CEL 没有对列表求和,加权合计只能逐行累加(07 §6.3 PF15,数据点记在 objectstack#14500)。 diff --git a/src/core/flows/quality/scar-overdue-sweep.flow.ts b/src/core/flows/quality/scar-overdue-sweep.flow.ts index 1df9428..dfcb848 100644 --- a/src/core/flows/quality/scar-overdue-sweep.flow.ts +++ b/src/core/flows/quality/scar-overdue-sweep.flow.ts @@ -51,8 +51,8 @@ const eachIssue = (key: string, collection: ReturnType, title: strin // P7 步骤 5(03 §8,FR-605):追赶式扫描,超过临时措施期限仍「待供应商响应」、或超过最终回复期限仍未进入 // 效果验证的整改单置为逾期,通知负责 SQE 与质量经理(SQE 主管)。只处理还没标记逾期的整改单, // 重复运行不重复通知(03 §2.3 R2);标志保留到关闭,供「SCAR 按期关闭率」统计(05 §4.1)。 -// 每小时跑一次。外部心跳(03 §2.3 R3)暂缺:api 触发的 flow 只能把签名密钥写死在元数据里,公开仓库不能这么做 -// (objectstack#25675),平台支持部署时注入密钥后再加 srm_scar_overdue_heartbeat。 +// 每小时跑一次。外部心跳(03 §2.3 R3)暂缺:18.3.0 起 api flow 能按名字引用部署时注入的密钥(config.secretRef, +// objectstack#25675),srm_scar_overdue_heartbeat 见 hotsrm#98。 // 供应商侧的逾期通知在 srm_scar_supplier_notice(is_overdue 置位时,v0.6 U6)。 export const ScarOverdueSweepFlow: Automation.Flow = { name: 'srm_scar_overdue_sweep', diff --git a/src/core/index.ts b/src/core/index.ts index 8d96802..7603581 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -32,7 +32,7 @@ export default defineStack({ type: 'app', name: 'hotsrm', description: '供应商关系管理(SRM)', - engines: { protocol: '^18.1.0' }, + engines: { protocol: '^18.3.0' }, }, // auth + ui:登录页和控制台;automation + triggers + job:记录触发与定时 flow;approvals:审批节点; diff --git a/src/core/objects/kpi.object.ts b/src/core/objects/kpi.object.ts index efe2a1b..9d4d72b 100644 --- a/src/core/objects/kpi.object.ts +++ b/src/core/objects/kpi.object.ts @@ -9,13 +9,6 @@ const cel = (source: string) => ({ dialect: 'cel' as const, source }); const has = (k: string) => `(has(record.${k}) && record.${k} != null)`; const filled = (k: string) => `(${has(k)} && !isBlank(record.${k}))`; const auto = P`record.method == 'auto'`; -// 表单帮助文字。临时:i18n 检查不把源语言的 inlineHelpText 算作 zh-CN 词条,英文给了 help 后要 zh-CN 也有一份 -// (objectstack#25683),所以 zh-CN 翻译包引用这里的同一份文字(translations/index.ts),平台修复后删掉 zh-CN 包 -export const KpiHelp = { - full_score_value: '原始值达到此值得 100 分;比率按百分点填(98 即 98%),PPM 按 PPM 填', - zero_score_value: '原始值达到此值得 0 分,与满分值之间按比例计分', -}; - export const Kpi = ObjectSchema.create({ name: 'srm_kpi', label: '绩效指标', @@ -81,7 +74,7 @@ export const Kpi = ObjectSchema.create({ scale: 2, requiredWhen: auto, visibleWhen: auto, - inlineHelpText: KpiHelp.full_score_value, + inlineHelpText: '原始值达到此值得 100 分;比率按百分点填(98 即 98%),PPM 按 PPM 填', }), zero_score_value: Field.number({ label: '零分值', @@ -89,7 +82,7 @@ export const Kpi = ObjectSchema.create({ scale: 2, requiredWhen: auto, visibleWhen: auto, - inlineHelpText: KpiHelp.zero_score_value, + inlineHelpText: '原始值达到此值得 0 分,与满分值之间按比例计分', }), }, diff --git a/src/core/objects/supplier-bank-account.object.ts b/src/core/objects/supplier-bank-account.object.ts index a840057..c9ed00f 100644 --- a/src/core/objects/supplier-bank-account.object.ts +++ b/src/core/objects/supplier-bank-account.object.ts @@ -4,9 +4,8 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; // 银行账户(02 §4.1,v0.2 M2)。采购侧新建后为「待核验」并锁定,应付会计经「核验银行账户」 // screen flow 置为有效(双人审批,04 §4.2);有效账户的关键字段只读,修改走信息变更单(v0.2 M4)。 // 收款与付款留在 ERP(ADR-0008),这里只保存账户主数据。 -// 新建表单里 status 已带默认值「待核验」,界面照样按 readonlyWhen 锁字段(服务端新建时不看 readonlyWhen), -// 所以先判断记录已保存(有 id),新建表单才能填写。临时绕法(objectstack#25584),平台修复后去掉 id 判断 -const locked = P`has(record.id) && record.id != null && (record.status == 'pending_verify' || record.status == 'active' || record.status == 'inactive')`; +// 新建表单里 status 已带默认值「待核验」;服务端和新建表单在新建时都不看 readonlyWhen(新建表单 18.3.0 起,objectui#13436) +const locked = P`record.status == 'pending_verify' || record.status == 'active' || record.status == 'inactive'`; export const SupplierBankAccount = ObjectSchema.create({ name: 'srm_supplier_bank_account', diff --git a/src/core/objects/supplier-change-request.object.ts b/src/core/objects/supplier-change-request.object.ts index d6b926f..80eac23 100644 --- a/src/core/objects/supplier-change-request.object.ts +++ b/src/core/objects/supplier-change-request.object.ts @@ -5,12 +5,11 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; // 填写变更后的值;新建即进入审批(srm_change_approval),审批通过后由 flow 写回供应商、银行账户或联系人。 // 变更后的值放在本单的分类型字段里(表单可校验、退回后可修改),before_value / after_value 是 flow 写入的 // json 快照,供审计对照。银行账号不进快照(BR-20:本单对采购员可读,快照没有脱敏规则)。 -// 供应商、类型和要改的对象只在新建时填写(三个状态覆盖了所有已建记录)。服务端新建时不看 readonlyWhen, -// 新建表单却会按默认值(状态「审批中」、审批结论为空)把字段锁住,所以两条都先判断记录已保存(有 id)。 -// 临时绕法(objectstack#25584),平台修复后去掉 id 判断 -const created = P`has(record.id) && record.id != null && (record.status == 'in_review' || record.status == 'applied' || record.status == 'rejected')`; +// 供应商、类型和要改的对象只在新建时填写(三个状态覆盖了所有已建记录)。服务端和新建表单在新建时都不看 +// readonlyWhen(新建表单 18.3.0 起,objectui#13436) +const created = P`record.status == 'in_review' || record.status == 'applied' || record.status == 'rejected'`; // 变更内容只在新建时和被退回修改期间可改;审批中平台锁定记录,审结后只读 -const proposal = P`has(record.id) && record.id != null && record.approval_status != 'returned'`; +const proposal = P`record.approval_status != 'returned'`; export const SupplierChangeRequest = ObjectSchema.create({ name: 'srm_supplier_change_request', diff --git a/src/core/objects/supplier-disposition.object.ts b/src/core/objects/supplier-disposition.object.ts index 171b833..82126c8 100644 --- a/src/core/objects/supplier-disposition.object.ts +++ b/src/core/objects/supplier-disposition.object.ts @@ -9,13 +9,12 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; // 核对后经「提交审批」(srm_disposition_submit)进入审批,或经「作废」(srm_disposition_cancel)放弃。 // 提交时同一套状态校验再跑一遍(供应商状态可能在草稿期间变了)。提交人记入 submitted_by 并接管记录所有权, // 审批 flow 以它为发起人(手工新建的处置单没有 submitted_by,发起人是新建人)。 -// 供应商、动作、范围和品类只在新建时填写。服务端新建时不看 readonlyWhen,新建表单却会按默认值 -// (状态「审批中」)把字段锁住,所以先判断记录已保存(有 id)。 -// 临时绕法(objectstack#25584),平台修复后去掉 id 判断 +// 供应商、动作、范围和品类只在新建时填写:记录一经保存(有 id)即只读。服务端和新建表单在新建时都不看 +// readonlyWhen(新建表单 18.3.0 起,objectui#13436) const created = P`has(record.id) && record.id != null`; // 原因与生效日期只在新建时和被退回修改期间可改;审批中平台锁定记录,审结后只读。 // 草稿的原因由生成它的 flow 写好(如风险事件编号与描述),要换原因就作废草稿、手工新建 -const proposal = P`has(record.id) && record.id != null && record.approval_status != 'returned'`; +const proposal = P`record.approval_status != 'returned'`; // 动作与供应商状态的组合在新建和草稿提交时各校验一次;「只能有一张审批中」在新建(非草稿)和提交时校验 const allowedActions = `has(record.supplier.status) && !(` diff --git a/src/core/security/permission-sets.ts b/src/core/security/permission-sets.ts index 14527f6..2104156 100644 --- a/src/core/security/permission-sets.ts +++ b/src/core/security/permission-sets.ts @@ -248,7 +248,8 @@ export const FinancePermissionSet = { }, }; -// 管理员能删考评单要 modifyAllRecords,它连修改一起放开(objectstack#25470):考评单字段对管理员一律只读 +// 管理员删考评单。不用 modifyAllRecords:18.3.0 起它压过同一权限集上的字段级只读(security-superuser-field-mask-inert), +// 管理员就能改分数了。改为下方 AdminPermissionSet 的两条行级策略;字段对管理员一律只读 const reviewFieldsReadOnly = Object.fromEntries(Object.keys(PerformanceReview.fields).map((field) => [`srm_performance_review.${field}`, readOnly])); // 系统管理员:维护品类与工厂等配置,执行物料导入和存量供应商、联系人、证书导入(04 §2.2) @@ -268,8 +269,8 @@ export const AdminPermissionSet = { srm_delivery_record: { ...read, allowDelete: true, modifyAllRecords: true }, // 指标库的配置与清理(04 §2.2);删除自己没建的指标要 modifyAllRecords,同上 srm_kpi: { ...write, allowDelete: true, modifyAllRecords: true }, - // 删除错误生成的考评单(评分明细随之删除)后可补跑生成;删系统生成的记录要 modifyAllRecords,同交付记录 - srm_performance_review: { ...read, allowDelete: true, modifyAllRecords: true }, + // 删除错误生成的考评单(评分明细随之删除)后可补跑生成;删系统生成的记录要组织范围的写深度,字段仍只读(见上) + srm_performance_review: { ...read, allowEdit: true, allowDelete: true }, srm_performance_score: { ...read, allowDelete: true }, // 组织管理员通常兼任 SRM 系统管理员:「开通门户」以操作人身份读门户邀请(04 §3.3) srm_supplier_invitation: read, @@ -281,6 +282,12 @@ export const AdminPermissionSet = { 'srm_supplier_certificate.status': flowOwned, ...reviewFieldsReadOnly, }, + // 考评单由系统生成、没有所有人:删除要一条准许这些行的删除策略;评分明细随主记录级联删除,平台要求对主记录有编辑权, + // 所以再给一条编辑策略。字段级权限全部只读(见上),编辑策略不让管理员改动任何字段 + rowLevelSecurity: [ + { name: 'srm_admin_review_delete', object: 'srm_performance_review', operation: 'delete', using: "review_no != ''" }, + { name: 'srm_admin_review_update', object: 'srm_performance_review', operation: 'update', using: "review_no != ''" }, + ], }; // 法务:合同与明细读改(草稿期间改条款;提交后合同只读),审批由 flow 授予(04 §2.2) diff --git a/src/core/security/portal-permission-sets.ts b/src/core/security/portal-permission-sets.ts index 1ef9b5b..24e28d4 100644 --- a/src/core/security/portal-permission-sets.ts +++ b/src/core/security/portal-permission-sets.ts @@ -28,11 +28,11 @@ const none = { allowCreate: false, allowRead: true, allowEdit: false, allowDelet const readOnly = { readable: true, editable: false }; const hidden = { readable: false, editable: false }; -// 供应商档案:门户只能改资料字段(04 §4.2),关键信息走变更单;内部评估类字段门户不可见。 -// 临时:risk_level 暂时可读——平台的默认搜索会扫描选项字段而不跳过调用者读不到的字段,隐藏它会让门户的供应商搜索 -// 整体被拒(objectstack#25672,主干已修、18.1.0 未含),升级到含修复的版本后加回隐藏 +// 供应商档案:门户只能改资料字段(04 §4.2),关键信息走变更单;内部评估类字段(含风险等级)门户不可见。 +// 18.3.0 起默认搜索只扫调用人能读的字段(objectstack#25672),隐藏 risk_level 不再让门户的供应商搜索整体被拒 const supplierEditable = new Set(['short_name', 'website', 'supplier_type', 'taxpayer_type', 'established_date']); const supplierHidden = new Set([ + 'risk_level', 'is_related_party', 'preferred', 'open_risk_event_count', diff --git a/src/core/translations/en.ts b/src/core/translations/en.ts index bd6dd99..38aac8e 100644 --- a/src/core/translations/en.ts +++ b/src/core/translations/en.ts @@ -1495,4 +1495,42 @@ export const en: TranslationData = { }, }, }, + reports: { + srm_delivery_scorecard: { + label: 'Supplier Delivery and Quality (by Period)', + description: 'Delivery lines, on-time rate, lot acceptance rate and PPM for every supplier and period', + }, + }, + positions: { + srm_procurement_director: { label: 'Procurement Director' }, + srm_category_manager: { label: 'Category Manager' }, + srm_buyer: { label: 'Buyer' }, + srm_buyer_lead: { label: 'Purchasing Supervisor' }, + srm_sqe: { label: 'SQE' }, + srm_quality_manager: { label: 'Quality Manager' }, + srm_iqc_inspector: { label: 'IQC Inspector' }, + srm_ap_accountant: { label: 'AP Accountant' }, + srm_legal_counsel: { label: 'Legal Counsel' }, + srm_evaluator: { label: 'Evaluator' }, + srm_general_manager: { label: 'General Manager' }, + srm_procurement_compliance: { label: 'Internal Control / Bid Supervision' }, + srm_admin: { label: 'SRM Administrator' }, + srm_portal_user: { label: 'Supplier Portal User' }, + srm_portal_admin: { label: 'Supplier Portal Administrator' }, + }, + permissions: { + srm_internal_base: { label: 'SRM Internal Base' }, + srm_buyer: { label: 'SRM Purchasing' }, + srm_supplier_admin: { label: 'SRM Supplier Management' }, + srm_quality: { label: 'SRM Quality' }, + srm_iqc: { label: 'SRM Incoming Inspection' }, + srm_executive: { label: 'SRM Management' }, + srm_performance: { label: 'SRM Performance Management' }, + srm_finance: { label: 'SRM Finance' }, + srm_admin: { label: 'SRM Administrator' }, + srm_legal: { label: 'SRM Legal' }, + srm_auditor: { label: 'SRM Internal Audit' }, + srm_portal_supplier: { label: 'Supplier Portal User' }, + srm_portal_admin: { label: 'Supplier Portal Administrator' }, + }, }; diff --git a/src/core/translations/index.ts b/src/core/translations/index.ts index 32edf6f..665e424 100644 --- a/src/core/translations/index.ts +++ b/src/core/translations/index.ts @@ -1,18 +1,5 @@ import { defineTranslationBundle } from '@objectstack/spec/system'; -import { KpiHelp } from '../objects/kpi.object.js'; import { en } from './en.js'; -// 临时:zh-CN 只补源语言里检查认不到的帮助文字(objectstack#25683,见 kpi.object.ts 的 KpiHelp),标签等仍以源码中文为准 -export const SrmTranslations = defineTranslationBundle({ - en, - 'zh-CN': { - objects: { - srm_kpi: { - fields: { - full_score_value: { help: KpiHelp.full_score_value }, - zero_score_value: { help: KpiHelp.zero_score_value }, - }, - }, - }, - }, -}); +// 源语言(zh-CN)就是源码里的中文标签和帮助文字,翻译包只放英文 +export const SrmTranslations = defineTranslationBundle({ en }); diff --git a/src/core/views/supplier-audit.view.ts b/src/core/views/supplier-audit.view.ts index f9b1d06..f1dfd4c 100644 --- a/src/core/views/supplier-audit.view.ts +++ b/src/core/views/supplier-audit.view.ts @@ -38,8 +38,8 @@ export const SupplierAuditViews = defineView({ data, columns: ['audit_no', 'supplier', 'audit_type', 'status'], filter: [{ field: 'status', operator: 'equals', value: 'planned' }], - // 临时:标题本应显示供应商,但日历把 lookup 值当对象渲染成「[object Object]」(objectui#13471),先用审核编号 - calendar: { startDateField: 'planned_date', titleField: 'audit_no', colorField: 'audit_type' }, + // 标题显示供应商名称(18.3.0 的控制台按 lookup 的显示名渲染,objectui#13471) + calendar: { startDateField: 'planned_date', titleField: 'supplier', colorField: 'audit_type' }, }, admission: { label: '准入审核', diff --git a/src/core/views/supplier.view.ts b/src/core/views/supplier.view.ts index 0593608..d2397ee 100644 --- a/src/core/views/supplier.view.ts +++ b/src/core/views/supplier.view.ts @@ -80,7 +80,7 @@ export const SupplierViews = defineView({ filter: [{ field: 'preferred', operator: 'equals', value: true }], sort: [{ field: 'latest_score', order: 'desc' }], }, - // 同一批导入里的两位主联系人校验规则拦不住(objectstack#25477),在这里人工处理 + // 兜底视图:18.3.0 起同一批导入里的第二位主联系人也被校验规则拦下(objectstack#25579),正常不会有数据 primary_contact_conflict: { label: '主联系人重复', type: 'grid', diff --git a/test/contract.test.ts b/test/contract.test.ts index ad61199..ef45bb1 100644 --- a/test/contract.test.ts +++ b/test/contract.test.ts @@ -1,6 +1,7 @@ import { readFileSync } from 'node:fs'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { uploadedFile } from './support/files'; import config from '../objectstack.config'; import { Category, Contract, ContractLine, Supplier } from '../src/core/objects'; @@ -57,13 +58,7 @@ describe('contract', () => { return stack.flows.resume({ flowName: flow, runId: started.runId }, {}, { as }); }; const toSigning = (id: string) => stack.hooks.run(Contract.name, 'update', { id, status: 'signing' }, { system: true }); - const signedFile = async () => { - const name = `signed-${++seq}.pdf`; - const [file] = await stack.seed('sys_file', [ - { key: `attachments/${name}`, name, mime_type: 'application/pdf', size: 1, scope: 'attachments', status: 'committed' }, - ]); - return String(file!.id); - }; + const signedFile = (as?: string) => uploadedFile(stack, as, 'signed'); // 系统身份把一份合同推进到生效(跳过界面),用于到期与续签的用例 const activeContract = async (doc: Record) => { const created = await draft(manager.token, doc); @@ -202,7 +197,7 @@ describe('contract', () => { expect(res.status, await res.clone().text()).toBe(200); } expect((await contract(created.id)).status).toBe('signing'); - await stack.hooks.run(Contract.name, 'update', { id: created.id, signed_file: await signedFile(), signed_date: day(0) }, { as: buyer.token }); + await stack.hooks.run(Contract.name, 'update', { id: created.id, signed_file: await signedFile(buyer.token), signed_date: day(0) }, { as: buyer.token }); await runScreen('srm_contract_activate', buyer.token, created.id); expect(await contract(created.id)).toMatchObject({ status: 'active', signed_date: day(0) }); }); diff --git a/test/delivery-record.test.ts b/test/delivery-record.test.ts index f97d69f..2ee0241 100644 --- a/test/delivery-record.test.ts +++ b/test/delivery-record.test.ts @@ -218,14 +218,14 @@ describe('delivery record', () => { 'required', // 6 不合格没有严重度 'required', // 7 不合格没有不良数 'rule_violation', // 8 不良数大于实收数 - 'created', // 9 表格带了期间、准时两列:被忽略,按实到日期重算 - 'UNIQUE_VIOLATION', // 10 同一张表里重复的行 + 'DUPLICATE_MATCH_KEY', // 9、10 同一张表里重复的行:18.3.0 起两行都不写(objectstack#26133) + 'DUPLICATE_MATCH_KEY', 'required', // 11 没有实到日期 'created', // 12 数量带千分位「1,200」 'min_value', // 13 负数(退货行) 'reference_not_found', // 14 工厂写简称「一厂」 'required', // 15 没有收货单号 - 'created', // 16 物料列写物料名称:按名称找到 + 'created', // 16 物料列写物料名称:按名称找到;表格带了期间、准时两列(值是错的):被忽略,按实到日期重算 'created', // 17 检验结论带尾随空格 'reference_not_found', // 18 物料号小写 ]); @@ -233,8 +233,9 @@ describe('delivery record', () => { expect(dotted!.required_date).toBe('2026-10-08'); const [compact] = await stack.rows(DeliveryRecord.name, { erp_receipt_no: '5000090002' }); expect([compact!.required_date, compact!.promised_date, compact!.receipt_date]).toEqual(['2026-10-08', '2026-10-08', '2026-10-09']); - const [ignored] = await stack.rows(DeliveryRecord.name, { erp_receipt_no: '5000090009' }); - expect([ignored!.period, ignored!.on_time]).toEqual(['2026-10', false]); + expect(await stack.rows(DeliveryRecord.name, { erp_receipt_no: '5000090009' })).toEqual([]); + const [ignored] = await stack.rows(DeliveryRecord.name, { erp_receipt_no: '5000090015' }); + expect([ignored!.period, ignored!.on_time]).toEqual(['2026-10', true]); const [thousands] = await stack.rows(DeliveryRecord.name, { erp_receipt_no: '5000090011' }); expect(thousands!.received_quantity).toBe(1200); }); diff --git a/test/import-idempotency.test.ts b/test/import-idempotency.test.ts index 3573973..e93bd54 100644 --- a/test/import-idempotency.test.ts +++ b/test/import-idempotency.test.ts @@ -171,8 +171,9 @@ describe('legacy supplier import', () => { expect(certificates.results[0]!.error).toMatch(/Valid To|有效期至/i); }); - // 同一批里的第二位主联系人校验规则看不到(objectstack#25477),由「主联系人重复」视图兜底 - it('surfaces two primary contacts imported in the same sheet', async () => { + // 同一批里的第二位主联系人:18.3.0 起校验规则能数到同一批里先写入的行(objectstack#25579),第二行被拒; + // 「主联系人重复」视图保留作兜底 + it('refuses a second primary contact imported in the same sheet', async () => { await importSuppliers('公司全称,国家 / 地区,统一社会信用代码,存量导入\n示例双主联系人有限公司,中国,91000000MA0LEGAC31,是\n'); const contacts = await importContacts( [ @@ -181,11 +182,11 @@ describe('legacy supplier import', () => { '示例双主联系人有限公司,示例乙,b@shuangzhu.example.com,是', ].join('\n'), ); - expect(contacts.errors).toBe(0); + expect(contacts.results.map((r) => r.ok), JSON.stringify(contacts)).toEqual([true, false]); + expect(contacts.results[1]!.error).toMatch(/primary contact|主联系人/i); const supplier = await supplierByName('示例双主联系人有限公司'); - expect(supplier.primary_contact_count).toBe(2); - const conflicts = await stack.rows(Supplier.name, { primary_contact_count: { $gt: 1 } }, { as: importer }); - expect(conflicts.map((s) => s.name)).toEqual(['示例双主联系人有限公司']); + expect(supplier.primary_contact_count).toBe(1); + expect(await stack.rows(Supplier.name, { primary_contact_count: { $gt: 1 } }, { as: importer })).toEqual([]); }); it('keeps the legacy flag out of a category manager\'s hands', async () => { diff --git a/test/import-trial.test.ts b/test/import-trial.test.ts index 4a6d6a3..e2355f1 100644 --- a/test/import-trial.test.ts +++ b/test/import-trial.test.ts @@ -6,7 +6,8 @@ import { Supplier, SupplierCertificate, SupplierContact } from '../src/core/obje // 存量导入试导(#10 出口条件):data/samples/legacy-trial/ 是模拟的「脏」台账,覆盖重复行、格式错误的 // 信用代码、混合日期格式、选项别名、缺字段、同表两位主联系人等常见问题。这里逐行钉住平台给出的结果, -// 结论写在导入说明 §6。平台修复 objectstack#25476 / #25491 / #25493 / #26133 后这里的预期会变,届时同步更新说明。 +// 结论写在导入说明 §6。18.3.0 起同一文件里匹配键相同的行一律不写(objectstack#26133),所以先按原样导一次供应商 +// 钉住报错,再按清洗清单 §6.2 第 5 条去掉重复行重导,联系人和证书在清洗后的供应商上导入。 describe('legacy import trial with a messy ledger', () => { let stack: VerifyStack; let importer: string; @@ -16,10 +17,16 @@ describe('legacy import trial with a messy ledger', () => { type Described = { name: string; fields: Record }; const sheet = (file: string) => readFileSync(new URL(`../data/samples/legacy-trial/${file}`, import.meta.url), 'utf8'); - const run = async (object: Described, file: string, matchFields: string[], skipBlankMatchKey = false) => { + // 清洗清单 §6.2 第 5 条:同一家只留一行。去掉的是表里的第 2、11 行数据(华东紧固带尾随空格的一行、境外重复行) + const withoutRows = (csv: string, rows: number[]) => + csv + .split('\n') + .filter((_, i) => !rows.includes(i)) + .join('\n'); + const run = async (object: Described, csv: string, matchFields: string[], skipBlankMatchKey = false) => { const res = await stack.apiAs(importer, 'POST', `/data/${object.name}/import`, { format: 'csv', - csv: sheet(file), + csv, mapping: Object.fromEntries(Object.entries(object.fields).map(([name, field]) => [String(field.label), name])), writeMode: 'upsert', matchFields, @@ -34,6 +41,8 @@ describe('legacy import trial with a messy ledger', () => { // 每行的结果:成功写 action,失败或跳过写 code const outcome = (result: ImportResult) => result.results.map((r) => (r.action === 'created' || r.action === 'updated' ? r.action : r.code)); + let rawDomestic: ImportResult; + let rawOverseas: ImportResult; let domestic: ImportResult; let overseas: ImportResult; let contacts: ImportResult; @@ -46,10 +55,13 @@ describe('legacy import trial with a messy ledger', () => { const org = context.tenantId ? { organization_id: String(context.tenantId) } : {}; await stack.seed('sys_user_position', [{ user_id: String(context.userId), position: 'srm_admin', ...org }]); - domestic = await run(Supplier, 'srm_supplier.csv', ['credit_code'], true); - overseas = await run(Supplier, 'srm_supplier.csv', ['country', 'registration_no'], true); - contacts = await run(SupplierContact, 'srm_supplier_contact.csv', ['supplier', 'email']); - certificates = await run(SupplierCertificate, 'srm_supplier_certificate.csv', ['supplier', 'cert_type', 'cert_no']); + rawDomestic = await run(Supplier, sheet('srm_supplier.csv'), ['credit_code'], true); + rawOverseas = await run(Supplier, sheet('srm_supplier.csv'), ['country', 'registration_no'], true); + const cleaned = withoutRows(sheet('srm_supplier.csv'), [2, 11]); + domestic = await run(Supplier, cleaned, ['credit_code'], true); + overseas = await run(Supplier, cleaned, ['country', 'registration_no'], true); + contacts = await run(SupplierContact, sheet('srm_supplier_contact.csv'), ['supplier', 'email']); + certificates = await run(SupplierCertificate, sheet('srm_supplier_certificate.csv'), ['supplier', 'cert_type', 'cert_no']); }); afterAll(async () => { @@ -57,9 +69,9 @@ describe('legacy import trial with a messy ledger', () => { }); it('reports supplier rows that need cleaning', () => { - expect(outcome(domestic)).toEqual([ - 'created', // 1 - 'UNIQUE_VIOLATION', // 2 同一家重复一行(名称带尾随空格) + expect(outcome(rawDomestic)).toEqual([ + 'DUPLICATE_MATCH_KEY', // 1、2 同一家写了两行(第二行名称带尾随空格):18.3.0 起两行都不写(objectstack#26133) + 'DUPLICATE_MATCH_KEY', 'rule_violation', // 3 信用代码小写(日期 2011.07.01 从 18.1.0 起能识别) 'max_length', // 4 信用代码带连字符(日期 20090615 能识别) 'invalid_date', // 5 2018-02-30 @@ -72,25 +84,31 @@ describe('legacy import trial with a messy ledger', () => { 'BLANK_MATCH_KEY', 'BLANK_MATCH_KEY', ]); - expect(outcome(overseas).slice(9)).toEqual([ - 'created', // 10 - 'UNIQUE_VIOLATION', // 11 境外重复行 + expect(outcome(rawOverseas).slice(9)).toEqual([ + 'DUPLICATE_MATCH_KEY', // 10、11 境外重复行,同样两行都不写 + 'DUPLICATE_MATCH_KEY', 'BLANK_MATCH_KEY', // 12 境外缺注册号:两遍都被跳过 'created', // 13 ]); }); + it('imports the suppliers once the duplicate rows are removed', () => { + // 清洗后的表少了两行:华东紧固(第 1 行)、Example Precision(第 9 行)建档,新材料(第 8 行)上一轮已建,这次更新 + expect(outcome(domestic)[0]).toBe('created'); + expect(outcome(domestic)[7]).toBe('updated'); + expect(outcome(overseas).slice(8)).toEqual(['created', 'BLANK_MATCH_KEY', 'updated']); + }); + it('fails contacts and certificates of suppliers that did not import', () => { - expect(contacts.results.filter((r) => r.code === 'reference_not_found')).toHaveLength(6); + expect(contacts.results.filter((r) => r.code === 'reference_not_found'), JSON.stringify(contacts.results)).toHaveLength(6); expect(certificates.results.find((r) => r.row === 5)!.code).toBe('reference_not_found'); // 精工模具没导入 }); - // ISO9001 从 18.1.0 起按选项值接受;它和上一行是同一张证书,同一文件里两行匹配键相同却建成两条(objectstack#26133) - it('writes a certificate listed twice in one sheet twice', async () => { - expect([2, 3].map((n) => certificates.results.find((r) => r.row === n)!.action)).toEqual(['created', 'created']); + // ISO9001 从 18.1.0 起按选项值接受,它和上一行是同一张证书:18.3.0 起同一文件里匹配键相同的两行都不写(objectstack#26133) + it('refuses a certificate listed twice in one sheet', async () => { + expect([2, 3].map((n) => certificates.results.find((r) => r.row === n)!.code)).toEqual(['DUPLICATE_MATCH_KEY', 'DUPLICATE_MATCH_KEY']); const [supplier] = await stack.rows(Supplier.name, { credit_code: '91000000MA0TRIAL01' }, { as: importer }); - const iso = await stack.rows(SupplierCertificate.name, { supplier: supplier!.id, cert_type: 'iso9001' }, { as: importer }); - expect(iso.map((c) => c.cert_no)).toEqual(['EX-Q-1001', 'EX-Q-1001']); + expect(await stack.rows(SupplierCertificate.name, { supplier: supplier!.id, cert_type: 'iso9001' }, { as: importer })).toEqual([]); }); it('keeps a certificate with no expiry date as never expiring', async () => { @@ -100,9 +118,10 @@ describe('legacy import trial with a messy ledger', () => { expect(license!.valid_to ?? null).toBeNull(); }); - // 华东紧固:同表两位主联系人;新材料:同一人邮箱大小写不同写了两行,建成两个主联系人(匹配区分大小写) - it('surfaces two primary contacts from the same sheet for review', async () => { - const conflicts = await stack.rows(Supplier.name, { primary_contact_count: { $gt: 1 } }, { as: importer }); - expect(conflicts.map((s) => s.name).sort()).toEqual(['示例华东紧固件有限公司', '示例新材料科技有限公司'].sort()); + // 华东紧固:同表两位主联系人;新材料:同一人邮箱大小写不同写了两行(匹配区分大小写,是两个联系人)。 + // 18.3.0 起同一批里的第二位主联系人也被校验规则拦下(objectstack#25579),不再进「主联系人重复」视图 + it('refuses the second primary contact from the same sheet', async () => { + expect([2, 9].map((n) => contacts.results.find((r) => r.row === n)!.code)).toEqual(['rule_violation', 'rule_violation']); + expect(await stack.rows(Supplier.name, { primary_contact_count: { $gt: 1 } }, { as: importer })).toEqual([]); }); }); diff --git a/test/performance-reassign.test.ts b/test/performance-reassign.test.ts index 4cd90db..052e600 100644 --- a/test/performance-reassign.test.ts +++ b/test/performance-reassign.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { triggerFlow } from './support/flows'; import config from '../objectstack.config'; import { Category, DeliveryRecord, Kpi, Material, PerformanceReview, PerformanceScore, Supplier, SupplierCategory } from '../src/core/objects'; @@ -127,7 +128,7 @@ describe('performance scorer reassignment for recusal', () => { // 自动算分的评分项没有评分人:按钮不显示,直接启动 flow 也拒绝 expect(lines.OTD).toMatchObject({ scored: true, score: 100 }); - const auto = (await stack.flows.run('srm_score_reassign', { recordId: String(lines.OTD!.id) }, { as: director.token })) as FlowResult; + const auto = (await triggerFlow(stack, director.token, 'srm_score_reassign', 'srm_performance_score', String(lines.OTD!.id))) as FlowResult; expect(auto).toMatchObject({ status: 'refused', refusalMessage: expect.stringContaining('scored automatically') }); }); diff --git a/test/portal-invitation.test.ts b/test/portal-invitation.test.ts index 4b2c422..93a3ebc 100644 --- a/test/portal-invitation.test.ts +++ b/test/portal-invitation.test.ts @@ -1,5 +1,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { NOT_FOUND, triggerFlow } from './support/flows'; +import { uploadedFile } from './support/files'; import { cel, composeStacks, defineStack } from '@objectstack/spec'; import { stacks } from '../objectstack.composition'; @@ -76,11 +78,8 @@ describe('portal invitation and opening (v0.6 U3)', () => { const positionsOf = async (who: string) => (await ql.find('sys_user_position', { where: { user_id: uid[who] }, ...SYS })).map((p: Row) => String(p.position)).sort(); - const attachment = async () => { - const name = `invitation-${++seq}.pdf`; - const [file] = await stack.seed('sys_file', [{ key: `attachments/${name}`, name, mime_type: 'application/pdf', size: 1, scope: 'attachments', status: 'committed' }]); - return String(file!.id); - }; + // 授权书由品类经理上传(邀请以品类经理身份新建) + const attachment = () => uploadedFile(stack, token.manager, 'invitation'); // 品类经理先登记潜在供应商与联系人(总监裁定 2026-11-02:没有供应商档案就无法隔离) const supplierWithContact = async (label: string, extra: Row = {}) => { const n = ++seq; @@ -262,7 +261,7 @@ describe('portal invitation and opening (v0.6 U3)', () => { // 按钮只给有门户访问能力的人;直接触发时 flow 只认调用者自己的名册行 it('refuses an employee: the supplier comes from the caller’s own roster row', async () => { await expect(submitApplication(token.manager, X.supplier)).rejects.toThrow(/srm_portal_access/); - refused((await stack.flows.run('srm_application_submit', { recordId: X.supplier }, { as: token.manager })) as Row, /submitted by invited suppliers/); + refused(await triggerFlow(stack, token.manager, 'srm_application_submit', 'srm_supplier', X.supplier), /submitted by invited suppliers/); }); // 直接触发 flow(不经记录页按钮)也一样:系统身份的 flow 只认调用者在该供应商上的名册行 @@ -270,7 +269,8 @@ describe('portal invitation and opening (v0.6 U3)', () => { // 按钮入口:别家的供应商对门户不可见,平台答「不存在」或 flow 拒绝 const button = (await submitApplication(token.supA, X.supplier).catch((e) => ({ status: 'error', error: e }))) as Row; expect(['error', 'refused'], JSON.stringify(button)).toContain(button.status); - refused((await stack.flows.run('srm_application_submit', { recordId: X.supplier }, { as: token.supA })) as Row, /submitted by invited suppliers/); + // 直接触发:18.3.0 起入口先以调用人身份读供应商(objectstack#29050),别家供应商读不到,答「不存在」,不产生运行 + await expect(triggerFlow(stack, token.supA, 'srm_application_submit', 'srm_supplier', X.supplier)).rejects.toThrow(NOT_FOUND); expect(await stack.rows('srm_supplier_application', { supplier: X.supplier })).toHaveLength(0); expect((await row('srm_supplier_invitation', String(invitationX.id))).application ?? null).toBeNull(); }); diff --git a/test/portal-isolation.test.ts b/test/portal-isolation.test.ts index 5609a57..d3386dc 100644 --- a/test/portal-isolation.test.ts +++ b/test/portal-isolation.test.ts @@ -1,5 +1,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { NOT_FOUND, triggerFlow } from './support/flows'; +import { uploadedFile } from './support/files'; import config from '../objectstack.config'; import * as objects from '../src/core/objects'; import { SupplierContact } from '../src/core/objects'; @@ -168,11 +170,7 @@ describe('portal isolation (04 §3.7)', () => { const sys = async (object: string, doc: Row) => (await stack.hooks.run(object, 'insert', doc, { system: true })) as Row; const seed = async (object: string, doc: Row) => (await stack.seed(object, [doc]))[0] as Row; // 证书扫描件:一条文件记录只能被一个字段引用,每张证书各建一条 - const scan = async () => { - const name = `portal-scan-${++seq}.pdf`; - const [file] = await stack.seed('sys_file', [{ key: `attachments/${name}`, name, mime_type: 'application/pdf', size: 1, scope: 'attachments', status: 'committed' }]); - return String(file?.id); - }; + const scan = (as?: string) => uploadedFile(stack, as, 'portal-scan'); const assign = (userId: string, position: string) => stack.seed('sys_user_position', [{ user_id: userId, position, ...org }]); // 门户用户:注册 → 组织角色改为 external → 任命门户岗位 → 在供应商联系人上登记 portal_user @@ -345,6 +343,12 @@ describe('portal isolation (04 §3.7)', () => { expect(await names(await stack.apiAs(a.token, 'GET', '/data/srm_supplier'))).toEqual([A.name]); const search = await stack.apiAs(a.token, 'GET', `/data/srm_supplier?search=${encodeURIComponent('示例门户隔离')}`); expect(await names(search)).toEqual([A.name]); + // 风险等级对门户隐藏,搜索照常(objectstack#25672 在 18.3.0 修复前,隐藏它会让搜索整体被拒) + await stack.hooks.run('srm_supplier', 'update', { id: A.supplier, risk_level: 'medium' }, { system: true }); + expect(await names(await stack.apiAs(a.token, 'GET', `/data/srm_supplier?search=${encodeURIComponent('示例门户隔离')}`))).toEqual([A.name]); + const [own] = (await stack.rows('srm_supplier', { id: A.supplier }, { as: a.token })) as Row[]; + expect(own).toBeDefined(); + expect(own!.risk_level ?? null).toBeNull(); expect(await names(await stack.apiAs(a.token, 'POST', '/data/srm_supplier/query', {}))).toEqual([A.name]); for (const object of PORTAL_OBJECTS) { const res = await stack.apiAs(a.token, 'GET', `/data/${object}`); @@ -423,25 +427,26 @@ describe('portal isolation (04 §3.7)', () => { // 系统身份运行的 screen flow 自己做隔离检查:别家的整改单、考评单一律拒绝,记录不变 it('refuses the claim and appeal flows on another supplier\'s records', async () => { - // 两个入口都试:记录页按钮(action)和直接触发 flow(REST trigger,不经按钮所在页面的读取)。flow 以系统身份 - // 读记录,所以隔离只能靠 flow 自己的名册检查 + // 两个入口都试:记录页按钮(action)和直接触发 flow(REST trigger,不经按钮所在页面的读取)。18.3.0 起两个入口 + // 都先以调用人身份读记录(objectstack#29050),别家的记录读不到,flow 不启动;flow 自己的名册检查仍保留,作第二道防线 + // (内部员工等读得到记录的调用人由它拒绝,见 portal-self-service / portal-invitation) const run = async (who: Portal, object: string, flow: string, recordId: string, input: Row) => { const finish = async (start: Promise) => { - let res = (await start.catch((e) => ({ status: 'error', error: e }))) as Row; + let res = (await start.catch((e) => ({ status: /answered 404/.test(String(e)) ? 'not_found' : 'error', error: e }))) as Row; for (let i = 0; i < 3 && res.status === 'paused' && res.runId; i++) { res = (await stack.flows.resume({ flowName: flow, runId: res.runId }, input, { as: who.token }).catch((e) => ({ status: 'error', error: e }))) as Row; } return String(res.status ?? (res.success === true ? 'completed' : 'failed')); }; return { - // 按钮入口:记录对门户不可见,平台答「不存在」;直接触发:flow 的名册检查拒绝 + // 按钮入口:记录对门户不可见,平台答「不存在」;直接触发:入口答 404「不存在」 status: [ await finish(stack.actions.run(object, flow, { as: who.token, recordId })), - await finish(stack.flows.run(flow, { recordId }, { as: who.token })), + await finish(triggerFlow(stack, who.token, flow, object, recordId)), ], }; }; - const REFUSED = { status: ['error', 'refused'] }; + const REFUSED = { status: ['error', 'not_found'] }; const foreign = String((await sys('srm_quality_issue', { supplier: B.supplier, severity: 'major', problem_description: '示例索赔隔离', claim_amount: 100, claim_status: 'proposed' })).id); B.visible.get('srm_quality_issue')!.push(foreign); expect(await run(a, 'srm_quality_issue', 'srm_claim_respond', foreign, { decision: 'accept' })).toEqual(REFUSED); @@ -506,7 +511,7 @@ describe('portal isolation (04 §3.7)', () => { const created = await stack.hooks.run( 'srm_supplier_certificate', 'insert', - { supplier: A.supplier, cert_type: 'iso14001', cert_no: `ISO14-${++seq}`, valid_to: '2030-01-01', attachment: await scan() }, + { supplier: A.supplier, cert_type: 'iso14001', cert_no: `ISO14-${++seq}`, valid_to: '2030-01-01', attachment: await scan(a.token) }, { as: a.token }, ); expect(created.verified).toBe(false); @@ -548,12 +553,11 @@ describe('portal isolation (04 §3.7)', () => { const bChange = B.visible.get('srm_supplier_change_request')![0]!; await expect(stack.hooks.run('srm_supplier_change_request', 'update', { id: bChange, reason: '冒名' }, { as: aAdmin.token })).rejects.toBeTruthy(); // 系统身份的 screen flow:门户管理员伪造别家的供应商、门户用户伪造别家的准入申请,名册对不上即结束 - const submit = (await stack.flows.run('srm_change_submit', { recordId: B.supplier }, { as: aAdmin.token })) as { status?: string }; - expect(submit.status).toBe('refused'); - const resubmit = (await stack.flows.run('srm_application_resubmit', { recordId: B.visible.get('srm_supplier_application')![0]! }, { as: a.token })) as { status?: string }; - expect(resubmit.status).toBe('refused'); + // 18.3.0 起入口先以调用人身份读记录(objectstack#29050),别家的记录读不到,答「不存在」,flow 不启动 + await expect(triggerFlow(stack, aAdmin.token, 'srm_change_submit', 'srm_supplier', B.supplier)).rejects.toThrow(NOT_FOUND); + await expect(triggerFlow(stack, a.token, 'srm_application_resubmit', 'srm_supplier_application', B.visible.get('srm_supplier_application')![0]!)).rejects.toThrow(NOT_FOUND); // 核验证书只给 SQE / 质量经理 - const verify = (await stack.flows.run('srm_certificate_verify', { recordId: own }, { as: aAdmin.token })) as { status?: string }; + const verify = await triggerFlow(stack, aAdmin.token, 'srm_certificate_verify', 'srm_supplier_certificate', own); expect(verify.status).toBe('refused'); expect((await stack.rows('srm_supplier_certificate', { id: bCert }))[0]!.valid_to).not.toBe('2041-01-01'); expect((await stack.rows('srm_supplier_contact', { id: bContact }))[0]!.mobile ?? null).toBeNull(); diff --git a/test/portal-platform.test.ts b/test/portal-platform.test.ts index 9803171..eac0fb6 100644 --- a/test/portal-platform.test.ts +++ b/test/portal-platform.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { uploadedFile } from './support/files'; import { cel, composeStacks, defineStack } from '@objectstack/spec'; import { Field } from '@objectstack/spec/data'; import { stacks } from '../objectstack.composition'; @@ -153,19 +154,13 @@ describe('portal platform checks (v0.6 U1, V1–V5)', () => { }; const hold = (who: string, position: string) => stack.seed('sys_user_position', [{ user_id: uid[who], position, organization_id: orgId }]); - const attachment = async () => { - const name = `portal-probe-${++seq}.pdf`; - const [file] = await stack.seed('sys_file', [ - { key: `attachments/${name}`, name, mime_type: 'application/pdf', size: 1, scope: 'attachments', status: 'committed' }, - ]); - return String(file!.id); - }; - const certificateDoc = async (supplierId: string) => ({ + const attachment = (as?: string) => uploadedFile(stack, as, 'portal-probe'); + const certificateDoc = async (supplierId: string, uploader?: string) => ({ supplier: supplierId, cert_type: 'iso14001', valid_from: '2025-01-01', valid_to: '2027-12-31', - attachment: await attachment(), + attachment: await attachment(uploader), }); const get = async (who: string, path: string) => { const res = await stack.apiAs(token[who], 'GET', path); @@ -201,7 +196,7 @@ describe('portal platform checks (v0.6 U1, V1–V5)', () => { { as: token.admin }, ); supplier[key] = String(row.id); - certificate[key] = String((await stack.hooks.run(SupplierCertificate.name, 'insert', await certificateDoc(supplier[key]), { as: token.admin })).id); + certificate[key] = String((await stack.hooks.run(SupplierCertificate.name, 'insert', await certificateDoc(supplier[key], token.admin), { as: token.admin })).id); await stack.hooks.run(SupplierContact.name, 'insert', { supplier: supplier[key], name: `联系人 ${key}`, email: `contact-${key}@supplier.test` }, { as: token.admin }); } @@ -404,11 +399,11 @@ describe('portal platform checks (v0.6 U1, V1–V5)', () => { }); it('adds a certificate to its own supplier, at the engine and at REST, and not to another supplier', async () => { - const own = await stack.hooks.run(SupplierCertificate.name, 'insert', await certificateDoc(supplier.a), asA()); + const own = await stack.hooks.run(SupplierCertificate.name, 'insert', await certificateDoc(supplier.a, token.supA), asA()); expect(own.supplier).toBe(supplier.a); - const rest = await stack.apiAs(token.supA, 'POST', `/data/${SupplierCertificate.name}`, await certificateDoc(supplier.a)); + const rest = await stack.apiAs(token.supA, 'POST', `/data/${SupplierCertificate.name}`, await certificateDoc(supplier.a, token.supA)); expect(rest.status, await rest.clone().text()).toBeLessThan(300); - await expect(stack.hooks.run(SupplierCertificate.name, 'insert', await certificateDoc(supplier.b), asA())).rejects.toMatchObject({ + await expect(stack.hooks.run(SupplierCertificate.name, 'insert', await certificateDoc(supplier.b, token.supA), asA())).rejects.toMatchObject({ code: 'PERMISSION_DENIED', }); }); @@ -439,7 +434,7 @@ describe('portal platform checks (v0.6 U1, V1–V5)', () => { it('needs edit on the parent: with read-only access to its supplier it can neither add nor edit a certificate', async () => { await expect( - stack.hooks.run(SupplierCertificate.name, 'insert', await certificateDoc(supplier.c), { as: token.supC }), + stack.hooks.run(SupplierCertificate.name, 'insert', await certificateDoc(supplier.c, token.supC), { as: token.supC }), ).rejects.toMatchObject({ code: 'PERMISSION_DENIED', message: expect.stringContaining('master') }); await expect( stack.hooks.run(SupplierCertificate.name, 'update', { id: certificate.c, valid_to: '2030-01-01' }, { as: token.supC }), diff --git a/test/portal-response.test.ts b/test/portal-response.test.ts index 2d383d1..b335934 100644 --- a/test/portal-response.test.ts +++ b/test/portal-response.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { uploadedFile } from './support/files'; import config from '../objectstack.config'; import { Category, Material, PerformanceReview, PerformanceScore, QualityIssue, Supplier, SupplierContact } from '../src/core/objects'; @@ -33,11 +34,7 @@ describe('portal SCAR reply, claim and performance appeal', () => { return { token, id }; }; const reply = (as: Person, id: string, doc: Row) => stack.hooks.run(QualityIssue.name, 'update', { id, ...doc }, { as: as.token }); - const file = async () => { - const name = `portal-evidence-${++seq}.pdf`; - const [created] = await stack.seed('sys_file', [{ key: `attachments/${name}`, name, mime_type: 'application/pdf', size: 1, scope: 'attachments', status: 'committed' }]); - return String(created?.id); - }; + const file = (as?: string) => uploadedFile(stack, as, 'portal-evidence'); // 站内信经消息服务异步投递:等到收齐 count 条(或超时)再读 const inbox = async (userId: string, recordId: string, count = 1) => { const read = async () => (await stack.rows('sys_inbox_message', { user_id: userId })).filter((m) => String(m.action_url).endsWith(`/${recordId}`)); @@ -120,7 +117,7 @@ describe('portal SCAR reply, claim and performance appeal', () => { expect(await inbox(sqe.id, id)).toHaveLength(1); // D4–D7 与证据,提交后由负责 SQE 审核(srm_scar_review),审核期间锁定 - const evidence = await file(); + const evidence = await file(x.token); await reply(x, id, { root_cause: '示例:模具磨损未按计划更换', corrective_action: '示例:更换模具并复测首件', @@ -137,7 +134,7 @@ describe('portal SCAR reply, claim and performance appeal', () => { const [returned] = await stack.rows(QualityIssue.name, { id }, { as: x.token }); expect(returned).toMatchObject({ status: 'containment_submitted', sqe_comment: '示例:根因分析需附模具点检记录' }); - await reply(x, id, { root_cause: '示例:模具磨损未按计划更换,点检记录见附件', evidence: [await file()] }); + await reply(x, id, { root_cause: '示例:模具磨损未按计划更换,点检记录见附件', evidence: [await file(x.token)] }); await reply(x, id, { status: 'action_submitted' }); await decide(sqe, id, 'approve', '示例:同意'); expect((await row(QualityIssue.name, id)).status).toBe('verifying'); diff --git a/test/portal-self-service.test.ts b/test/portal-self-service.test.ts index 71629b4..9931a1b 100644 --- a/test/portal-self-service.test.ts +++ b/test/portal-self-service.test.ts @@ -1,5 +1,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { triggerFlow } from './support/flows'; +import { uploadedFile } from './support/files'; import config from '../objectstack.config'; // v0.6 U4 门户自助维护(03 §3 第 4 步、§5、§11,04 §3.2、§4.2):门户补证书 → SQE 核验后资质异常解除;门户管理员经 @@ -19,11 +21,7 @@ describe('portal self-service (v0.6 U4)', () => { const day = (offset: number) => new Date(Date.now() + offset * 86_400_000).toISOString().slice(0, 10); const sys = async (object: string, doc: Row) => (await stack.hooks.run(object, 'insert', doc, { system: true })) as Row; const row = async (object: string, id: string) => (await stack.rows(object, { id }))[0]!; - const scan = async () => { - const name = `self-service-scan-${++seq}.pdf`; - const [file] = await stack.seed('sys_file', [{ key: `attachments/${name}`, name, mime_type: 'application/pdf', size: 1, scope: 'attachments', status: 'committed' }]); - return String(file?.id); - }; + const scan = (as?: string) => uploadedFile(stack, as, 'self-service-scan'); const assign = (userId: string, position: string) => stack.seed('sys_user_position', [{ user_id: userId, position, ...org }]); const person = async (...positions: string[]): Promise => { const token = await stack.signUp(`ss-${positions.join('-')}-${++seq}@example.com`); @@ -54,9 +52,13 @@ describe('portal self-service (v0.6 U4)', () => { if (started.status !== 'paused' || !started.runId) return started as Row; return (await stack.flows.resume({ flowName: flow, runId: started.runId }, input, { as })) as Row; }; - // 绕过按钮直接启动 flow(任何成员都能经 trigger 入口启动系统身份的 screen flow,objectstack#25481) - const startDirect = async (as: string, flow: string, recordId: string, input: Row = {}) => { - const started = (await stack.flows.run(flow, { recordId }, { as })) as { runId?: string; status?: string }; + // 绕过按钮直接启动 flow(任何成员都能经 trigger 入口启动系统身份的 screen flow,objectstack#25481)。 + // 18.3.0 起 trigger 入口先以调用人身份读 recordId(objectstack#29050),读不到答 404,不产生运行,这里记作 not_found + const startDirect = async (as: string, object: string, flow: string, recordId: string, input: Row = {}) => { + const started = (await triggerFlow(stack, as, flow, object, recordId).catch((error) => { + if (/answered 404/.test(String(error))) return { status: 'not_found' }; + throw error; + })) as { runId?: string; status?: string }; if (started.status !== 'paused' || !started.runId) return started as Row; return (await stack.flows.resume({ flowName: flow, runId: started.runId }, input, { as })) as Row; }; @@ -152,7 +154,7 @@ describe('portal self-service (v0.6 U4)', () => { const uploaded = await stack.hooks.run( 'srm_supplier_certificate', 'insert', - { supplier: x.id, cert_type: 'iso9001', cert_no: `ISO-NEW-${seq}`, issuer: '示例认证机构', valid_from: day(-1), valid_to: day(1095), attachment: await scan() }, + { supplier: x.id, cert_type: 'iso9001', cert_no: `ISO-NEW-${seq}`, issuer: '示例认证机构', valid_from: day(-1), valid_to: day(1095), attachment: await scan(p.token) }, { as: p.token }, ); expect(await row('srm_supplier_certificate', String(uploaded.id))).toMatchObject({ verified: false, is_mandatory: true }); @@ -161,7 +163,7 @@ describe('portal self-service (v0.6 U4)', () => { await expect.poll(() => notified(String(uploaded.id), sqe.id)).toBe(true); // 门户用户自己核验不了,过期证书不能核验 - expect((await startDirect(p.token, 'srm_certificate_verify', String(uploaded.id))).status).toBe('refused'); + expect((await startDirect(p.token, 'srm_supplier_certificate', 'srm_certificate_verify', String(uploaded.id))).status).toBe('refused'); expect((await runScreen(sqe.token, 'srm_supplier_certificate', 'srm_certificate_verify', x.iso)).status).toBe('refused'); const verified = await runScreen(sqe.token, 'srm_supplier_certificate', 'srm_certificate_verify', String(uploaded.id)); @@ -279,10 +281,10 @@ describe('portal self-service (v0.6 U4)', () => { const plain = await portalUser(x.id); const pa = await portalUser(x.id, true); const input = { change_type: 'basic', reason: '冒名', new_name: '示例冒名有限公司' }; - expect((await startDirect(plain.token, 'srm_change_submit', x.id, input)).status).toBe('refused'); - expect((await startDirect(manager.token, 'srm_change_submit', x.id, input)).status).toBe('refused'); - // 门户管理员伪造别家的供应商:名册对不上 - expect((await startDirect(pa.token, 'srm_change_submit', y.id, input)).status).toBe('refused'); + expect((await startDirect(plain.token, 'srm_supplier', 'srm_change_submit', x.id, input)).status).toBe('refused'); + expect((await startDirect(manager.token, 'srm_supplier', 'srm_change_submit', x.id, input)).status).toBe('refused'); + // 门户管理员伪造别家的供应商:18.3.0 起入口读不到别家供应商,答「不存在」(objectstack#29050);flow 的名册检查是第二道防线 + expect((await startDirect(pa.token, 'srm_supplier', 'srm_change_submit', y.id, input)).status).toBe('not_found'); expect(await stack.rows('srm_supplier_change_request', { supplier: y.id })).toEqual([]); // 选别家的银行账户:变更单的校验规则拒绝 const forged = await runScreen(pa.token, 'srm_supplier', 'srm_change_submit', x.id, { @@ -307,8 +309,9 @@ describe('portal self-service (v0.6 U4)', () => { const p = await portalUser(supplier); const other = await portalUser((await qualifiedSupplier()).id); - expect((await startDirect(other.token, 'srm_application_resubmit', id, { note: '冒名' })).status).toBe('refused'); - expect((await startDirect(manager.token, 'srm_application_resubmit', id, { note: '内部' })).status).toBe('refused'); + // 别家的门户用户:入口读不到这张申请,答「不存在」(objectstack#29050);内部员工读得到,由 flow 的名册检查拒绝 + expect((await startDirect(other.token, 'srm_supplier_application', 'srm_application_resubmit', id, { note: '冒名' })).status).toBe('not_found'); + expect((await startDirect(manager.token, 'srm_supplier_application', 'srm_application_resubmit', id, { note: '内部' })).status).toBe('refused'); const run = await runScreen(p.token, 'srm_supplier_application', 'srm_application_resubmit', id, { note: '已补传 ISO 9001 证书' }); expect(run.success, JSON.stringify(run)).toBe(true); expect(await row('srm_supplier_application', id)).toMatchObject({ status: 'doc_review', supplement_note: '已补传 ISO 9001 证书' }); diff --git a/test/qualification.test.ts b/test/qualification.test.ts index 51d573d..2395fed 100644 --- a/test/qualification.test.ts +++ b/test/qualification.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { uploadedFile } from './support/files'; import config from '../objectstack.config'; import { Category, Supplier, SupplierCategory, SupplierCertificate } from '../src/core/objects'; @@ -24,14 +25,11 @@ describe('qualification monitoring', () => { { as: admin }, )).id as string; const newCertificate = async (supplier: string, validTo: string, isMandatory: boolean, certType = 'iso9001') => { - const name = `cert-${++seq}.pdf`; - const [file] = await stack.seed('sys_file', [ - { key: `attachments/${name}`, name, mime_type: 'application/pdf', size: 1, scope: 'attachments', status: 'committed' }, - ]); + const file = await uploadedFile(stack, admin, 'cert'); const cert = await stack.hooks.run( SupplierCertificate.name, 'insert', - { supplier, cert_type: certType, valid_from: day(-400), valid_to: validTo, is_mandatory: isMandatory, attachment: String(file!.id) }, + { supplier, cert_type: certType, valid_from: day(-400), valid_to: validTo, is_mandatory: isMandatory, attachment: file }, { as: admin }, ); return cert.id as string; diff --git a/test/risk-event.test.ts b/test/risk-event.test.ts index 7975aa4..00de0c1 100644 --- a/test/risk-event.test.ts +++ b/test/risk-event.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { uploadedFile } from './support/files'; import config from '../objectstack.config'; import { Category, RiskEvent, Supplier, SupplierCategory, SupplierCertificate, SupplierDisposition } from '../src/core/objects'; @@ -49,10 +50,7 @@ describe('risk events and the blacklist register', () => { expect(run.success, JSON.stringify(run)).toBe(true); }; const certificate = async (supplier: string, validTo: string, isMandatory: boolean) => { - const name = `rsk-cert-${++seq}.pdf`; - const [file] = await stack.seed('sys_file', [ - { key: `attachments/${name}`, name, mime_type: 'application/pdf', size: 1, scope: 'attachments', status: 'committed' }, - ]); + const file = await uploadedFile(stack, sqe.token, 'rsk-cert'); return ( await stack.hooks.run( SupplierCertificate.name, @@ -64,7 +62,7 @@ describe('risk events and the blacklist register', () => { valid_from: day(-400), valid_to: validTo, is_mandatory: isMandatory, - attachment: String(file!.id), + attachment: file, }, { as: sqe.token }, ) diff --git a/test/supplier.test.ts b/test/supplier.test.ts index 8d0ea05..dab45f9 100644 --- a/test/supplier.test.ts +++ b/test/supplier.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { uploadedFile } from './support/files'; import config from '../objectstack.config'; import { Category, Supplier, SupplierCategory, SupplierCertificate, SupplierContact } from '../src/core/objects'; @@ -15,13 +16,7 @@ describe('supplier master data', () => { stack.hooks.run(Supplier.name, 'insert', { name: `测试供应商 ${++seq}`, credit_code: creditCode(), ...extra }, { as: admin }); // 证书扫描件必填;一条文件记录只能被一个字段引用,所以每张证书各建一条 - const newScan = async () => { - const name = `scan-${++seq}.pdf`; - const [file] = await stack.seed('sys_file', [ - { key: `attachments/${name}`, name, mime_type: 'application/pdf', size: 1, scope: 'attachments', status: 'committed' }, - ]); - return String(file?.id); - }; + const newScan = () => uploadedFile(stack, admin, 'scan'); beforeAll(async () => { stack = await bootStack(config); diff --git a/test/support/files.ts b/test/support/files.ts new file mode 100644 index 0000000..109d710 --- /dev/null +++ b/test/support/files.ts @@ -0,0 +1,19 @@ +import type { VerifyStack } from '@objectstack/verify'; + +let seq = 0; + +/** + * 模拟一次上传,返回文件 id。文件记录归上传人(`owner_id`),和控制台上传时一样。 + * + * 18.3.0 起,写入记录时文件字段只能引用调用人自己打开得了的文件(objectstack#25692): + * 不归任何人、也没挂到任何记录上的文件会被拒绝。系统身份的写入没有调用人,不受此限, + * 所以 `token` 只在以某个用户身份写入时需要。一条文件记录只能被一个字段引用,每次各建一条。 + */ +export async function uploadedFile(stack: VerifyStack, token?: string, prefix = 'upload'): Promise { + const owner = token ? { owner_id: String((await stack.contextFor(token)).userId) } : {}; + const name = `${prefix}-${++seq}.pdf`; + const [file] = await stack.seed('sys_file', [ + { key: `attachments/${name}`, name, mime_type: 'application/pdf', size: 1, scope: 'attachments', status: 'committed', ...owner }, + ]); + return String(file!.id); +} diff --git a/test/support/flows.ts b/test/support/flows.ts new file mode 100644 index 0000000..94d5d82 --- /dev/null +++ b/test/support/flows.ts @@ -0,0 +1,25 @@ +import type { VerifyStack } from '@objectstack/verify'; + +type Row = Record; + +/** + * 以 `token` 的身份经 trigger 入口(`POST /automation/:name/trigger`)直接启动 flow,并指明记录所在对象。 + * + * 18.3.0 起,按名字启动 flow 的入口先以调用人身份读这次启动指向的记录(objectstack#29050): + * 读不到答 404、不产生运行;系统身份的 flow 只给 `recordId` 不给 `objectName` 答 400。 + * `stack.flows.run` 把参数整包放进 `params`,传不了顶层的 `objectName`,所以这里直接调接口。 + * + * 返回运行结果(与 `stack.flows.run` 相同,含 `flowName`);入口拒绝时抛出,消息里带状态码和 `code`。 + */ +export async function triggerFlow(stack: VerifyStack, token: string, flow: string, objectName: string, recordId: string, params: Row = {}): Promise { + const res = await stack.apiAs(token, 'POST', `/automation/${encodeURIComponent(flow)}/trigger`, { objectName, recordId, params }); + const body = (await res.json().catch(() => null)) as Row | null; + if (res.status >= 400) { + const error = body?.error ?? body; + throw new Error(`trigger ${flow} answered ${res.status} ${error?.code ?? ''}: ${error?.message ?? JSON.stringify(body)}`); + } + return { ...(body?.data ?? {}), flowName: flow }; +} + +/** 入口以「记录不存在」拒绝(调用人读不到这条记录)时的错误 */ +export const NOT_FOUND = /answered 404/;