diff --git a/AGENTS.md b/AGENTS.md index 8b609a9..25f10f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,6 +84,7 @@ openspec validate --all ``` 启动、隔离数据库重置、smoke 与日志读取见 [`README.md`](README.md)。迁移使用 `scripts/migrate.sh` 和显式 `DB_*` 参数;生产迁移仅按生产运行说明由维护者手工执行。 +- 测试验证:涉及迁移、Redis、部署或集成 Smoke 时,MUST 读取 [`docs/engineering/工程约束.md`](docs/engineering/工程约束.md) 的 ENG-TEST-001;维护者指定的测试环境是唯一验证面。 ## 渐进披露 diff --git a/CONTEXT.md b/CONTEXT.md deleted file mode 100644 index 379ed3b..0000000 --- a/CONTEXT.md +++ /dev/null @@ -1,32 +0,0 @@ -# 新卡管系统 - -新卡管系统保存物联网卡、设备、套餐、订单、钱包、分佣、审批与运营协作的本地业务事实,并协调外部支付、运营商和企业微信能力。 - -## 资金与审批 - -**员工代收款账单**:员工代客户完成套餐购买或充值等业务后生成的待核销记录,表示该员工经办业务形成的暂挂欠款;可由已匹配支付记录的客户付款凭证核销。 -_Avoid_: 员工账单、客户应收款、销账单 - -**核销**:公司对员工代收款账单及已匹配支付记录的客户付款凭证作出的确认,使对应暂挂欠款减少或结清的业务决定。 -_Avoid_: 客户付款、订单支付 - -**核销申请**:员工针对一笔外部付款提交的、包含凭证和一至多条账单分摊明细的审批业务单;一张申请对应一个企业微信审批实例。 -_Avoid_: 单张账单审批、付款截图 - -**核销分摊明细**:核销申请对一张员工代收款账单确认的本次核销金额。 -_Avoid_: 账单金额、付款金额 - -**账单核销状态**:员工代收款账单的结算状态,取待核销、部分核销、已核销或已关闭;与核销申请的审批状态相互独立。 -_Avoid_: 审批状态、企业微信状态 - -**审批实例**:本地保存、唯一关联一笔业务单的审批生命周期事实;企业微信是该实例的审批渠道,而非本地业务事实的替代。 -_Avoid_: 企业微信审批单 - -**交易流水号**:支付或退款渠道为一笔交易生成的外部标识;OCR 识别结果只能预填该字段,须由业务人员最终确认。 -_Avoid_: OCR 结果、系统订单号 - -**线下收款方式**:业务字典中供核销等线下付款场景选择的收款路径标识,例如某个指定微信或银行卡;它不等同于线上支付渠道枚举。 -_Avoid_: 支付方式枚举、固定收款人名单、收款账户目录 - -**业务字典**:由研发固定注册的业务分类及其由业务维护的字典项,用于稳定的业务选项;业务人员不能自行创建字典分类。 -_Avoid_: 系统配置、任意自定义字段平台 diff --git a/docs/engineering/工程约束.md b/docs/engineering/工程约束.md index 3449d9c..07292fe 100644 --- a/docs/engineering/工程约束.md +++ b/docs/engineering/工程约束.md @@ -300,3 +300,16 @@ - **Owner**:基础设施负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:新增配置或依赖升级 + +## ENG-TEST-001 +- **状态**:生效 +- **适用范围**:Agent 执行的迁移、Redis、API/Worker、部署与集成 Smoke 验证。 +- **规则**:维护者指定的测试 PostgreSQL `junhong_cmp_test`、Redis DB 6、`Iteration/8-11` 测试部署和 `cmp-test` 日志主机构成唯一测试验证面;MUST 使用该环境,不额外要求独立数据库、Redis DB 或 namespace。迁移从本地工作区以明确 `DB_*` 参数执行;测试 fixture 仅可创建、删除当前 Change 自己的记录,MUST NOT 重置整个测试库。测试部署通过 Gitea 工作流完成,SSH 仅用于日志、容器状态与受控 Smoke。 +- **理由**:同一可控测试面避免每个 Change 重复索取环境,且保留可复现的迁移、缓存、并发和部署证据。 +- **最小正例**:当前 Change 在 `junhong_cmp_test` 执行迁移 up/down/up,清理自己的 fixture,并在测试部署后读取 `cmp-test` 日志。 +- **最小反例**:以未提供额外“隔离环境”为由暂停,或重置整个测试数据库。 +- **机械检查/人工原因**:记录显式目标、迁移命令、fixture 清理范围、Redis 操作与测试部署 SHA;通过 API/Worker 日志和可观察状态核对。 +- **例外条件**:真实支付渠道和外部审批系统不作自动验证;第三方协议变更以契约文档和维护者提供的证据为准。生产仍按生产运行说明由维护者执行。 +- **Owner**:基础设施负责人 +- **最后验证日期**:2026-09-08 +- **更新触发条件**:测试库、Redis DB、部署分支、测试主机或验证授权变化 diff --git a/docs/verification/context-reset/entry-capability-requirement-matrix.json b/docs/verification/context-reset/entry-capability-requirement-matrix.json index ba0c856..9f6944c 100644 --- a/docs/verification/context-reset/entry-capability-requirement-matrix.json +++ b/docs/verification/context-reset/entry-capability-requirement-matrix.json @@ -210,7 +210,8 @@ "requirements": [ "agent-funds-commission::充值边界", "identity-access::数据范围拒绝", - "order-payment-wallet::金额单位" + "order-payment-wallet::金额单位", + "agent-funds-commission::代理在线充值可用支付方式按支付配置判定" ], "classification": "behavior" }, @@ -330,7 +331,12 @@ "capability": "asset-device", "requirements": [ "asset-device::资产业务状态", - "identity-access::数据范围拒绝" + "identity-access::数据范围拒绝", + "package-lifecycle::资产套餐层级投影", + "package-lifecycle::套餐历史整组筛选与分页", + "package-lifecycle::套餐历史稳定排序", + "package-lifecycle::物理缺失与不可展示关系区分", + "package-lifecycle::套餐历史读取范围与兼容边界" ], "classification": "behavior" }, @@ -390,7 +396,8 @@ "capability": "operations-audit", "requirements": [ "identity-access::数据范围拒绝", - "operations-audit::审计时间线" + "operations-audit::审计时间线", + "operations-audit::审计调查留存边界连续" ], "classification": "behavior" }, @@ -470,7 +477,8 @@ "capability": "operations-audit", "requirements": [ "identity-access::数据范围拒绝", - "operations-audit::审计时间线" + "operations-audit::审计时间线", + "operations-audit::审计调查留存边界连续" ], "classification": "behavior" }, @@ -1356,7 +1364,12 @@ "entry": "GET /api/c/v1/asset/package-history", "capability": "personal-customer", "requirements": [ - "personal-customer::资产归属查询" + "personal-customer::资产归属查询", + "package-lifecycle::资产套餐层级投影", + "package-lifecycle::套餐历史整组筛选与分页", + "package-lifecycle::套餐历史稳定排序", + "package-lifecycle::物理缺失与不可展示关系区分", + "package-lifecycle::套餐历史读取范围与兼容边界" ], "classification": "behavior" }, @@ -1514,7 +1527,8 @@ "capability": "agent-open-api", "requirements": [ "agent-open-api::开放接口数据范围", - "agent-open-api::开放接口认证" + "agent-open-api::开放接口认证", + "agent-open-api::代理开放接口查询不触发可靠卡观测" ], "classification": "behavior" }, @@ -1524,7 +1538,8 @@ "capability": "agent-open-api", "requirements": [ "agent-open-api::开放接口数据范围", - "agent-open-api::开放接口认证" + "agent-open-api::开放接口认证", + "agent-open-api::代理开放接口查询不触发可靠卡观测" ], "classification": "behavior" }, @@ -1534,7 +1549,8 @@ "capability": "agent-open-api", "requirements": [ "agent-open-api::开放接口数据范围", - "agent-open-api::开放接口认证" + "agent-open-api::开放接口认证", + "agent-open-api::代理开放接口查询不触发可靠卡观测" ], "classification": "behavior" }, @@ -1544,7 +1560,8 @@ "capability": "agent-open-api", "requirements": [ "agent-open-api::开放接口数据范围", - "agent-open-api::开放接口认证" + "agent-open-api::开放接口认证", + "agent-open-api::代理开放接口查询不触发可靠卡观测" ], "classification": "behavior" }, @@ -1751,7 +1768,8 @@ "agent-funds-commission::充值边界", "external-integration::富友调用超时兼容行为", "identity-access::数据范围拒绝", - "order-payment-wallet::金额单位" + "order-payment-wallet::金额单位", + "external-integration::富友主扫统一下单与订单查询" ], "classification": "behavior" }, @@ -2304,7 +2322,10 @@ "capability": "order-refund-exchange", "requirements": [ "identity-access::数据范围拒绝", - "order-refund-exchange::订单、退款与换货状态门禁" + "order-refund-exchange::订单、退款与换货状态门禁", + "refund-approval::零金额退款审批通过", + "refund-approval::零金额退款不产生资金回款", + "refund-approval::非法退款金额仍被拒绝" ], "classification": "behavior" }, @@ -3283,14 +3304,9 @@ "entry_type": "async", "entry": "constants.TaskTypeAuditDailyArchive", "capability": "infrastructure", - "requirements": [], - "classification": "route_index_or_infrastructure" - }, - { - "entry_type": "async", - "entry": "constants.TaskTypeAuditMonthlyRetention", - "capability": "infrastructure", - "requirements": [], + "requirements": [ + "operations-audit::审计归档与日留存受控执行" + ], "classification": "route_index_or_infrastructure" }, { @@ -3399,14 +3415,10 @@ "entry_type": "async", "entry": "constants.TaskTypeIntegrationDailyArchive", "capability": "infrastructure", - "requirements": [], - "classification": "route_index_or_infrastructure" - }, - { - "entry_type": "async", - "entry": "constants.TaskTypeIntegrationMonthlyFinalize", - "capability": "infrastructure", - "requirements": [], + "requirements": [ + "external-integration::外部交互日志归档与留存受控执行", + "external-integration::外部交互日志逐日物理留存" + ], "classification": "route_index_or_infrastructure" }, { @@ -3466,7 +3478,11 @@ "entry_type": "async", "entry": "constants.TaskTypePackageQueueActivation", "capability": "infrastructure", - "requirements": [], + "requirements": [ + "package-queue-activation::当前主套餐过期后自动激活下一个", + "package-queue-activation::孤儿待生效套餐必须公平恢复", + "package-queue-activation::Asynq 激活结果必须准确且可重试" + ], "classification": "route_index_or_infrastructure" }, { @@ -3474,7 +3490,12 @@ "entry": "constants.TaskTypePollingCardStatus", "capability": "polling-operations", "requirements": [ - "polling-operations::手工轮询任务状态" + "polling-operations::手工轮询任务状态", + "external-integration::高频轮询外部交互日志保留边界", + "operations-audit::轮询审计事实保留边界", + "polling-load-control::轮询全局并发与背压", + "polling-load-control::正常无变化观测的低写入处理", + "polling-load-control::业务变化与轮询异常仍可追溯" ], "classification": "behavior" }, @@ -3483,7 +3504,12 @@ "entry": "constants.TaskTypePollingCarddata", "capability": "polling-operations", "requirements": [ - "polling-operations::手工轮询任务状态" + "polling-operations::手工轮询任务状态", + "external-integration::高频轮询外部交互日志保留边界", + "operations-audit::轮询审计事实保留边界", + "polling-load-control::轮询全局并发与背压", + "polling-load-control::正常无变化观测的低写入处理", + "polling-load-control::业务变化与轮询异常仍可追溯" ], "classification": "behavior" }, @@ -3492,7 +3518,9 @@ "entry": "constants.TaskTypePollingPackage", "capability": "polling-operations", "requirements": [ - "polling-operations::手工轮询任务状态" + "polling-operations::手工轮询任务状态", + "polling-load-control::轮询全局并发与背压", + "polling-operations::停复机遵循实际生效实名策略" ], "classification": "behavior" }, @@ -3501,7 +3529,8 @@ "entry": "constants.TaskTypePollingProtect", "capability": "polling-operations", "requirements": [ - "polling-operations::手工轮询任务状态" + "polling-operations::手工轮询任务状态", + "polling-load-control::轮询全局并发与背压" ], "classification": "behavior" }, @@ -3510,7 +3539,12 @@ "entry": "constants.TaskTypePollingRealname", "capability": "polling-operations", "requirements": [ - "polling-operations::手工轮询任务状态" + "polling-operations::手工轮询任务状态", + "external-integration::高频轮询外部交互日志保留边界", + "operations-audit::轮询审计事实保留边界", + "polling-load-control::轮询全局并发与背压", + "polling-load-control::正常无变化观测的低写入处理", + "polling-load-control::业务变化与轮询异常仍可追溯" ], "classification": "behavior" }, @@ -3560,5 +3594,44 @@ "order-refund-exchange::历史待审批退款可主动接入企业微信审批" ], "classification": "behavior" + }, + { + "entry_type": "async", + "entry": "constants.TaskTypeAuditDailyRetention", + "capability": "operations-audit", + "requirements": [ + "external-integration::外部交互日志归档与留存受控执行", + "external-integration::外部交互日志逐日物理留存", + "operations-audit::审计归档与日留存受控执行", + "operations-audit::审计在线数据逐日物理留存", + "operations-audit::日留存独立运行日志" + ], + "classification": "behavior" + }, + { + "entry_type": "cli", + "entry": "scripts/migration/migrate_runtime.py", + "capability": "qicheng-migration-package-lifecycle-overrides", + "requirements": [ + "qicheng-migration-package-lifecycle-overrides::迁移配置支持逐卡套餐生命周期覆盖", + "qicheng-migration-package-lifecycle-overrides::覆盖必须完整且可验证", + "qicheng-migration-package-lifecycle-overrides::未覆盖资产维持严格冲突阻断", + "qicheng-migration-package-usage::迁移套餐使用记录保存计时条款快照", + "qicheng-migration-package-usage::迁移套餐使用 SQL 满足快照完整性约束" + ], + "classification": "behavior" + }, + { + "entry_type": "cli", + "entry": "scripts/migration/import_shops.py", + "capability": "shop-bulk-import", + "requirements": [ + "shop-bulk-import::导入输入必须在生成 SQL 前完成全量预检", + "shop-bulk-import::生成的 SQL 必须在写入前校验目标库状态", + "shop-bulk-import::批量导入 SQL 必须创建完整的店铺初始事实", + "shop-bulk-import::导入执行必须由维护者审核后以单事务手工确认", + "shop-bulk-import::导入结果必须可审核和重跑定位" + ], + "classification": "behavior" } ] diff --git a/docs/verification/context-reset/requirement-evidence.json b/docs/verification/context-reset/requirement-evidence.json index 0c4fef1..4a10bc3 100644 --- a/docs/verification/context-reset/requirement-evidence.json +++ b/docs/verification/context-reset/requirement-evidence.json @@ -1481,5 +1481,1066 @@ ], "exit_status": 0 } + }, +{ + "capability": "agent-funds-commission", + "requirement": "代理在线充值可用支付方式按支付配置判定", + "spec": "openspec/specs/agent-funds-commission/spec.md", + "entries": [ + "GET /api/admin/agent-recharges/payment-methods" + ], + "handler_consumer_job": [ + "internal/handler/admin/agent_recharge.go:AgentRechargeHandler.PaymentMethods" + ], + "application_service_query": [ + "internal/application/agentrecharge/online_creation.go:OnlineCreationService.AvailablePaymentMethods" + ], + "domain_state_amount": [ + "internal/model/agent_wallet.go:AgentRechargeRecord.PaymentMethod/PaymentChannel" + ], + "store_migration_config": [ + "internal/application/agentrecharge/online_creation.go:adapter.Available" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/handler/admin/agent_recharge.go'\"'\"', '\"'\"'AvailablePaymentMethods'\"'\"'), ('\"'\"'internal/application/agentrecharge/online_creation.go'\"'\"', '\"'\"'adapter.Available(config)'\"'\"'), ('\"'\"'internal/model/agent_wallet.go'\"'\"', '\"'\"'PaymentChannel'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: agent-funds-commission::代理在线充值可用支付方式按支付配置判定'\"'\"')'", + "literal_output": [ + "OK: agent-funds-commission::代理在线充值可用支付方式按支付配置判定" + ], + "exit_status": 0 } +}, + { + "capability": "agent-open-api", + "requirement": "代理开放接口查询不触发可靠卡观测", + "spec": "openspec/specs/agent-open-api/spec.md", + "entries": [ + "GET /api/open/v1/cards/traffic", + "GET /api/open/v1/cards/status", + "GET /api/open/v1/cards/realname-status", + "GET /api/open/v1/devices/traffic" + ], + "handler_consumer_job": [ + "internal/handler/openapi/handler.go:GetCardTraffic -> service.GetCardTraffic", + "internal/handler/openapi/handler.go:GetCardStatus -> service.GetCardStatus", + "internal/handler/openapi/handler.go:GetRealnameStatus -> service.GetRealnameStatus", + "internal/handler/openapi/handler.go:GetDeviceTraffic -> service.GetDeviceTraffic" + ], + "application_service_query": [ + "internal/service/agent_open_api/service.go:GetCardTraffic", + "internal/service/agent_open_api/service.go:GetCardStatus", + "internal/service/agent_open_api/service.go:GetRealnameStatus", + "internal/service/agent_open_api/service.go:GetDeviceTraffic" + ], + "domain_state_amount": [ + "仅静态证明上述四个 GET handler 直接调用对应查询服务方法;不对全闭包的观测可达性作断言" + ], + "store_migration_config": [ + "internal/service/agent_open_api/service.go:packageUsageStore/deviceSimBindingStore/deviceStore" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/handler/openapi/handler.go'\"'\"', '\"'\"'func (h *Handler) GetCardTraffic'\"'\"'), ('\"'\"'internal/handler/openapi/handler.go'\"'\"', '\"'\"'h.service.GetCardTraffic(c.UserContext(), &req)'\"'\"'), ('\"'\"'internal/service/agent_open_api/service.go'\"'\"', '\"'\"'func (s *Service) GetCardTraffic'\"'\"'), ('\"'\"'internal/handler/openapi/handler.go'\"'\"', '\"'\"'func (h *Handler) GetCardStatus'\"'\"'), ('\"'\"'internal/handler/openapi/handler.go'\"'\"', '\"'\"'h.service.GetCardStatus(c.UserContext(), &req)'\"'\"'), ('\"'\"'internal/service/agent_open_api/service.go'\"'\"', '\"'\"'func (s *Service) GetCardStatus'\"'\"'), ('\"'\"'internal/handler/openapi/handler.go'\"'\"', '\"'\"'func (h *Handler) GetRealnameStatus'\"'\"'), ('\"'\"'internal/handler/openapi/handler.go'\"'\"', '\"'\"'h.service.GetRealnameStatus(c.UserContext(), &req)'\"'\"'), ('\"'\"'internal/service/agent_open_api/service.go'\"'\"', '\"'\"'func (s *Service) GetRealnameStatus'\"'\"'), ('\"'\"'internal/handler/openapi/handler.go'\"'\"', '\"'\"'func (h *Handler) GetDeviceTraffic'\"'\"'), ('\"'\"'internal/handler/openapi/handler.go'\"'\"', '\"'\"'h.service.GetDeviceTraffic(c.UserContext(), &req)'\"'\"'), ('\"'\"'internal/service/agent_open_api/service.go'\"'\"', '\"'\"'func (s *Service) GetDeviceTraffic'\"'\"')]; ordered=[('\"'\"'internal/handler/openapi/handler.go'\"'\"', ['\"'\"'func (h *Handler) GetCardTraffic'\"'\"', '\"'\"'h.service.GetCardTraffic(c.UserContext(), &req)'\"'\"']), ('\"'\"'internal/handler/openapi/handler.go'\"'\"', ['\"'\"'func (h *Handler) GetCardStatus'\"'\"', '\"'\"'h.service.GetCardStatus(c.UserContext(), &req)'\"'\"']), ('\"'\"'internal/handler/openapi/handler.go'\"'\"', ['\"'\"'func (h *Handler) GetRealnameStatus'\"'\"', '\"'\"'h.service.GetRealnameStatus(c.UserContext(), &req)'\"'\"']), ('\"'\"'internal/handler/openapi/handler.go'\"'\"', ['\"'\"'func (h *Handler) GetDeviceTraffic'\"'\"', '\"'\"'h.service.GetDeviceTraffic(c.UserContext(), &req)'\"'\"'])]; absent=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; unordered=[]; exec(\"for p,ts in ordered:\\n text=Path(p).read_text(); pos=-1; ok=True\\n for t in ts:\\n pos=text.find(t,pos+1)\\n if pos<0: ok=False; break\\n if not ok: unordered.append(f'\"'\"'{p}:{ts}'\"'\"')\"); present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in absent if t in Path(p).read_text()]; assert not missing and not unordered and not present, {'\"'\"'missing'\"'\"':missing,'\"'\"'unordered'\"'\"':unordered,'\"'\"'present'\"'\"':present}; print('\"'\"'OK: agent-open-api::代理开放接口查询不触发可靠卡观测'\"'\"')'", + "literal_output": [ + "OK: agent-open-api::代理开放接口查询不触发可靠卡观测" + ], + "exit_status": 0 + } + }, +{ + "capability": "external-integration", + "requirement": "富友主扫统一下单与订单查询", + "spec": "openspec/specs/external-integration/spec.md", + "entries": [ + "POST /api/admin/agent-recharges" + ], + "handler_consumer_job": [ + "internal/infrastructure/payment/fuiou_scan.go:FuiouScanAdapter.CreatePaymentURL/Query" + ], + "application_service_query": [ + "pkg/fuiou/scan.go:Client.PreCreate/CommonQuery" + ], + "domain_state_amount": [ + "pkg/fuiou/types.go:CommonQueryResponse.TransStat" + ], + "store_migration_config": [ + "internal/infrastructure/integrationlog" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/infrastructure/payment/fuiou_scan.go'\"'\"', '\"'\"'client.PreCreate('\"'\"'), ('\"'\"'internal/infrastructure/payment/fuiou_scan.go'\"'\"', '\"'\"'client.CommonQuery('\"'\"'), ('\"'\"'pkg/fuiou/scan.go'\"'\"', '\"'\"'func (c *Client) PreCreate'\"'\"'), ('\"'\"'pkg/fuiou/scan.go'\"'\"', '\"'\"'func (c *Client) CommonQuery'\"'\"'), ('\"'\"'pkg/fuiou/types.go'\"'\"', '\"'\"'xml:\"trans_stat\"'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: external-integration::富友主扫统一下单与订单查询'\"'\"')'", + "literal_output": [ + "OK: external-integration::富友主扫统一下单与订单查询" + ], + "exit_status": 0 + } +}, +{ + "capability": "external-integration", + "requirement": "高频轮询外部交互日志保留边界", + "spec": "openspec/specs/external-integration/spec.md", + "entries": [ + "constants.TaskTypePollingCarddata", + "constants.TaskTypePollingCardStatus", + "constants.TaskTypePollingRealname" + ], + "handler_consumer_job": [ + "internal/task/polling_integration_log.go:newGatewayAttempt/recordGatewayAttempt" + ], + "application_service_query": [ + "internal/application/cardobservation/traffic.go:ApplyTrafficObservation" + ], + "domain_state_amount": [ + "internal/infrastructure/integrationlog.Repository:Attempt.StateChanged" + ], + "store_migration_config": [ + "internal/query/retention/retention.go:Info.OnlineFrom/ArchivedBefore" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/task/polling_integration_log.go'\"'\"', '\"'\"'成功无变化的轮询不落库'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'recordGatewayAttempt'\"'\"'), ('\"'\"'internal/query/retention/retention.go'\"'\"', '\"'\"'ArchivedBefore'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: external-integration::高频轮询外部交互日志保留边界'\"'\"')'", + "literal_output": [ + "OK: external-integration::高频轮询外部交互日志保留边界" + ], + "exit_status": 0 + } +}, +{ + "capability": "external-integration", + "requirement": "外部交互日志归档与留存受控执行", + "spec": "openspec/specs/external-integration/spec.md", + "entries": [ + "constants.TaskTypeIntegrationDailyArchive", + "constants.TaskTypeAuditDailyRetention" + ], + "handler_consumer_job": [ + "internal/task/integration_archive.go:IntegrationArchiveHandler.HandleDaily", + "internal/task/audit_monthly_retention.go:AuditRetentionHandler.Handle" + ], + "application_service_query": [ + "internal/application/auditarchive/integration.go:ArchivePreviousIntegrationDay", + "internal/application/auditarchive/retention.go:RetainPendingDays" + ], + "domain_state_amount": [ + "internal/application/auditarchive/retention.go:RetentionBlockedError" + ], + "store_migration_config": [ + "pkg/constants/audit_archive.go:IntegrationArchiveSource" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/task/integration_archive.go'\"'\"', '\"'\"'func (h *IntegrationArchiveHandler) HandleDaily'\"'\"'), ('\"'\"'internal/task/audit_monthly_retention.go'\"'\"', '\"'\"'cleanupEnabled'\"'\"'), ('\"'\"'internal/application/auditarchive/retention.go'\"'\"', '\"'\"'RetentionBlockedError'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: external-integration::外部交互日志归档与留存受控执行'\"'\"')'", + "literal_output": [ + "OK: external-integration::外部交互日志归档与留存受控执行" + ], + "exit_status": 0 + } +}, +{ + "capability": "external-integration", + "requirement": "外部交互日志逐日物理留存", + "spec": "openspec/specs/external-integration/spec.md", + "entries": [ + "constants.TaskTypeIntegrationDailyArchive", + "constants.TaskTypeAuditDailyRetention" + ], + "handler_consumer_job": [ + "internal/task/integration_archive.go:IntegrationArchiveHandler.HandleDaily", + "internal/task/audit_monthly_retention.go:AuditRetentionHandler.Handle" + ], + "application_service_query": [ + "internal/application/auditarchive/integration.go:ArchivePreviousIntegrationDay", + "internal/application/auditarchive/retention.go:RetainPendingDays" + ], + "domain_state_amount": [ + "internal/application/auditarchive/retention.go:RetentionResult.IntegrationCount" + ], + "store_migration_config": [ + "internal/query/retention/retention.go:SourceIntegration/tb_integration_log" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/application/auditarchive/integration.go'\"'\"', '\"'\"'ArchivePreviousIntegrationDay'\"'\"'), ('\"'\"'internal/application/auditarchive/retention.go'\"'\"', '\"'\"'retainIntegrationDate'\"'\"'), ('\"'\"'internal/query/retention/retention.go'\"'\"', '\"'\"'tb_integration_log'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: external-integration::外部交互日志逐日物理留存'\"'\"')'", + "literal_output": [ + "OK: external-integration::外部交互日志逐日物理留存" + ], + "exit_status": 0 + } +}, +{ + "capability": "operations-audit", + "requirement": "轮询审计事实保留边界", + "spec": "openspec/specs/operations-audit/spec.md", + "entries": [ + "constants.TaskTypePollingCarddata", + "constants.TaskTypePollingCardStatus", + "constants.TaskTypePollingRealname" + ], + "handler_consumer_job": [ + "internal/task/polling_carddata_handler.go:Handle", + "internal/task/polling_cardstatus_handler.go:Handle", + "internal/task/polling_realname_handler.go:Handle" + ], + "application_service_query": [ + "internal/application/cardobservation/apply.go:ApplyCardObservation", + "internal/application/cardobservation/network.go:ApplyNetworkObservation", + "internal/application/cardobservation/traffic.go:ApplyTrafficObservation" + ], + "domain_state_amount": [ + "internal/application/cardobservation:workerObservationAudited" + ], + "store_migration_config": [ + "internal/query/audit/events.go:EventPage.Retention" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/application/cardobservation/network.go'\"'\"', '\"'\"'workerObservationAudited(ctx) && stateChanged'\"'\"'), ('\"'\"'internal/application/cardobservation/traffic.go'\"'\"', '\"'\"'workerObservationAudited(ctx) && stateChanged'\"'\"'), ('\"'\"'internal/query/audit/events.go'\"'\"', '\"'\"'在线查询留存边界'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: operations-audit::轮询审计事实保留边界'\"'\"')'", + "literal_output": [ + "OK: operations-audit::轮询审计事实保留边界" + ], + "exit_status": 0 + } +}, +{ + "capability": "operations-audit", + "requirement": "审计归档与日留存受控执行", + "spec": "openspec/specs/operations-audit/spec.md", + "entries": [ + "constants.TaskTypeAuditDailyArchive", + "constants.TaskTypeAuditDailyRetention" + ], + "handler_consumer_job": [ + "cmd/worker/main.go:registerAsynqScheduleTasks", + "internal/task/audit_monthly_retention.go:AuditRetentionHandler.Handle" + ], + "application_service_query": [ + "internal/application/auditarchive/service.go:ArchivePreviousDay", + "internal/application/auditarchive/retention.go:RetainPendingDays" + ], + "domain_state_amount": [ + "internal/application/auditarchive/retention.go:RetentionBlockedError" + ], + "store_migration_config": [ + "pkg/constants/audit_archive.go:TaskTypeAuditDailyArchive/TaskTypeAuditDailyRetention" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'cmd/worker/main.go'\"'\"', '\"'\"'constants.TaskTypeAuditDailyArchive'\"'\"'), ('\"'\"'cmd/worker/main.go'\"'\"', '\"'\"'constants.TaskTypeAuditDailyRetention'\"'\"'), ('\"'\"'internal/task/audit_monthly_retention.go'\"'\"', '\"'\"'cleanupEnabled'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: operations-audit::审计归档与日留存受控执行'\"'\"')'", + "literal_output": [ + "OK: operations-audit::审计归档与日留存受控执行" + ], + "exit_status": 0 + } +}, +{ + "capability": "operations-audit", + "requirement": "审计在线数据逐日物理留存", + "spec": "openspec/specs/operations-audit/spec.md", + "entries": [ + "constants.TaskTypeAuditDailyRetention" + ], + "handler_consumer_job": [ + "internal/task/audit_monthly_retention.go:AuditRetentionHandler.Handle" + ], + "application_service_query": [ + "internal/application/auditarchive/retention.go:RetainPendingDays/retainDate" + ], + "domain_state_amount": [ + "internal/application/auditarchive/retention.go:RetentionResult.EventCount/ResourceCount" + ], + "store_migration_config": [ + "internal/query/retention/retention.go:tb_audit_event/LogArchiveRun" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/application/auditarchive/retention.go'\"'\"', '\"'\"'RetainPendingDays'\"'\"'), ('\"'\"'internal/application/auditarchive/retention.go'\"'\"', '\"'\"'retainAuditDate'\"'\"'), ('\"'\"'internal/query/retention/retention.go'\"'\"', '\"'\"'tb_audit_event'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: operations-audit::审计在线数据逐日物理留存'\"'\"')'", + "literal_output": [ + "OK: operations-audit::审计在线数据逐日物理留存" + ], + "exit_status": 0 + } +}, +{ + "capability": "operations-audit", + "requirement": "日留存独立运行日志", + "spec": "openspec/specs/operations-audit/spec.md", + "entries": [ + "constants.TaskTypeAuditDailyRetention" + ], + "handler_consumer_job": [ + "cmd/worker/main.go:NewRetentionLogger", + "internal/task/audit_monthly_retention.go:AuditRetentionHandler.Handle" + ], + "application_service_query": [ + "internal/application/auditarchive/retention.go:RetainPendingDays" + ], + "domain_state_amount": [ + "internal/application/auditarchive/retention.go:RetentionResult.Duration" + ], + "store_migration_config": [ + "cmd/worker/main.go:cfg.Logging.RetentionLog" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'cmd/worker/main.go'\"'\"', '\"'\"'logger.NewRetentionLogger'\"'\"'), ('\"'\"'cmd/worker/main.go'\"'\"', '\"'\"'cfg.Logging.RetentionLog'\"'\"'), ('\"'\"'internal/task/audit_monthly_retention.go'\"'\"', '\"'\"'日志日留存处理完成'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: operations-audit::日留存独立运行日志'\"'\"')'", + "literal_output": [ + "OK: operations-audit::日留存独立运行日志" + ], + "exit_status": 0 + } +}, +{ + "capability": "operations-audit", + "requirement": "审计调查留存边界连续", + "spec": "openspec/specs/operations-audit/spec.md", + "entries": [ + "GET /api/admin/audit/events", + "GET /api/admin/audit/resources/{resource_type}/{resource_id}/timeline" + ], + "handler_consumer_job": [ + "internal/handler/admin/audit.go", + "internal/query/audit/timeline.go:LinkTimeline" + ], + "application_service_query": [ + "internal/query/audit/events.go:List", + "internal/query/audit/timeline.go:LinkTimeline" + ], + "domain_state_amount": [ + "internal/query/retention/retention.go:NormalizeRange" + ], + "store_migration_config": [ + "internal/query/retention/retention.go:sourceBoundary" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/query/audit/timeline.go'\"'\"', '\"'\"'Audit与Integration共同在线留存边界'\"'\"'), ('\"'\"'internal/query/retention/retention.go'\"'\"', '\"'\"'func NormalizeRange'\"'\"'), ('\"'\"'internal/query/retention/retention.go'\"'\"', '\"'\"'for _, row := range rows'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: operations-audit::审计调查留存边界连续'\"'\"')'", + "literal_output": [ + "OK: operations-audit::审计调查留存边界连续" + ], + "exit_status": 0 + } +}, + { + "capability": "package-lifecycle", + "requirement": "资产套餐层级投影", + "spec": "openspec/specs/package-lifecycle/spec.md", + "entries": [ + "GET /api/admin/assets/{identifier}/packages", + "GET /api/c/v1/asset/package-history" + ], + "handler_consumer_job": [ + "internal/handler/admin/asset.go:AssetHandler.Packages -> asset.Service.GetPackages", + "internal/handler/app/client_asset.go:ClientAssetHandler.GetPackageHistory -> assetquery.NewPackageHistoryQuery.List" + ], + "application_service_query": [ + "internal/service/asset/service.go:Service.GetPackages -> assetquery.NewPackageHistoryQuery.List", + "internal/query/asset/package_history.go:PackageHistoryQuery.List" + ], + "domain_state_amount": [ + "internal/query/asset/package_history.go:PackageHistoryNode.Children/RelationshipStatus", + "internal/model/package.go:PackageUsage.MasterUsageID" + ], + "store_migration_config": [ + "internal/query/asset/package_history.go:baseUsageQuery" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/handler/admin/asset.go'\"'\"', '\"'\"'func (h *AssetHandler) Packages'\"'\"'), ('\"'\"'internal/handler/admin/asset.go'\"'\"', '\"'\"'h.assetService.GetPackages(c.UserContext(), asset.AssetType, asset.AssetID, page, pageSize, status, callerAccountType)'\"'\"'), ('\"'\"'internal/service/asset/service.go'\"'\"', '\"'\"'func (s *Service) GetPackages'\"'\"'), ('\"'\"'internal/service/asset/service.go'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(s.db).List'\"'\"'), ('\"'\"'internal/handler/app/client_asset.go'\"'\"', '\"'\"'func (h *ClientAssetHandler) GetPackageHistory'\"'\"'), ('\"'\"'internal/handler/app/client_asset.go'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(h.db).List'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'func (q *PackageHistoryQuery) List'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'Children []*PackageHistoryNode'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'RelationshipStatus'\"'\"'), ('\"'\"'internal/model/package.go'\"'\"', '\"'\"'MasterUsageID'\"'\"')]; ordered=[('\"'\"'internal/handler/admin/asset.go'\"'\"', ['\"'\"'func (h *AssetHandler) Packages'\"'\"', '\"'\"'h.assetService.GetPackages(c.UserContext(), asset.AssetType, asset.AssetID, page, pageSize, status, callerAccountType)'\"'\"']), ('\"'\"'internal/service/asset/service.go'\"'\"', ['\"'\"'func (s *Service) GetPackages'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(s.db).List'\"'\"']), ('\"'\"'internal/handler/app/client_asset.go'\"'\"', ['\"'\"'func (h *ClientAssetHandler) GetPackageHistory'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(h.db).List'\"'\"'])]; absent=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; unordered=[]; exec(\"for p,ts in ordered:\\n text=Path(p).read_text(); pos=-1; ok=True\\n for t in ts:\\n pos=text.find(t,pos+1)\\n if pos<0: ok=False; break\\n if not ok: unordered.append(f'\"'\"'{p}:{ts}'\"'\"')\"); present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in absent if t in Path(p).read_text()]; assert not missing and not unordered and not present, {'\"'\"'missing'\"'\"':missing,'\"'\"'unordered'\"'\"':unordered,'\"'\"'present'\"'\"':present}; print('\"'\"'OK: package-lifecycle::资产套餐层级投影'\"'\"')'", + "literal_output": [ + "OK: package-lifecycle::资产套餐层级投影" + ], + "exit_status": 0 + } + }, + { + "capability": "package-lifecycle", + "requirement": "套餐历史整组筛选与分页", + "spec": "openspec/specs/package-lifecycle/spec.md", + "entries": [ + "GET /api/admin/assets/{identifier}/packages", + "GET /api/c/v1/asset/package-history" + ], + "handler_consumer_job": [ + "internal/handler/admin/asset.go:AssetHandler.Packages -> asset.Service.GetPackages", + "internal/handler/app/client_asset.go:ClientAssetHandler.GetPackageHistory -> assetquery.NewPackageHistoryQuery.List" + ], + "application_service_query": [ + "internal/service/asset/service.go:Service.GetPackages -> assetquery.NewPackageHistoryQuery.List", + "internal/query/asset/package_history.go:PackageHistoryQuery.List" + ], + "domain_state_amount": [ + "internal/query/asset/package_history.go:filterPackageHistoryGroups/paginatePackageHistoryGroups" + ], + "store_migration_config": [ + "internal/query/asset/package_history.go:baseUsageQuery" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/handler/admin/asset.go'\"'\"', '\"'\"'func (h *AssetHandler) Packages'\"'\"'), ('\"'\"'internal/handler/admin/asset.go'\"'\"', '\"'\"'h.assetService.GetPackages(c.UserContext(), asset.AssetType, asset.AssetID, page, pageSize, status, callerAccountType)'\"'\"'), ('\"'\"'internal/service/asset/service.go'\"'\"', '\"'\"'func (s *Service) GetPackages'\"'\"'), ('\"'\"'internal/service/asset/service.go'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(s.db).List'\"'\"'), ('\"'\"'internal/handler/app/client_asset.go'\"'\"', '\"'\"'func (h *ClientAssetHandler) GetPackageHistory'\"'\"'), ('\"'\"'internal/handler/app/client_asset.go'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(h.db).List'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'func (q *PackageHistoryQuery) List'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'filterPackageHistoryGroups'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'paginatePackageHistoryGroups'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'PageSize'\"'\"')]; ordered=[('\"'\"'internal/handler/admin/asset.go'\"'\"', ['\"'\"'func (h *AssetHandler) Packages'\"'\"', '\"'\"'h.assetService.GetPackages(c.UserContext(), asset.AssetType, asset.AssetID, page, pageSize, status, callerAccountType)'\"'\"']), ('\"'\"'internal/service/asset/service.go'\"'\"', ['\"'\"'func (s *Service) GetPackages'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(s.db).List'\"'\"']), ('\"'\"'internal/handler/app/client_asset.go'\"'\"', ['\"'\"'func (h *ClientAssetHandler) GetPackageHistory'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(h.db).List'\"'\"'])]; absent=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; unordered=[]; exec(\"for p,ts in ordered:\\n text=Path(p).read_text(); pos=-1; ok=True\\n for t in ts:\\n pos=text.find(t,pos+1)\\n if pos<0: ok=False; break\\n if not ok: unordered.append(f'\"'\"'{p}:{ts}'\"'\"')\"); present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in absent if t in Path(p).read_text()]; assert not missing and not unordered and not present, {'\"'\"'missing'\"'\"':missing,'\"'\"'unordered'\"'\"':unordered,'\"'\"'present'\"'\"':present}; print('\"'\"'OK: package-lifecycle::套餐历史整组筛选与分页'\"'\"')'", + "literal_output": [ + "OK: package-lifecycle::套餐历史整组筛选与分页" + ], + "exit_status": 0 + } + }, + { + "capability": "package-lifecycle", + "requirement": "套餐历史稳定排序", + "spec": "openspec/specs/package-lifecycle/spec.md", + "entries": [ + "GET /api/admin/assets/{identifier}/packages", + "GET /api/c/v1/asset/package-history" + ], + "handler_consumer_job": [ + "internal/handler/admin/asset.go:AssetHandler.Packages -> asset.Service.GetPackages", + "internal/handler/app/client_asset.go:ClientAssetHandler.GetPackageHistory -> assetquery.NewPackageHistoryQuery.List" + ], + "application_service_query": [ + "internal/service/asset/service.go:Service.GetPackages -> assetquery.NewPackageHistoryQuery.List", + "internal/query/asset/package_history.go:PackageHistoryQuery.List" + ], + "domain_state_amount": [ + "internal/query/asset/package_history.go:sortPackageHistoryGroups/packageHistoryChildLess" + ], + "store_migration_config": [ + "internal/query/asset/package_history.go:baseUsageQuery" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/handler/admin/asset.go'\"'\"', '\"'\"'func (h *AssetHandler) Packages'\"'\"'), ('\"'\"'internal/handler/admin/asset.go'\"'\"', '\"'\"'h.assetService.GetPackages(c.UserContext(), asset.AssetType, asset.AssetID, page, pageSize, status, callerAccountType)'\"'\"'), ('\"'\"'internal/service/asset/service.go'\"'\"', '\"'\"'func (s *Service) GetPackages'\"'\"'), ('\"'\"'internal/service/asset/service.go'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(s.db).List'\"'\"'), ('\"'\"'internal/handler/app/client_asset.go'\"'\"', '\"'\"'func (h *ClientAssetHandler) GetPackageHistory'\"'\"'), ('\"'\"'internal/handler/app/client_asset.go'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(h.db).List'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'func (q *PackageHistoryQuery) List'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'sort.Slice(item.Children'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'sort.Slice(items'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'left.ID < right.ID'\"'\"')]; ordered=[('\"'\"'internal/handler/admin/asset.go'\"'\"', ['\"'\"'func (h *AssetHandler) Packages'\"'\"', '\"'\"'h.assetService.GetPackages(c.UserContext(), asset.AssetType, asset.AssetID, page, pageSize, status, callerAccountType)'\"'\"']), ('\"'\"'internal/service/asset/service.go'\"'\"', ['\"'\"'func (s *Service) GetPackages'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(s.db).List'\"'\"']), ('\"'\"'internal/handler/app/client_asset.go'\"'\"', ['\"'\"'func (h *ClientAssetHandler) GetPackageHistory'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(h.db).List'\"'\"'])]; absent=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; unordered=[]; exec(\"for p,ts in ordered:\\n text=Path(p).read_text(); pos=-1; ok=True\\n for t in ts:\\n pos=text.find(t,pos+1)\\n if pos<0: ok=False; break\\n if not ok: unordered.append(f'\"'\"'{p}:{ts}'\"'\"')\"); present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in absent if t in Path(p).read_text()]; assert not missing and not unordered and not present, {'\"'\"'missing'\"'\"':missing,'\"'\"'unordered'\"'\"':unordered,'\"'\"'present'\"'\"':present}; print('\"'\"'OK: package-lifecycle::套餐历史稳定排序'\"'\"')'", + "literal_output": [ + "OK: package-lifecycle::套餐历史稳定排序" + ], + "exit_status": 0 + } + }, + { + "capability": "package-lifecycle", + "requirement": "物理缺失与不可展示关系区分", + "spec": "openspec/specs/package-lifecycle/spec.md", + "entries": [ + "GET /api/admin/assets/{identifier}/packages", + "GET /api/c/v1/asset/package-history" + ], + "handler_consumer_job": [ + "internal/handler/admin/asset.go:AssetHandler.Packages -> asset.Service.GetPackages", + "internal/handler/app/client_asset.go:ClientAssetHandler.GetPackageHistory -> assetquery.NewPackageHistoryQuery.List" + ], + "application_service_query": [ + "internal/service/asset/service.go:Service.GetPackages -> assetquery.NewPackageHistoryQuery.List", + "internal/query/asset/package_history.go:PackageHistoryQuery.List" + ], + "domain_state_amount": [ + "internal/query/asset/package_history.go:inRange/presentMasters/relationshipStatusMasterMissing" + ], + "store_migration_config": [ + "internal/query/asset/package_history.go:baseUsageQuery" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/handler/admin/asset.go'\"'\"', '\"'\"'func (h *AssetHandler) Packages'\"'\"'), ('\"'\"'internal/handler/admin/asset.go'\"'\"', '\"'\"'h.assetService.GetPackages(c.UserContext(), asset.AssetType, asset.AssetID, page, pageSize, status, callerAccountType)'\"'\"'), ('\"'\"'internal/service/asset/service.go'\"'\"', '\"'\"'func (s *Service) GetPackages'\"'\"'), ('\"'\"'internal/service/asset/service.go'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(s.db).List'\"'\"'), ('\"'\"'internal/handler/app/client_asset.go'\"'\"', '\"'\"'func (h *ClientAssetHandler) GetPackageHistory'\"'\"'), ('\"'\"'internal/handler/app/client_asset.go'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(h.db).List'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'func (q *PackageHistoryQuery) List'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'if inRange'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'presentMasters'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'relationshipStatusMasterMissing'\"'\"')]; ordered=[('\"'\"'internal/handler/admin/asset.go'\"'\"', ['\"'\"'func (h *AssetHandler) Packages'\"'\"', '\"'\"'h.assetService.GetPackages(c.UserContext(), asset.AssetType, asset.AssetID, page, pageSize, status, callerAccountType)'\"'\"']), ('\"'\"'internal/service/asset/service.go'\"'\"', ['\"'\"'func (s *Service) GetPackages'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(s.db).List'\"'\"']), ('\"'\"'internal/handler/app/client_asset.go'\"'\"', ['\"'\"'func (h *ClientAssetHandler) GetPackageHistory'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(h.db).List'\"'\"'])]; absent=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; unordered=[]; exec(\"for p,ts in ordered:\\n text=Path(p).read_text(); pos=-1; ok=True\\n for t in ts:\\n pos=text.find(t,pos+1)\\n if pos<0: ok=False; break\\n if not ok: unordered.append(f'\"'\"'{p}:{ts}'\"'\"')\"); present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in absent if t in Path(p).read_text()]; assert not missing and not unordered and not present, {'\"'\"'missing'\"'\"':missing,'\"'\"'unordered'\"'\"':unordered,'\"'\"'present'\"'\"':present}; print('\"'\"'OK: package-lifecycle::物理缺失与不可展示关系区分'\"'\"')'", + "literal_output": [ + "OK: package-lifecycle::物理缺失与不可展示关系区分" + ], + "exit_status": 0 + } + }, + { + "capability": "package-lifecycle", + "requirement": "套餐历史读取范围与兼容边界", + "spec": "openspec/specs/package-lifecycle/spec.md", + "entries": [ + "GET /api/admin/assets/{identifier}/packages", + "GET /api/c/v1/asset/package-history" + ], + "handler_consumer_job": [ + "internal/handler/admin/asset.go:AssetHandler.Packages -> asset.Service.GetPackages", + "internal/handler/app/client_asset.go:ClientAssetHandler.GetPackageHistory -> assetquery.NewPackageHistoryQuery.List" + ], + "application_service_query": [ + "internal/service/asset/service.go:Service.GetPackages -> assetquery.NewPackageHistoryQuery.List", + "internal/query/asset/package_history.go:PackageHistoryQuery.List" + ], + "domain_state_amount": [ + "internal/query/asset/package_history.go:PackageHistoryInput.AssetType/Generation/baseUsageQuery" + ], + "store_migration_config": [ + "internal/query/asset/package_history.go:baseUsageQuery" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/handler/admin/asset.go'\"'\"', '\"'\"'func (h *AssetHandler) Packages'\"'\"'), ('\"'\"'internal/handler/admin/asset.go'\"'\"', '\"'\"'h.assetService.GetPackages(c.UserContext(), asset.AssetType, asset.AssetID, page, pageSize, status, callerAccountType)'\"'\"'), ('\"'\"'internal/service/asset/service.go'\"'\"', '\"'\"'func (s *Service) GetPackages'\"'\"'), ('\"'\"'internal/service/asset/service.go'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(s.db).List'\"'\"'), ('\"'\"'internal/handler/app/client_asset.go'\"'\"', '\"'\"'func (h *ClientAssetHandler) GetPackageHistory'\"'\"'), ('\"'\"'internal/handler/app/client_asset.go'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(h.db).List'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'func (q *PackageHistoryQuery) List'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'AssetType'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'Generation'\"'\"'), ('\"'\"'internal/query/asset/package_history.go'\"'\"', '\"'\"'baseUsageQuery'\"'\"')]; ordered=[('\"'\"'internal/handler/admin/asset.go'\"'\"', ['\"'\"'func (h *AssetHandler) Packages'\"'\"', '\"'\"'h.assetService.GetPackages(c.UserContext(), asset.AssetType, asset.AssetID, page, pageSize, status, callerAccountType)'\"'\"']), ('\"'\"'internal/service/asset/service.go'\"'\"', ['\"'\"'func (s *Service) GetPackages'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(s.db).List'\"'\"']), ('\"'\"'internal/handler/app/client_asset.go'\"'\"', ['\"'\"'func (h *ClientAssetHandler) GetPackageHistory'\"'\"', '\"'\"'assetquery.NewPackageHistoryQuery(h.db).List'\"'\"'])]; absent=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; unordered=[]; exec(\"for p,ts in ordered:\\n text=Path(p).read_text(); pos=-1; ok=True\\n for t in ts:\\n pos=text.find(t,pos+1)\\n if pos<0: ok=False; break\\n if not ok: unordered.append(f'\"'\"'{p}:{ts}'\"'\"')\"); present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in absent if t in Path(p).read_text()]; assert not missing and not unordered and not present, {'\"'\"'missing'\"'\"':missing,'\"'\"'unordered'\"'\"':unordered,'\"'\"'present'\"'\"':present}; print('\"'\"'OK: package-lifecycle::套餐历史读取范围与兼容边界'\"'\"')'", + "literal_output": [ + "OK: package-lifecycle::套餐历史读取范围与兼容边界" + ], + "exit_status": 0 + } + }, +{ + "capability": "package-queue-activation", + "requirement": "当前主套餐过期后自动激活下一个", + "spec": "openspec/specs/package-queue-activation/spec.md", + "entries": [ + "constants.TaskTypePackageQueueActivation" + ], + "handler_consumer_job": [ + "internal/polling/package_activation_handler.go:PackageActivationHandler.Handle", + "pkg/queue/handler.go:HandlePackageQueueActivation" + ], + "application_service_query": [ + "internal/polling/package_activation_handler.go:activateNextPackage" + ], + "domain_state_amount": [ + "internal/model/package.go:PackageUsage.Priority/MasterUsageID" + ], + "store_migration_config": [ + "internal/polling/package_activation_handler.go:tb_package_usage" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/polling/package_activation_handler.go'\"'\"', '\"'\"'expires_at <= ?'\"'\"'), ('\"'\"'internal/polling/package_activation_handler.go'\"'\"', '\"'\"'enqueueActivationTask'\"'\"'), ('\"'\"'pkg/queue/handler.go'\"'\"', '\"'\"'TaskTypePackageQueueActivation'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: package-queue-activation::当前主套餐过期后自动激活下一个'\"'\"')'", + "literal_output": [ + "OK: package-queue-activation::当前主套餐过期后自动激活下一个" + ], + "exit_status": 0 + } +}, +{ + "capability": "package-queue-activation", + "requirement": "孤儿待生效套餐必须公平恢复", + "spec": "openspec/specs/package-queue-activation/spec.md", + "entries": [ + "constants.TaskTypePackageQueueActivation" + ], + "handler_consumer_job": [ + "internal/polling/package_activation_handler.go:findAndActivateOrphanPackages" + ], + "application_service_query": [ + "internal/polling/package_activation_handler.go:enqueueActivationTask" + ], + "domain_state_amount": [ + "internal/polling/package_activation_handler.go:ROW_NUMBER/priority/created_at/id" + ], + "store_migration_config": [ + "internal/polling/package_activation_handler.go:tb_package_usage" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/polling/package_activation_handler.go'\"'\"', '\"'\"'func (h *PackageActivationHandler) findAndActivateOrphanPackages'\"'\"'), ('\"'\"'internal/polling/package_activation_handler.go'\"'\"', '\"'\"'ROW_NUMBER() OVER'\"'\"'), ('\"'\"'internal/polling/package_activation_handler.go'\"'\"', '\"'\"'ORDER BY candidate.priority ASC, candidate.created_at ASC, candidate.id ASC'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: package-queue-activation::孤儿待生效套餐必须公平恢复'\"'\"')'", + "literal_output": [ + "OK: package-queue-activation::孤儿待生效套餐必须公平恢复" + ], + "exit_status": 0 + } +}, + { + "capability": "package-queue-activation", + "requirement": "Asynq 激活结果必须准确且可重试", + "spec": "openspec/specs/package-queue-activation/spec.md", + "entries": [ + "constants.TaskTypePackageQueueActivation" + ], + "handler_consumer_job": [ + "pkg/queue/handler.go:registerPackageActivationHandlers -> TaskTypePackageQueueActivation", + "internal/polling/package_activation_handler.go:PackageActivationHandler.HandlePackageQueueActivation" + ], + "application_service_query": [ + "internal/polling/package_activation_handler.go:enqueueActivationTask", + "internal/service/package/activation_service.go:ActivationService.ActivateSpecificPackage" + ], + "domain_state_amount": [ + "internal/service/package/activation_service.go:SetNX conflict -> CodePackageActivationConflict", + "internal/polling/package_activation_handler.go:business failure returns err" + ], + "store_migration_config": [ + "internal/service/package/activation_service.go:RedisPackageActivationLockKey", + "internal/polling/package_activation_handler.go:tb_package_usage" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'pkg/queue/handler.go'\"'\"', '\"'\"'h.mux.HandleFunc(constants.TaskTypePackageQueueActivation, packageActivationHandler.HandlePackageQueueActivation)'\"'\"'), ('\"'\"'internal/polling/package_activation_handler.go'\"'\"', '\"'\"'func (h *PackageActivationHandler) enqueueActivationTask'\"'\"'), ('\"'\"'internal/polling/package_activation_handler.go'\"'\"', '\"'\"'asynq.MaxRetry(3)'\"'\"'), ('\"'\"'internal/polling/package_activation_handler.go'\"'\"', '\"'\"'return err'\"'\"'), ('\"'\"'internal/service/package/activation_service.go'\"'\"', '\"'\"'return errors.New(errors.CodePackageActivationConflict)'\"'\"')]; ordered=[('\"'\"'internal/polling/package_activation_handler.go'\"'\"', ['\"'\"'if err := h.activationService.ActivateSpecificPackage'\"'\"', '\"'\"'return err'\"'\"'])]; absent=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; unordered=[]; exec(\"for p,ts in ordered:\\n text=Path(p).read_text(); pos=-1; ok=True\\n for t in ts:\\n pos=text.find(t,pos+1)\\n if pos<0: ok=False; break\\n if not ok: unordered.append(f'\"'\"'{p}:{ts}'\"'\"')\"); present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in absent if t in Path(p).read_text()]; assert not missing and not unordered and not present, {'\"'\"'missing'\"'\"':missing,'\"'\"'unordered'\"'\"':unordered,'\"'\"'present'\"'\"':present}; print('\"'\"'OK: package-queue-activation::Asynq 激活结果必须准确且可重试'\"'\"')'", + "literal_output": [ + "OK: package-queue-activation::Asynq 激活结果必须准确且可重试" + ], + "exit_status": 0 + } + }, + { + "capability": "polling-load-control", + "requirement": "轮询全局并发与背压", + "spec": "openspec/specs/polling-load-control/spec.md", + "entries": [ + "constants.TaskTypePollingCarddata", + "constants.TaskTypePollingCardStatus", + "constants.TaskTypePollingPackage", + "constants.TaskTypePollingProtect", + "constants.TaskTypePollingRealname" + ], + "handler_consumer_job": [ + "internal/task/polling_carddata_handler.go:PollingCarddataHandler.Handle", + "internal/task/polling_cardstatus_handler.go:PollingCardStatusHandler.Handle", + "internal/task/polling_package_handler.go:PollingPackageHandler.Handle", + "internal/task/polling_protect_handler.go:PollingProtectHandler.Handle", + "internal/task/polling_realname_handler.go:PollingRealnameHandler.Handle" + ], + "application_service_query": [ + "internal/task/polling_base.go:PollingBase.acquireConcurrency/requeueCard/releaseConcurrency" + ], + "domain_state_amount": [ + "internal/task/polling_base.go:acquireConcurrencyScript" + ], + "store_migration_config": [ + "pkg/constants/redis.go", + "pkg/constants/polling.go" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingCarddata)'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingCarddata)'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingCardStatus)'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCardStatus)'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingCardStatus)'\"'\"'), ('\"'\"'internal/task/polling_package_handler.go'\"'\"', '\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingPackage)'\"'\"'), ('\"'\"'internal/task/polling_package_handler.go'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingPackage)'\"'\"'), ('\"'\"'internal/task/polling_package_handler.go'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingPackage)'\"'\"'), ('\"'\"'internal/task/polling_protect_handler.go'\"'\"', '\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingProtect)'\"'\"'), ('\"'\"'internal/task/polling_protect_handler.go'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect)'\"'\"'), ('\"'\"'internal/task/polling_protect_handler.go'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingProtect)'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingRealname)'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingRealname)'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingRealname)'\"'\"'), ('\"'\"'internal/task/polling_base.go'\"'\"', '\"'\"'var acquireConcurrencyScript'\"'\"')]; ordered=[('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', ['\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingCarddata)'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingCarddata)'\"'\"']), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', ['\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingCardStatus)'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCardStatus)'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingCardStatus)'\"'\"']), ('\"'\"'internal/task/polling_package_handler.go'\"'\"', ['\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingPackage)'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingPackage)'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingPackage)'\"'\"']), ('\"'\"'internal/task/polling_protect_handler.go'\"'\"', ['\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingProtect)'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect)'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingProtect)'\"'\"']), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', ['\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingRealname)'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingRealname)'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingRealname)'\"'\"'])]; absent=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; unordered=[]; exec(\"for p,ts in ordered:\\n text=Path(p).read_text(); pos=-1; ok=True\\n for t in ts:\\n pos=text.find(t,pos+1)\\n if pos<0: ok=False; break\\n if not ok: unordered.append(f'\"'\"'{p}:{ts}'\"'\"')\"); present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in absent if t in Path(p).read_text()]; assert not missing and not unordered and not present, {'\"'\"'missing'\"'\"':missing,'\"'\"'unordered'\"'\"':unordered,'\"'\"'present'\"'\"':present}; print('\"'\"'OK: polling-load-control::轮询全局并发与背压'\"'\"')'", + "literal_output": [ + "OK: polling-load-control::轮询全局并发与背压" + ], + "exit_status": 0 + } + }, + { + "capability": "polling-load-control", + "requirement": "正常无变化观测的低写入处理", + "spec": "openspec/specs/polling-load-control/spec.md", + "entries": [ + "constants.TaskTypePollingCarddata", + "constants.TaskTypePollingCardStatus", + "constants.TaskTypePollingRealname" + ], + "handler_consumer_job": [ + "internal/task/polling_carddata_handler.go:PollingCarddataHandler.Handle", + "internal/task/polling_cardstatus_handler.go:PollingCardStatusHandler.Handle", + "internal/task/polling_realname_handler.go:PollingRealnameHandler.Handle", + "internal/task/polling_integration_log.go:recordGatewayAttempt" + ], + "application_service_query": [ + "internal/application/cardobservation/traffic.go:ApplyTrafficObservation", + "internal/application/cardobservation/network.go:ApplyNetworkObservation", + "internal/application/cardobservation/apply.go:ApplyCardObservation" + ], + "domain_state_amount": [ + "internal/task/polling_integration_log.go:attempt created in memory then persisted only on decided outcome" + ], + "store_migration_config": [ + "internal/infrastructure/integrationlog.Repository" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingCarddata)'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingCarddata)'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingCardStatus)'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCardStatus)'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingCardStatus)'\"'\"'), ('\"'\"'internal/task/polling_package_handler.go'\"'\"', '\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingPackage)'\"'\"'), ('\"'\"'internal/task/polling_package_handler.go'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingPackage)'\"'\"'), ('\"'\"'internal/task/polling_package_handler.go'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingPackage)'\"'\"'), ('\"'\"'internal/task/polling_protect_handler.go'\"'\"', '\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingProtect)'\"'\"'), ('\"'\"'internal/task/polling_protect_handler.go'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect)'\"'\"'), ('\"'\"'internal/task/polling_protect_handler.go'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingProtect)'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingRealname)'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingRealname)'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingRealname)'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'attempt := newGatewayAttempt'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'h.observation.ApplyTrafficObservation'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'changed := decision.IncrementMB > 0 || decision.CrossMonth'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'if changed {'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'IntegrationResultSuccess, true'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'attempt := newGatewayAttempt'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'h.observation.ApplyNetworkObservation'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'changed := decision.StatusChanged || decision.StopReasonChanged || decision.StopPolling'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'if changed {'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'IntegrationResultSuccess, true'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'attempt := newGatewayAttempt'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'h.observation.ApplyCardObservation'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'changed := decision.StatusChanged'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'if changed {'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'IntegrationResultSuccess, true'\"'\"')]; ordered=[('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', ['\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingCarddata)'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingCarddata)'\"'\"']), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', ['\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingCardStatus)'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCardStatus)'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingCardStatus)'\"'\"']), ('\"'\"'internal/task/polling_package_handler.go'\"'\"', ['\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingPackage)'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingPackage)'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingPackage)'\"'\"']), ('\"'\"'internal/task/polling_protect_handler.go'\"'\"', ['\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingProtect)'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect)'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingProtect)'\"'\"']), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', ['\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingRealname)'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingRealname)'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingRealname)'\"'\"']), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', ['\"'\"'attempt := newGatewayAttempt'\"'\"', '\"'\"'h.observation.ApplyTrafficObservation'\"'\"', '\"'\"'changed := decision.IncrementMB > 0 || decision.CrossMonth'\"'\"', '\"'\"'if changed {'\"'\"', '\"'\"'IntegrationResultSuccess, true'\"'\"']), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', ['\"'\"'attempt := newGatewayAttempt'\"'\"', '\"'\"'h.observation.ApplyNetworkObservation'\"'\"', '\"'\"'changed := decision.StatusChanged || decision.StopReasonChanged || decision.StopPolling'\"'\"', '\"'\"'if changed {'\"'\"', '\"'\"'IntegrationResultSuccess, true'\"'\"']), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', ['\"'\"'attempt := newGatewayAttempt'\"'\"', '\"'\"'h.observation.ApplyCardObservation'\"'\"', '\"'\"'changed := decision.StatusChanged'\"'\"', '\"'\"'if changed {'\"'\"', '\"'\"'IntegrationResultSuccess, true'\"'\"'])]; absent=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; unordered=[]; exec(\"for p,ts in ordered:\\n text=Path(p).read_text(); pos=-1; ok=True\\n for t in ts:\\n pos=text.find(t,pos+1)\\n if pos<0: ok=False; break\\n if not ok: unordered.append(f'\"'\"'{p}:{ts}'\"'\"')\"); present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in absent if t in Path(p).read_text()]; assert not missing and not unordered and not present, {'\"'\"'missing'\"'\"':missing,'\"'\"'unordered'\"'\"':unordered,'\"'\"'present'\"'\"':present}; print('\"'\"'OK: polling-load-control::正常无变化观测的低写入处理'\"'\"')'", + "literal_output": [ + "OK: polling-load-control::正常无变化观测的低写入处理" + ], + "exit_status": 0 + } + }, + { + "capability": "polling-load-control", + "requirement": "业务变化与轮询异常仍可追溯", + "spec": "openspec/specs/polling-load-control/spec.md", + "entries": [ + "constants.TaskTypePollingCarddata", + "constants.TaskTypePollingCardStatus", + "constants.TaskTypePollingRealname" + ], + "handler_consumer_job": [ + "internal/task/polling_carddata_handler.go:PollingCarddataHandler.Handle", + "internal/task/polling_cardstatus_handler.go:PollingCardStatusHandler.Handle", + "internal/task/polling_realname_handler.go:PollingRealnameHandler.Handle", + "internal/task/polling_integration_log.go:recordGatewayAttempt" + ], + "application_service_query": [ + "internal/application/cardobservation/traffic.go:ApplyTrafficObservation", + "internal/application/cardobservation/network.go:ApplyNetworkObservation", + "internal/application/cardobservation/apply.go:ApplyCardObservation" + ], + "domain_state_amount": [ + "internal/task polling gateway failure/result paths", + "internal/application/cardobservation traffic/network Outbox and worker audit paths" + ], + "store_migration_config": [ + "internal/infrastructure/integrationlog.Repository" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingCarddata)'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingCarddata)'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingCardStatus)'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCardStatus)'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingCardStatus)'\"'\"'), ('\"'\"'internal/task/polling_package_handler.go'\"'\"', '\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingPackage)'\"'\"'), ('\"'\"'internal/task/polling_package_handler.go'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingPackage)'\"'\"'), ('\"'\"'internal/task/polling_package_handler.go'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingPackage)'\"'\"'), ('\"'\"'internal/task/polling_protect_handler.go'\"'\"', '\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingProtect)'\"'\"'), ('\"'\"'internal/task/polling_protect_handler.go'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect)'\"'\"'), ('\"'\"'internal/task/polling_protect_handler.go'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingProtect)'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingRealname)'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingRealname)'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingRealname)'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'IntegrationResultFailed'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'应用流量观测失败'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'IntegrationResultUnknown'\"'\"'), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', '\"'\"'return h.failAndRequeue'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'IntegrationResultFailed'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'应用网络状态观测失败'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'IntegrationResultUnknown'\"'\"'), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', '\"'\"'return h.failAndRequeue'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'IntegrationResultFailed'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'应用实名观测失败'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'IntegrationResultUnknown'\"'\"'), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', '\"'\"'return h.failAndRequeue'\"'\"')]; ordered=[('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', ['\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingCarddata)'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingCarddata)'\"'\"']), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', ['\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingCardStatus)'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCardStatus)'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingCardStatus)'\"'\"']), ('\"'\"'internal/task/polling_package_handler.go'\"'\"', ['\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingPackage)'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingPackage)'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingPackage)'\"'\"']), ('\"'\"'internal/task/polling_protect_handler.go'\"'\"', ['\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingProtect)'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect)'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingProtect)'\"'\"']), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', ['\"'\"'!h.base.acquireConcurrency(ctx, constants.TaskTypePollingRealname)'\"'\"', '\"'\"'return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingRealname)'\"'\"', '\"'\"'defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingRealname)'\"'\"']), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', ['\"'\"'IntegrationResultFailed'\"'\"', '\"'\"'return h.failAndRequeue'\"'\"']), ('\"'\"'internal/task/polling_carddata_handler.go'\"'\"', ['\"'\"'IntegrationResultUnknown'\"'\"', '\"'\"'应用流量观测失败'\"'\"', '\"'\"'return h.failAndRequeue'\"'\"']), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', ['\"'\"'IntegrationResultFailed'\"'\"', '\"'\"'return h.failAndRequeue'\"'\"']), ('\"'\"'internal/task/polling_cardstatus_handler.go'\"'\"', ['\"'\"'IntegrationResultUnknown'\"'\"', '\"'\"'应用网络状态观测失败'\"'\"', '\"'\"'return h.failAndRequeue'\"'\"']), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', ['\"'\"'IntegrationResultFailed'\"'\"', '\"'\"'return h.failAndRequeue'\"'\"']), ('\"'\"'internal/task/polling_realname_handler.go'\"'\"', ['\"'\"'IntegrationResultUnknown'\"'\"', '\"'\"'应用实名观测失败'\"'\"', '\"'\"'return h.failAndRequeue'\"'\"'])]; absent=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; unordered=[]; exec(\"for p,ts in ordered:\\n text=Path(p).read_text(); pos=-1; ok=True\\n for t in ts:\\n pos=text.find(t,pos+1)\\n if pos<0: ok=False; break\\n if not ok: unordered.append(f'\"'\"'{p}:{ts}'\"'\"')\"); present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in absent if t in Path(p).read_text()]; assert not missing and not unordered and not present, {'\"'\"'missing'\"'\"':missing,'\"'\"'unordered'\"'\"':unordered,'\"'\"'present'\"'\"':present}; print('\"'\"'OK: polling-load-control::业务变化与轮询异常仍可追溯'\"'\"')'", + "literal_output": [ + "OK: polling-load-control::业务变化与轮询异常仍可追溯" + ], + "exit_status": 0 + } + }, + { + "capability": "polling-operations", + "requirement": "停复机遵循实际生效实名策略", + "spec": "openspec/specs/polling-operations/spec.md", + "entries": [ + "constants.TaskTypePollingPackage" + ], + "handler_consumer_job": [ + "internal/task/polling_package_handler.go:PollingPackageHandler.Handle", + "pkg/queue/handler.go:registerPollingHandlers" + ], + "application_service_query": [ + "internal/task/polling_package_handler.go:PollingPackageHandler.Handle -> StopResumeServiceInterface.EvaluateAndAct", + "internal/service/iot_card/stop_resume_service.go:StopResumeService.EvaluateAndAct/isRealnameOK" + ], + "domain_state_amount": [ + "internal/model/iot_card.go:IotCard.RealnamePolicy", + "internal/model/device.go:Device.RealnamePolicy" + ], + "store_migration_config": [ + "internal/task/polling_package_handler.go:IotCardStore.GetByID" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/task/polling_package_handler.go'\"'\"', '\"'\"'func (h *PollingPackageHandler) Handle'\"'\"'), ('\"'\"'internal/task/polling_package_handler.go'\"'\"', '\"'\"'h.stopResumeSvc.EvaluateAndAct(ctx, freshCard)'\"'\"'), ('\"'\"'pkg/queue/handler.go'\"'\"', '\"'\"'h.mux.HandleFunc(constants.TaskTypePollingPackage, packageHandler.Handle)'\"'\"'), ('\"'\"'internal/service/iot_card/stop_resume_service.go'\"'\"', '\"'\"'func (s *StopResumeService) isRealnameOK'\"'\"')]; ordered=[]; absent=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; unordered=[]; exec(\"for p,ts in ordered:\\n text=Path(p).read_text(); pos=-1; ok=True\\n for t in ts:\\n pos=text.find(t,pos+1)\\n if pos<0: ok=False; break\\n if not ok: unordered.append(f'\"'\"'{p}:{ts}'\"'\"')\"); present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in absent if t in Path(p).read_text()]; assert not missing and not unordered and not present, {'\"'\"'missing'\"'\"':missing,'\"'\"'unordered'\"'\"':unordered,'\"'\"'present'\"'\"':present}; print('\"'\"'OK: polling-operations::停复机遵循实际生效实名策略'\"'\"')'", + "literal_output": [ + "OK: polling-operations::停复机遵循实际生效实名策略" + ], + "exit_status": 0 + } + }, +{ + "capability": "qicheng-migration-package-lifecycle-overrides", + "requirement": "迁移配置支持逐卡套餐生命周期覆盖", + "spec": "openspec/specs/qicheng-migration-package-lifecycle-overrides/spec.md", + "entries": [ + "scripts/migration/migrate_runtime.py" + ], + "handler_consumer_job": [ + "scripts/migration/migrate_runtime.py:main" + ], + "application_service_query": [ + "scripts/migration/lib/mapping_loader.py:Mapping.lookup_package_lifecycle_override", + "scripts/migration/lib/sql_builder.py:_apply_package_lifecycle_override" + ], + "domain_state_amount": [ + "scripts/migration/lib/mapping_loader.py:PackageLifecycleOverride" + ], + "store_migration_config": [ + "scripts/migration/config/mapping.yaml", + "scripts/migration/lib/legacy_query.py:tbl_card_life" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'scripts/migration/lib/mapping_loader.py'\"'\"', '\"'\"'package_lifecycle_overrides'\"'\"'), ('\"'\"'scripts/migration/lib/sql_builder.py'\"'\"', '\"'\"'_apply_package_lifecycle_override'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: qicheng-migration-package-lifecycle-overrides::迁移配置支持逐卡套餐生命周期覆盖'\"'\"')'", + "literal_output": [ + "OK: qicheng-migration-package-lifecycle-overrides::迁移配置支持逐卡套餐生命周期覆盖" + ], + "exit_status": 0 + } +}, +{ + "capability": "qicheng-migration-package-lifecycle-overrides", + "requirement": "覆盖必须完整且可验证", + "spec": "openspec/specs/qicheng-migration-package-lifecycle-overrides/spec.md", + "entries": [ + "scripts/migration/migrate_runtime.py" + ], + "handler_consumer_job": [ + "scripts/migration/migrate_runtime.py:main" + ], + "application_service_query": [ + "scripts/migration/lib/sql_builder.py:_apply_package_lifecycle_override" + ], + "domain_state_amount": [ + "scripts/migration/lib/sql_builder.py:selected != migratable" + ], + "store_migration_config": [ + "scripts/migration/lib/mapping_loader.py:_load_package_lifecycle_overrides" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'scripts/migration/lib/sql_builder.py'\"'\"', '\"'\"'套餐生命周期覆盖必须完整裁决原本可迁移记录'\"'\"'), ('\"'\"'scripts/migration/lib/mapping_loader.py'\"'\"', '\"'\"'生命周期 ID 不可重复或跨状态重复'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: qicheng-migration-package-lifecycle-overrides::覆盖必须完整且可验证'\"'\"')'", + "literal_output": [ + "OK: qicheng-migration-package-lifecycle-overrides::覆盖必须完整且可验证" + ], + "exit_status": 0 + } +}, +{ + "capability": "qicheng-migration-package-lifecycle-overrides", + "requirement": "未覆盖资产维持严格冲突阻断", + "spec": "openspec/specs/qicheng-migration-package-lifecycle-overrides/spec.md", + "entries": [ + "scripts/migration/migrate_runtime.py" + ], + "handler_consumer_job": [ + "scripts/migration/migrate_runtime.py:main" + ], + "application_service_query": [ + "scripts/migration/lib/sql_builder.py:_resolve_runtime_packages" + ], + "domain_state_amount": [ + "scripts/migration/lib/sql_builder.py:multiple_active_packages/blocked" + ], + "store_migration_config": [ + "scripts/migration/lib/legacy_query.py:tbl_card_life" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'scripts/migration/lib/sql_builder.py'\"'\"', '\"'\"'multiple_active_packages'\"'\"'), ('\"'\"'scripts/migration/lib/sql_builder.py'\"'\"', '\"'\"'同一资产存在多个当前生效正式套餐,无法唯一裁决'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: qicheng-migration-package-lifecycle-overrides::未覆盖资产维持严格冲突阻断'\"'\"')'", + "literal_output": [ + "OK: qicheng-migration-package-lifecycle-overrides::未覆盖资产维持严格冲突阻断" + ], + "exit_status": 0 + } +}, +{ + "capability": "qicheng-migration-package-usage", + "requirement": "迁移套餐使用记录保存计时条款快照", + "spec": "openspec/specs/qicheng-migration-package-usage/spec.md", + "entries": [ + "scripts/migration/migrate_runtime.py" + ], + "handler_consumer_job": [ + "scripts/migration/migrate_runtime.py:main" + ], + "application_service_query": [ + "scripts/migration/lib/sql_builder.py:_write_package_usages" + ], + "domain_state_amount": [ + "scripts/migration/lib/sql_builder.py:expiry_base_snapshot/calendar_type_snapshot/duration snapshots" + ], + "store_migration_config": [ + "migrations/000163_add_package_usage_terms_snapshots.up.sql" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'scripts/migration/lib/sql_builder.py'\"'\"', '\"'\"'expiry_base_snapshot, calendar_type_snapshot, duration_months_snapshot, duration_days_snapshot'\"'\"'), ('\"'\"'scripts/migration/lib/sql_builder.py'\"'\"', '\"'\"'INSERT INTO tb_package_usage'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: qicheng-migration-package-usage::迁移套餐使用记录保存计时条款快照'\"'\"')'", + "literal_output": [ + "OK: qicheng-migration-package-usage::迁移套餐使用记录保存计时条款快照" + ], + "exit_status": 0 + } +}, +{ + "capability": "qicheng-migration-package-usage", + "requirement": "迁移套餐使用 SQL 满足快照完整性约束", + "spec": "openspec/specs/qicheng-migration-package-usage/spec.md", + "entries": [ + "scripts/migration/migrate_runtime.py" + ], + "handler_consumer_job": [ + "scripts/migration/migrate_runtime.py:main" + ], + "application_service_query": [ + "scripts/migration/lib/sql_builder.py:_write_package_usages" + ], + "domain_state_amount": [ + "migrations/000163_add_package_usage_terms_snapshots.up.sql:validate_package_usage_terms_snapshot" + ], + "store_migration_config": [ + "migrations/000163_add_package_usage_terms_snapshots.up.sql:trg_validate_package_usage_terms_snapshot" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'migrations/000163_add_package_usage_terms_snapshots.up.sql'\"'\"', '\"'\"'CREATE TRIGGER trg_validate_package_usage_terms_snapshot'\"'\"'), ('\"'\"'migrations/000163_add_package_usage_terms_snapshots.up.sql'\"'\"', '\"'\"'套餐使用记录计时快照不完整或无效'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: qicheng-migration-package-usage::迁移套餐使用 SQL 满足快照完整性约束'\"'\"')'", + "literal_output": [ + "OK: qicheng-migration-package-usage::迁移套餐使用 SQL 满足快照完整性约束" + ], + "exit_status": 0 + } +}, + { + "capability": "refund-approval", + "requirement": "零金额退款审批通过", + "spec": "openspec/specs/refund-approval/spec.md", + "entries": [ + "POST /api/admin/refunds/{id}/approve" + ], + "handler_consumer_job": [ + "internal/handler/admin/refund.go:RefundHandler.Approve(BodyParser 后直接传 Service;该方法体无 Validate/ValidateStruct 调用)", + "cmd/worker/main.go:ApprovalBusinessTypeRefund -> refund.Service.Handle" + ], + "application_service_query": [ + "internal/service/refund/service.go:Service.Approve/validateApprovedRefundAmount", + "internal/service/refund/approval_decision.go:Service.applyApprovedDecision" + ], + "domain_state_amount": [ + "Service 和企业微信受控审批决策路径可传递/处理 approvedAmount == 0;公开 DTO 字段仍标记 omitempty,min=1" + ], + "store_migration_config": [ + "internal/model/refund.go:RefundRequest", + "migrations/archive/000093_create_refund_request.up.sql" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/model/dto/refund_dto.go'\"'\"', '\"'\"'validate:\"omitempty,min=1\"'\"'\"'), ('\"'\"'internal/handler/admin/refund.go'\"'\"', '\"'\"'func (h *RefundHandler) Approve'\"'\"'), ('\"'\"'internal/handler/admin/refund.go'\"'\"', '\"'\"'if err := c.BodyParser(&req); err != nil'\"'\"'), ('\"'\"'internal/handler/admin/refund.go'\"'\"', '\"'\"'h.service.Approve(c.UserContext(), uint(id), &req)'\"'\"'), ('\"'\"'internal/service/refund/service.go'\"'\"', '\"'\"'if approvedAmount < 0'\"'\"'), ('\"'\"'internal/service/refund/approval_decision.go'\"'\"', '\"'\"'approvedAmount := refund.RequestedRefundAmount'\"'\"')]; ordered=[('\"'\"'internal/handler/admin/refund.go'\"'\"', ['\"'\"'func (h *RefundHandler) Approve'\"'\"', '\"'\"'if err := c.BodyParser(&req); err != nil'\"'\"', '\"'\"'h.service.Approve(c.UserContext(), uint(id), &req)'\"'\"'])]; absent=[('\"'\"'internal/handler/admin/refund.go'\"'\"', '\"'\"'ValidateStruct'\"'\"'), ('\"'\"'internal/handler/admin/refund.go'\"'\"', '\"'\"'.Validate('\"'\"')]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; unordered=[]; exec(\"for p,ts in ordered:\\n text=Path(p).read_text(); pos=-1; ok=True\\n for t in ts:\\n pos=text.find(t,pos+1)\\n if pos<0: ok=False; break\\n if not ok: unordered.append(f'\"'\"'{p}:{ts}'\"'\"')\"); present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in absent if t in Path(p).read_text()]; assert not missing and not unordered and not present, {'\"'\"'missing'\"'\"':missing,'\"'\"'unordered'\"'\"':unordered,'\"'\"'present'\"'\"':present}; print('\"'\"'OK: refund-approval::零金额退款审批通过'\"'\"')'", + "literal_output": [ + "OK: refund-approval::零金额退款审批通过" + ], + "exit_status": 0 + } + }, + { + "capability": "refund-approval", + "requirement": "零金额退款不产生资金回款", + "spec": "openspec/specs/refund-approval/spec.md", + "entries": [ + "POST /api/admin/refunds/{id}/approve" + ], + "handler_consumer_job": [ + "internal/handler/admin/refund.go:RefundHandler.Approve(BodyParser 后直接传 Service;该方法体无 Validate/ValidateStruct 调用)", + "cmd/worker/main.go:ApprovalBusinessTypeRefund -> refund.Service.Handle" + ], + "application_service_query": [ + "internal/service/refund/service.go:Service.Approve/validateApprovedRefundAmount", + "internal/service/refund/approval_decision.go:Service.applyApprovedDecision" + ], + "domain_state_amount": [ + "Service 和企业微信受控审批决策路径可传递/处理 approvedAmount == 0;公开 DTO 字段仍标记 omitempty,min=1" + ], + "store_migration_config": [ + "internal/model/refund.go:RefundRequest", + "migrations/archive/000093_create_refund_request.up.sql" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/model/dto/refund_dto.go'\"'\"', '\"'\"'validate:\"omitempty,min=1\"'\"'\"'), ('\"'\"'internal/handler/admin/refund.go'\"'\"', '\"'\"'func (h *RefundHandler) Approve'\"'\"'), ('\"'\"'internal/handler/admin/refund.go'\"'\"', '\"'\"'if err := c.BodyParser(&req); err != nil'\"'\"'), ('\"'\"'internal/handler/admin/refund.go'\"'\"', '\"'\"'h.service.Approve(c.UserContext(), uint(id), &req)'\"'\"'), ('\"'\"'internal/service/refund/service.go'\"'\"', '\"'\"'if approvedAmount < 0'\"'\"'), ('\"'\"'internal/service/refund/approval_decision.go'\"'\"', '\"'\"'approvedAmount := refund.RequestedRefundAmount'\"'\"'), ('\"'\"'internal/service/refund/service.go'\"'\"', '\"'\"'if amount == 0 || order.PaymentMethod != model.PaymentMethodWallet'\"'\"')]; ordered=[('\"'\"'internal/handler/admin/refund.go'\"'\"', ['\"'\"'func (h *RefundHandler) Approve'\"'\"', '\"'\"'if err := c.BodyParser(&req); err != nil'\"'\"', '\"'\"'h.service.Approve(c.UserContext(), uint(id), &req)'\"'\"'])]; absent=[('\"'\"'internal/handler/admin/refund.go'\"'\"', '\"'\"'ValidateStruct'\"'\"'), ('\"'\"'internal/handler/admin/refund.go'\"'\"', '\"'\"'.Validate('\"'\"')]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; unordered=[]; exec(\"for p,ts in ordered:\\n text=Path(p).read_text(); pos=-1; ok=True\\n for t in ts:\\n pos=text.find(t,pos+1)\\n if pos<0: ok=False; break\\n if not ok: unordered.append(f'\"'\"'{p}:{ts}'\"'\"')\"); present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in absent if t in Path(p).read_text()]; assert not missing and not unordered and not present, {'\"'\"'missing'\"'\"':missing,'\"'\"'unordered'\"'\"':unordered,'\"'\"'present'\"'\"':present}; print('\"'\"'OK: refund-approval::零金额退款不产生资金回款'\"'\"')'", + "literal_output": [ + "OK: refund-approval::零金额退款不产生资金回款" + ], + "exit_status": 0 + } + }, + { + "capability": "refund-approval", + "requirement": "非法退款金额仍被拒绝", + "spec": "openspec/specs/refund-approval/spec.md", + "entries": [ + "POST /api/admin/refunds/{id}/approve" + ], + "handler_consumer_job": [ + "internal/handler/admin/refund.go:RefundHandler.Approve(BodyParser 后直接传 Service;该方法体无 Validate/ValidateStruct 调用)", + "cmd/worker/main.go:ApprovalBusinessTypeRefund -> refund.Service.Handle" + ], + "application_service_query": [ + "internal/service/refund/service.go:Service.Approve/validateApprovedRefundAmount", + "internal/service/refund/approval_decision.go:Service.applyApprovedDecision" + ], + "domain_state_amount": [ + "Service 和企业微信受控审批决策路径可传递/处理 approvedAmount == 0;公开 DTO 字段仍标记 omitempty,min=1" + ], + "store_migration_config": [ + "internal/model/refund.go:RefundRequest", + "migrations/archive/000093_create_refund_request.up.sql" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'internal/model/dto/refund_dto.go'\"'\"', '\"'\"'validate:\"omitempty,min=1\"'\"'\"'), ('\"'\"'internal/handler/admin/refund.go'\"'\"', '\"'\"'func (h *RefundHandler) Approve'\"'\"'), ('\"'\"'internal/handler/admin/refund.go'\"'\"', '\"'\"'if err := c.BodyParser(&req); err != nil'\"'\"'), ('\"'\"'internal/handler/admin/refund.go'\"'\"', '\"'\"'h.service.Approve(c.UserContext(), uint(id), &req)'\"'\"'), ('\"'\"'internal/service/refund/service.go'\"'\"', '\"'\"'if approvedAmount < 0'\"'\"'), ('\"'\"'internal/service/refund/approval_decision.go'\"'\"', '\"'\"'approvedAmount := refund.RequestedRefundAmount'\"'\"'), ('\"'\"'internal/service/refund/service.go'\"'\"', '\"'\"'审批退款金额不能大于申请退款金额'\"'\"'), ('\"'\"'internal/service/refund/service.go'\"'\"', '\"'\"'申请退款金额不能大于订单实收金额'\"'\"')]; ordered=[('\"'\"'internal/handler/admin/refund.go'\"'\"', ['\"'\"'func (h *RefundHandler) Approve'\"'\"', '\"'\"'if err := c.BodyParser(&req); err != nil'\"'\"', '\"'\"'h.service.Approve(c.UserContext(), uint(id), &req)'\"'\"'])]; absent=[('\"'\"'internal/handler/admin/refund.go'\"'\"', '\"'\"'ValidateStruct'\"'\"'), ('\"'\"'internal/handler/admin/refund.go'\"'\"', '\"'\"'.Validate('\"'\"')]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; unordered=[]; exec(\"for p,ts in ordered:\\n text=Path(p).read_text(); pos=-1; ok=True\\n for t in ts:\\n pos=text.find(t,pos+1)\\n if pos<0: ok=False; break\\n if not ok: unordered.append(f'\"'\"'{p}:{ts}'\"'\"')\"); present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in absent if t in Path(p).read_text()]; assert not missing and not unordered and not present, {'\"'\"'missing'\"'\"':missing,'\"'\"'unordered'\"'\"':unordered,'\"'\"'present'\"'\"':present}; print('\"'\"'OK: refund-approval::非法退款金额仍被拒绝'\"'\"')'", + "literal_output": [ + "OK: refund-approval::非法退款金额仍被拒绝" + ], + "exit_status": 0 + } + }, +{ + "capability": "shop-bulk-import", + "requirement": "导入输入必须在生成 SQL 前完成全量预检", + "spec": "openspec/specs/shop-bulk-import/spec.md", + "entries": [ + "scripts/migration/import_shops.py" + ], + "handler_consumer_job": [ + "scripts/migration/import_shops.py:main" + ], + "application_service_query": [ + "scripts/migration/lib/shop_import.py:load_config/load_plan" + ], + "domain_state_amount": [ + "scripts/migration/lib/shop_import.py:ShopImportPlan.errors" + ], + "store_migration_config": [ + "scripts/migration/config/shop_bulk_import.yaml" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'scripts/migration/import_shops.py'\"'\"', '\"'\"'plan = load_plan(csv_path, config)'\"'\"'), ('\"'\"'scripts/migration/import_shops.py'\"'\"', '\"'\"'if plan.errors:'\"'\"'), ('\"'\"'scripts/migration/lib/shop_import.py'\"'\"', '\"'\"'def load_plan'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: shop-bulk-import::导入输入必须在生成 SQL 前完成全量预检'\"'\"')'", + "literal_output": [ + "OK: shop-bulk-import::导入输入必须在生成 SQL 前完成全量预检" + ], + "exit_status": 0 + } +}, +{ + "capability": "shop-bulk-import", + "requirement": "生成的 SQL 必须在写入前校验目标库状态", + "spec": "openspec/specs/shop-bulk-import/spec.md", + "entries": [ + "scripts/migration/import_shops.py" + ], + "handler_consumer_job": [ + "scripts/migration/import_shops.py:main" + ], + "application_service_query": [ + "scripts/migration/lib/shop_sql.py:build_shop_sql/_preflight_sql" + ], + "domain_state_amount": [ + "scripts/migration/lib/shop_sql.py:DO $shop_import_guard$" + ], + "store_migration_config": [ + "scripts/migration/lib/shop_sql.py:tb_role/tb_account/tb_shop guards" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'scripts/migration/lib/shop_sql.py'\"'\"', '\"'\"'所有目标库守卫必须在业务写入前完成。'\"'\"'), ('\"'\"'scripts/migration/lib/shop_sql.py'\"'\"', '\"'\"'DO $shop_import_guard$'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: shop-bulk-import::生成的 SQL 必须在写入前校验目标库状态'\"'\"')'", + "literal_output": [ + "OK: shop-bulk-import::生成的 SQL 必须在写入前校验目标库状态" + ], + "exit_status": 0 + } +}, +{ + "capability": "shop-bulk-import", + "requirement": "批量导入 SQL 必须创建完整的店铺初始事实", + "spec": "openspec/specs/shop-bulk-import/spec.md", + "entries": [ + "scripts/migration/import_shops.py" + ], + "handler_consumer_job": [ + "scripts/migration/import_shops.py:main" + ], + "application_service_query": [ + "scripts/migration/lib/shop_sql.py:_row_sql/_audit_sql" + ], + "domain_state_amount": [ + "scripts/migration/lib/shop_import.py:initial_password_hash" + ], + "store_migration_config": [ + "scripts/migration/lib/shop_sql.py:tb_shop/tb_account/tb_account_role/tb_shop_role/tb_agent_wallet/tb_audit_event" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'scripts/migration/lib/shop_sql.py'\"'\"', '\"'\"'INSERT INTO tb_shop'\"'\"'), ('\"'\"'scripts/migration/lib/shop_sql.py'\"'\"', '\"'\"'INSERT INTO tb_account_role'\"'\"'), ('\"'\"'scripts/migration/lib/shop_sql.py'\"'\"', '\"'\"'INSERT INTO tb_agent_wallet'\"'\"'), ('\"'\"'scripts/migration/lib/shop_sql.py'\"'\"', '\"'\"'INSERT INTO tb_audit_event'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: shop-bulk-import::批量导入 SQL 必须创建完整的店铺初始事实'\"'\"')'", + "literal_output": [ + "OK: shop-bulk-import::批量导入 SQL 必须创建完整的店铺初始事实" + ], + "exit_status": 0 + } +}, +{ + "capability": "shop-bulk-import", + "requirement": "导入执行必须由维护者审核后以单事务手工确认", + "spec": "openspec/specs/shop-bulk-import/spec.md", + "entries": [ + "scripts/migration/import_shops.py" + ], + "handler_consumer_job": [ + "scripts/migration/import_shops.py:main" + ], + "application_service_query": [ + "scripts/migration/lib/shop_sql.py:build_shop_sql" + ], + "domain_state_amount": [ + "scripts/migration/lib/shop_sql.py:review before psql -1" + ], + "store_migration_config": [ + "scripts/migration/lib/shop_sql.py:psql -1" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'scripts/migration/lib/shop_sql.py'\"'\"', '\"'\"'执行前必须审核结果清单与摘要。'\"'\"'), ('\"'\"'scripts/migration/lib/shop_sql.py'\"'\"', '\"'\"'psql -1 -v ON_ERROR_STOP=1 -f <此文件>'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: shop-bulk-import::导入执行必须由维护者审核后以单事务手工确认'\"'\"')'", + "literal_output": [ + "OK: shop-bulk-import::导入执行必须由维护者审核后以单事务手工确认" + ], + "exit_status": 0 + } +}, +{ + "capability": "shop-bulk-import", + "requirement": "导入结果必须可审核和重跑定位", + "spec": "openspec/specs/shop-bulk-import/spec.md", + "entries": [ + "scripts/migration/import_shops.py" + ], + "handler_consumer_job": [ + "scripts/migration/import_shops.py:main" + ], + "application_service_query": [ + "scripts/migration/lib/shop_import.py:write_errors/write_results/write_summary" + ], + "domain_state_amount": [ + "scripts/migration/lib/shop_import.py:batch_id/ShopImportResult" + ], + "store_migration_config": [ + "scripts/migration/config/shop_bulk_import.yaml" + ], + "verification": { + "command": "python3 -c 'from pathlib import Path; checks=[('\"'\"'scripts/migration/import_shops.py'\"'\"', '\"'\"'write_results(result_path, plan.results)'\"'\"'), ('\"'\"'scripts/migration/import_shops.py'\"'\"', '\"'\"'write_summary(summary_path, plan, sql_path)'\"'\"'), ('\"'\"'scripts/migration/lib/shop_import.py'\"'\"', '\"'\"'batch_id={plan.config.batch_id}'\"'\"')]; negative=[]; missing=[f'\"'\"'{p}:{t}'\"'\"' for p,t in checks if t not in Path(p).read_text()]; present=[f'\"'\"'{p}:{t}'\"'\"' for p,t in negative if t in Path(p).read_text()]; assert not missing and not present, {'\"'\"'missing'\"'\"': missing, '\"'\"'present'\"'\"': present}; print('\"'\"'OK: shop-bulk-import::导入结果必须可审核和重跑定位'\"'\"')'", + "literal_output": [ + "OK: shop-bulk-import::导入结果必须可审核和重跑定位" + ], + "exit_status": 0 + } +} ] diff --git a/openspec/changes/add-employee-collection-bills/design.md b/openspec/changes/add-employee-collection-bills/design.md index 62e349e..fd588ae 100644 --- a/openspec/changes/add-employee-collection-bills/design.md +++ b/openspec/changes/add-employee-collection-bills/design.md @@ -4,6 +4,8 @@ 本设计只处理上线后事件。线下付款并非本系统支付渠道事实,企业微信审批人员以第三方记录核验,因此本地只留存申请人声明、附件、冻结快照和企业微信最终结果。 +术语边界:员工代收款账单是后台账号代客户经办业务形成的本地暂挂欠款,不等同于来源订单、客户付款或企业微信审批单。核销申请对应一笔外部付款及一次企业微信审批实例;核销分摊是该申请对单张账单确认的本次金额,账单核销状态与申请审批状态独立保存。企业微信审批实例仅是本地业务单的外部审批渠道生命周期事实;外部交易流水号可由 OCR 预填,但必须经人工确认。线下收款方式是固定注册分类下的业务字典项,不等同于线上支付方式枚举或收款账户目录;已被引用的字典项只可停用并保留历史名称快照。 + ## Goals / Non-Goals **Goals:** @@ -16,6 +18,8 @@ - 不回填历史业务,不允许“其他”来源手工建账,不处理平台代理 C 端资产钱包充值。 - 不以账单功能重构订单、代理充值或企业微信通用审批模块。 + + ## Decisions ### 1. 账单、申请、分摊分表保存 diff --git a/openspec/changes/add-payment-merchant-pools/tasks.md b/openspec/changes/add-payment-merchant-pools/tasks.md index f94e8c8..2ed72f1 100644 --- a/openspec/changes/add-payment-merchant-pools/tasks.md +++ b/openspec/changes/add-payment-merchant-pools/tasks.md @@ -18,4 +18,4 @@ - [x] 3.1 更新支付、商户、商户池和微信授权管理接口 OpenAPI、可执行路由说明及生成入口,覆盖权限、启停、成员排序、引用锁定、二次确认删除、零 active 失败提示、三类新支付和后台排除边界。敏感凭证的脱敏/禁止泄露仅适用于普通 DTO、错误、日志、审计、支付快照、导出及非专用管理响应;超级管理员和平台用户的专用管理列表、详情响应继续允许返回完整凭证。文档与 OpenAPI 示例只能使用占位值,绝不写入真实凭据。不得把具体迁移编号或 candidate 目录写入契约,不新增测试体系;不得暗示本 Change 实现渠道退款 API。 - [x] 3.2 富友 B:保持现有 `CommonQuery` 请求格式、签名算法、验签、状态映射和恢复语义;仅改本地双读配置来源与商户加载,使双读查单可按商户当前凭证调用。不得改变协议、验签、状态解释、恢复规则或业务能力,不得增加退款能力;未第三方实测可记录但不得作为任务或归档阻塞。 - [x] 3.3 在本地工作区以明确 `DB_*` 指向维护者提供的 `junhong_cmp_test` PostgreSQL 与 Redis DB6,完成 migration up/down/up 和数据行为验证:零/一/多 active、支付/授权凭证缺失、三类新支付始终走商户池、后台 wallet/offline 排除、缺失配置、三种轮询、统计世代与迟到成功、凭证轮换及缓存一致性、并发成功累计、商户停用后的历史回调/查单/退款 A、merchant ID 为空旧单兼容、敏感字段不泄露和重复处理幂等。允许仅为当前 Change 创建/删除 fixtures,禁止重置整个库;仅连接、迁移或实际行为失败时阻塞对应场景。完成后才提交推送 Iteration/8-11;每个场景提供可观察状态/错误/快照/统计事实证据;不新增测试体系,不验收实际渠道退款成功。 -- [ ] 3.4 Iteration/8-11 分支 Gitea 以本次提交 SHA 仅构建/部署 `cmp-test` 测试镜像并检查 migration version,不自动执行 migration up/down 或重置整库;记录 API/Worker 容器状态、健康检查和 `/opt/junhong_cmp/logs` 的有限日志,不建运行时开关且不是生产发布。部署后验证三类后续新支付立即走商户池并冻结路由,无池、成员或停用池明确失败且无旧创建回退,merchant ID 为空历史支付仍读旧路径。故障仅在双读版本上前向修复;存在非空 `merchant_id` 支付、成功事实或新配置时,禁止部署不识别新路由的旧二进制和破坏性 down。后续 Apply 必须删除商户池新支付创建开关、所有引用及任何关闭后恢复旧创建的代码。 \ No newline at end of file +- [x] 3.4 Iteration/8-11 分支 Gitea 以本次提交 SHA 仅构建/部署 `cmp-test` 测试镜像并检查 migration version,不自动执行 migration up/down 或重置整库;记录 API/Worker 容器状态、健康检查和 `/opt/junhong_cmp/logs` 的有限日志,不建运行时开关且不是生产发布。部署后验证三类后续新支付立即走商户池并冻结路由,无池、成员或停用池明确失败且无旧创建回退,merchant ID 为空历史支付仍读旧路径。故障仅在双读版本上前向修复;存在非空 `merchant_id` 支付、成功事实或新配置时,禁止部署不识别新路由的旧二进制和破坏性 down。后续 Apply 必须删除商户池新支付创建开关、所有引用及任何关闭后恢复旧创建的代码。 \ No newline at end of file diff --git a/openspec/changes/archive/2026-09-08-repair-context-reset-evidence/.openspec.yaml b/openspec/changes/archive/2026-09-08-repair-context-reset-evidence/.openspec.yaml new file mode 100644 index 0000000..7a8e2be --- /dev/null +++ b/openspec/changes/archive/2026-09-08-repair-context-reset-evidence/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-08 diff --git a/openspec/changes/archive/2026-09-08-repair-context-reset-evidence/design.md b/openspec/changes/archive/2026-09-08-repair-context-reset-evidence/design.md new file mode 100644 index 0000000..aab7480 --- /dev/null +++ b/openspec/changes/archive/2026-09-08-repair-context-reset-evidence/design.md @@ -0,0 +1,72 @@ +## Context + +见 `proposal.md`。`scripts/context-health.sh` 以集合相等检查主 Specs、`requirement-evidence.json` 和 `entry-capability-requirement-matrix.json`;调查确认主 Specs 为 89 条、现有证据为 53 条,缺失 36 条,且没有过期 evidence。矩阵也缺少同一 36 条 Requirement 链接。 + +## Goals / Non-Goals + +**Goals:** +- 仅以当前可达源码、迁移/脚本、配置和已有归档验证记录补齐 36 条的静态可追溯证据。 +- 令两个 JSON 文件分别满足脚本的 Requirement 集合双向覆盖;保持 HTTP 路由集和 Asynq 注册入口集不变。 +- 每条证据在 `entries`、`handler_consumer_job`、`application_service_query`、`domain_state_amount`、`store_migration_config` 和 `verification` 中只引用实际存在的事实。 + +**Non-Goals:** +- 不修改 `openspec/specs/**`、业务代码、迁移、测试、运行配置、路由或 Worker 注册。 +- 不以历史 Change 的完成状态、推测的调用链或无法重现的生产结果替代当前可达证据。 +- 不添加、删除或重分类既有 HTTP/async 矩阵入口。 + +## Decisions + +### 1. 固定缺失集合,按 capability 分组补行 + +以下是唯一可新增的 evidence Requirement 键;实现阶段先由同一解析规则重算,若集合变化则停止并重新调查: + +| capability | 缺失 Requirement | +| --- | --- | +| `agent-funds-commission` | 代理在线充值可用支付方式按支付配置判定 | +| `agent-open-api` | 代理开放接口查询不触发可靠卡观测 | +| `external-integration` | 富友主扫统一下单与订单查询;高频轮询外部交互日志保留边界;外部交互日志归档与留存受控执行;外部交互日志逐日物理留存 | +| `operations-audit` | 轮询审计事实保留边界;审计归档与日留存受控执行;审计在线数据逐日物理留存;日留存独立运行日志;审计调查留存边界连续 | +| `package-lifecycle` | 资产套餐层级投影;套餐历史整组筛选与分页;套餐历史稳定排序;物理缺失与不可展示关系区分;套餐历史读取范围与兼容边界 | +| `package-queue-activation` | 当前主套餐过期后自动激活下一个;孤儿待生效套餐必须公平恢复;Asynq 激活结果必须准确且可重试 | +| `polling-load-control` | 轮询全局并发与背压;正常无变化观测的低写入处理;业务变化与轮询异常仍可追溯 | +| `polling-operations` | 停复机遵循实际生效实名策略 | +| `qicheng-migration-package-lifecycle-overrides` | 迁移配置支持逐卡套餐生命周期覆盖;覆盖必须完整且可验证;未覆盖资产维持严格冲突阻断 | +| `qicheng-migration-package-usage` | 迁移套餐使用记录保存计时条款快照;迁移套餐使用 SQL 满足快照完整性约束 | +| `refund-approval` | 零金额退款审批通过;零金额退款不产生资金回款;非法退款金额仍被拒绝 | +| `shop-bulk-import` | 导入输入必须在生成 SQL 前完成全量预检;生成的 SQL 必须在写入前校验目标库状态;批量导入 SQL 必须创建完整的店铺初始事实;导入执行必须由维护者审核后以单事务手工确认;导入结果必须可审核和重跑定位 | + +### 2. 各类证据的直接来源 + +| Requirement 分组 | 入口/任务与调用链事实 | 状态、存储、迁移或验证事实 | +| --- | --- | --- | +| 代理充值与富友(前两组中相关 2 条) | `internal/handler/admin` 与 `internal/routes` 的代理充值入口;`internal/application/agentrecharge/online_creation.go`;`internal/infrastructure/payment/fuiou_scan.go`;`pkg/fuiou/scan.go` | `model.AgentRechargeRecord.PaymentMethod/PaymentChannel`、支付配置完整性、`integrationlog.Repository`;验证输出只取当前源码可命中的 `PreCreate`、`CommonQuery`、`trans_stat` 映射或既有归档 Change 的可复核命令。 | +| 开放接口本地查询(1 条) | 四个 GET 入口:`/api/open/v1/cards/traffic`、`/cards/status`、`/cards/realname-status`、`/devices/traffic`;`internal/handler/openapi/handler.go` → `internal/service/agent_open_api/service.go` | 对四个读取方法做静态可达性检查:不得引用观测分发/系列调度;只列实际查询的 Store/模型。 | +| 轮询低写入、轮询审计与外部交互(3 条) | 五个已注册 polling Task;`internal/task/polling_base.go` 和各 `polling_*_handler.go`;`internal/infrastructure/cardobservation/series_runner.go` | `internal/application/cardobservation/{apply,network,traffic}.go` 的变化审计/Outbox 事实,及 `internal/task/polling_integration_log.go` 的失败终结事实;正常无变化仅在当前代码存在明确跳过分支时记录,不以 Requirement 文本反推。 | +| 审计/Integration 归档、清理和调查(7 条) | 已注册 `constants.TaskTypeAuditDailyArchive`、`TaskTypeIntegrationDailyArchive`、`TaskTypeAuditDailyRetention`;`cmd/worker/main.go` 的注册/总开关;`internal/task/{integration_archive,audit_monthly_retention}.go` | `internal/application/auditarchive/{service,integration,retention}.go`;`internal/infrastructure/audit/retention.go`;`pkg/constants/audit_archive.go`;`cmd/worker/main.go` 的 `NewRetentionLogger`;`internal/query/{audit,retention}` 的在线边界。 | +| 套餐历史层级(5 条) | `GET /api/admin/assets/{identifier}/packages` 与 `GET /api/c/v1/asset/package-history`;后台 Service、H5 Handler 均调用 `internal/query/asset/package_history.go` | `internal/model/package.go` 的 `master_usage_id`,历史 DTO 的 children/关系状态字段;Query 的整组筛选、排序、物理存在性核对和分页函数。 | +| 套餐队列激活(3 条) | `internal/polling/package_activation_handler.go` 的定时检查、孤儿恢复、任务投递和 `HandlePackageQueueActivation`;已注册 `constants.TaskTypePackageQueueActivation` | `tb_package_usage` 的状态条件更新、`master_usage_id`、优先级排序、Asynq `MaxRetry(3)` 与 Redis 锁冲突返回错误。 | +| 轮询总并发与背压(1 条) | 五个 polling Handler 均调用 `PollingBase.acquireConcurrency`;调度器 `processOneShard` 背压跳过 | `internal/task/polling_base.go` 的双计数 Lua、释放/重入队;`pkg/config/config.go` 的范围校验;`pkg/constants/{redis,polling}.go`。 | +| 停复机实名策略(1 条) | 自动、设备、手动停复机路径经 `internal/service/iot_card/stop_resume_service.go`;策略管理路由保持现有入口 | `isRealnameOK` 的行业卡、已实名、独立卡和设备策略分支;`model.IotCard/Device.RealnamePolicy` 与现有套餐/流量判断。 | +| 奇成套餐迁移(5 条) | 离线迁移入口和配置加载:`scripts/migration/migrate_runtime.py`、`lib/mapping_loader.py`、`lib/sql_builder.py` | `package_lifecycle_overrides`、`tbl_card_life`、`multiple_active_packages`;Step2 INSERT 的 `expiry_base_snapshot`、`calendar_type_snapshot`、`duration_months_snapshot`、`duration_days_snapshot`。 | +| 零金额退款(3 条) | `POST /api/admin/refunds/{id}/approve` 与企业微信审批共用的退款决策路径;`internal/service/refund/{approval_decision,service}.go` | `validateApprovedRefundAmount` 拒绝负数;事务更新退款/订单;`refundWalletPayment` 在 `amount == 0` 直接返回,不写钱包回款。 | +| 店铺批量导入(5 条) | 离线 CLI `scripts/migration/import_shops.py` → `lib/shop_import.py` → `lib/shop_sql.py` | CSV/配置预检、SQL 事务起始守卫、店铺/账号/角色/双钱包/审计 SQL、bcrypt 哈希、同批次错误/结果/摘要产物;运行说明明确不连接目标 PostgreSQL。 | + +每个 `verification` 对象保留一条可直接执行的本地只读命令及其确定性源码文字命中;没有稳定文字命中的调用链,使用可重跑的只读脚本/命令输出。不得杜撰隔离库、生产库、外部渠道或已删除临时 smoke 的结果。 + +### 3. 两个 JSON 的最小同步策略 + +1. `requirement-evidence.json`:仅追加上述 36 个唯一键各一行;不改动现有 53 行,不新增过期键。 +2. `entry-capability-requirement-matrix.json`:仅把上述 36 个键链接到现有、已注册的 HTTP 或 async 行。HTTP 条目保持与 Specs 提取路由的集合相同;async 条目保持与 Worker 注册常量集合相同。 +3. 对无 HTTP/async 入口的两类离线流程追加 `cli` 行,入口分别为 `scripts/migration/migrate_runtime.py` 与 `scripts/migration/import_shops.py`;现有健康脚本只比较 `http` 与 `async` 的入口集合,`cli` 行仅承担这五条离线 Requirement 的可追溯链接,不改变该比较。不得把离线流程伪挂到 HTTP 或 Asynq 入口。 +4. 写入前用集合差验证恰为 36;写入后验证 evidence 键集和 matrix 链接集均等于主 Specs 键集,并确认 HTTP/async 入口集合未漂移。 + +### 4. 证据选择优先级 + +当前可达实现与可复跑只读输出优先;`migrations/`、脚本和显式配置是存储/离线事实;已归档 Change 只用于发现来源和复核历史验证边界,不能单独作为当前实现证据。无证明项保留为缺口并停止,不以相近功能、相同命名或 Spec 场景推断。 + +## Risks / Trade-offs + +- `polling-load-control` 和低写入类 Requirement 跨五个 Worker Handler;只引用共享基类无法证明每个入口均接入,必须在最终写入前逐 Handler 核对获取、释放和令牌不足重入队。 +- 离线迁移与店铺导入没有 HTTP/async 注册入口。将其错误塞入现有入口会使矩阵语义失真;是否允许 `cli` 类型是唯一可能需要确认的矩阵契约缺口。 +- 历史 Change 中有未完成验证任务。它们不能写成已验证事实;证据只能引用仍在仓库中的源码、脚本、迁移、配置和可重跑只读检查。 +- JSON 人工编辑容易造成重复键或遗漏链接;实现阶段必须在写入前后解析并做三集合精确比较,再运行 `./scripts/context-health.sh`。 \ No newline at end of file diff --git a/openspec/changes/archive/2026-09-08-repair-context-reset-evidence/proposal.md b/openspec/changes/archive/2026-09-08-repair-context-reset-evidence/proposal.md new file mode 100644 index 0000000..d3e91e6 --- /dev/null +++ b/openspec/changes/archive/2026-09-08-repair-context-reset-evidence/proposal.md @@ -0,0 +1,26 @@ +## Why + +`./scripts/context-health.sh` 已将主 Specs 的 Requirement 集合与 `requirement-evidence.json` 的集合设为严格双向约束。当前 89 条 Requirement 中有 36 条没有证据记录,且入口矩阵也没有链接这 36 条,导致上下文健康检查无法通过。需要仅补齐已存在实现的可追溯证据,不改变任何业务契约或运行行为。 + +## What Changes + +- 对 36 条缺失的主 Spec Requirement 补充 `docs/verification/context-reset/requirement-evidence.json` 证据行;每行只记录经源码、迁移、配置或既有可执行验证直接证明的入口、调用链、状态/存储事实和验证输出。 +- 在 `docs/verification/context-reset/entry-capability-requirement-matrix.json` 为相同 36 条 Requirement 建立最小入口链接;复用现有 HTTP 或已注册异步入口,离线迁移/导入流程仅使用不参与 HTTP/异步覆盖集合比较的已证实离线入口记录。 +- 保持证据集合、主 Specs Requirement 集合和入口矩阵 Requirement 链接集合一致;不添加过期条目。 +- 不修改主 Specs、业务代码、迁移、测试体系、运行配置或 `scripts/context-health.sh`。 + +## Capabilities + +### New Capabilities + +- 无。 + +### Modified Capabilities + +- `system-operations`: 为既有上下文健康检查的 Requirement 证据覆盖门禁补充可观察的基线维护契约;不改变 API、Worker、数据库或运行配置行为。 + +## Impact + +- Affected files: `docs/verification/context-reset/requirement-evidence.json`、`docs/verification/context-reset/entry-capability-requirement-matrix.json`;本 Change 的规划工件。 +- Affected validation: `./scripts/context-health.sh` 的 Requirement 证据链与入口矩阵双向覆盖检查。 +- No production impact: 不执行数据库写入、迁移、外部渠道调用或运行配置调整。 \ No newline at end of file diff --git a/openspec/changes/archive/2026-09-08-repair-context-reset-evidence/specs/system-operations/spec.md b/openspec/changes/archive/2026-09-08-repair-context-reset-evidence/specs/system-operations/spec.md new file mode 100644 index 0000000..042e5d8 --- /dev/null +++ b/openspec/changes/archive/2026-09-08-repair-context-reset-evidence/specs/system-operations/spec.md @@ -0,0 +1,13 @@ +## ADDED Requirements + +### Requirement: 上下文健康检查证据链完整 + +系统 SHALL 将当前主 Specs 的每一条 Requirement 与一条可追溯证据记录及至少一个已证实入口关联。证据记录与入口关联的 Requirement 集合 MUST 分别和主 Specs Requirement 集合双向一致;证据不足时健康检查 MUST 失败,而不得以假定的实现路径补齐。 + +#### Scenario: 主 Spec Requirement 缺少证据 +- **WHEN** 主 Specs 中存在 Requirement,但证据记录或入口矩阵未链接该 Requirement +- **THEN** 上下文健康检查失败并阻止将验证基线宣告为健康 + +#### Scenario: 补齐已实现 Requirement 的证据 +- **WHEN** 每条 Requirement 均有经源码、迁移、配置或既有验证输出直接证明的证据记录和入口关联 +- **THEN** 上下文健康检查的证据链与入口矩阵覆盖检查通过,且不改变业务接口、数据或运行行为 diff --git a/openspec/changes/archive/2026-09-08-repair-context-reset-evidence/tasks.md b/openspec/changes/archive/2026-09-08-repair-context-reset-evidence/tasks.md new file mode 100644 index 0000000..bafe2cd --- /dev/null +++ b/openspec/changes/archive/2026-09-08-repair-context-reset-evidence/tasks.md @@ -0,0 +1,23 @@ +## 1. 重新确认基线与证据边界 + +- [x] 1.1 解析当前主 Specs、`requirement-evidence.json` 和入口矩阵,确认待补 Requirement 键仍恰为 design.md 所列 36 条,且现有 evidence 没有过期键。 +- [x] 1.2 对每条待补 Requirement 静态追踪其路由、已注册任务或离线 CLI 至 Handler/Consumer、Application/Service/Query、状态/存储事实;缺少直接证明时停止,不写推测性证据。 +- [x] 1.3 为每条证据选择一个可重跑且只读的源码/脚本检查命令与确定性命中输出;不使用生产、外部渠道、数据库写入或已删除临时 smoke 结果。 + +## 2. 补齐 Requirement 证据记录 + +- [x] 2.1 在 `docs/verification/context-reset/requirement-evidence.json` 仅追加代理充值、开放接口、外部交互、审计归档、套餐历史、套餐激活与轮询类 Requirement 的直接证据行。 +- [x] 2.2 在同一文件仅追加实名策略、奇成迁移、零金额退款和店铺批量导入类 Requirement 的直接证据行,记录脚本/迁移事实而不虚构 HTTP 或 Asynq 调用链。 +- [x] 2.3 解析写入后的 JSON,确认 89 个唯一 `capability::requirement` 键与主 Specs 双向一致,保留原有 53 行内容且无重复或过期键。 + +## 3. 同步入口能力矩阵 + +- [x] 3.1 将 HTTP 可达的待补 Requirement 仅链接到已有路由行,将 Worker 行为仅链接到已有注册 async 行;不得新增、删除或重命名 HTTP/async 入口。 +- [x] 3.2 为奇成迁移和店铺导入的五条离线 Requirement 添加两个 `cli` 入口行:`scripts/migration/migrate_runtime.py` 与 `scripts/migration/import_shops.py`,不影响 HTTP/async 入口集合比较。 +- [x] 3.3 解析矩阵并确认其 Requirement 链接集合与主 Specs 完全相同,HTTP 路由集合和 async 注册常量集合保持不变。 + +## 4. 验证与交付 + +- [x] 4.1 运行只读集合核对,逐条复核新增 evidence 文件路径、符号和验证命令输出仍存在;对无直接证明的条目撤回并报告缺口。 +- [x] 4.2 运行 `./scripts/context-health.sh`,确认 Requirement 证据链、入口矩阵、OpenSpec 校验和既有生成确定性检查全部通过。 +- [x] 4.3 核对变更范围仅限两个验证 JSON 与本 Change 工件;不修改主 Specs、业务代码、迁移、测试体系或运行配置。 \ No newline at end of file diff --git a/openspec/specs/system-operations/spec.md b/openspec/specs/system-operations/spec.md index 0a67da0..9c2dc41 100644 --- a/openspec/specs/system-operations/spec.md +++ b/openspec/specs/system-operations/spec.md @@ -26,6 +26,18 @@ - **WHEN** 超级管理员请求更新 - **THEN** 系统拒绝更新并保留原数据库事实;可审计失败按当前接缝记录 +### Requirement: 上下文健康检查证据链完整 + +系统 SHALL 将当前主 Specs 的每一条 Requirement 与一条可追溯证据记录及至少一个已证实入口关联。证据记录与入口关联的 Requirement 集合 MUST 分别和主 Specs Requirement 集合双向一致;证据不足时健康检查 MUST 失败,而不得以假定的实现路径补齐。 + +#### Scenario: 主 Spec Requirement 缺少证据 +- **WHEN** 主 Specs 中存在 Requirement,但证据记录或入口矩阵未链接该 Requirement +- **THEN** 上下文健康检查失败并阻止将验证基线宣告为健康 + +#### Scenario: 补齐已实现 Requirement 的证据 +- **WHEN** 每条 Requirement 均有经源码、迁移、配置或既有验证输出直接证明的证据记录和入口关联 +- **THEN** 上下文健康检查的证据链与入口矩阵覆盖检查通过,且不改变业务接口、数据或运行行为 + ## 可达操作索引 本节只用于入口导航,不是行为 Requirement;业务义务以上述 Requirements 为准。 diff --git a/pkg/config/defaults/config.yaml b/pkg/config/defaults/config.yaml index 24da7f4..6916b88 100644 --- a/pkg/config/defaults/config.yaml +++ b/pkg/config/defaults/config.yaml @@ -154,6 +154,7 @@ approval: legacy_refund_manual_enabled: true legacy_offline_recharge_pay_enabled: true + # 企业微信 Adapter 配置 wecom: base_url: "https://qyapi.weixin.qq.com"