feat: sync lijiaoqiao implementation and staging validation artifacts

This commit is contained in:
Your Name
2026-03-31 13:40:00 +08:00
parent 0e5ecd930e
commit e9338dec28
686 changed files with 29213 additions and 168 deletions

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
.tools/
# Local/sensitive env files
scripts/supply-gate/.env
scripts/supply-gate/.env.*
!scripts/supply-gate/.env.example
# Generated raw artifacts and process files
tests/supply/artifacts/
reports/gates/*.log
reports/gates/*.out.log
reports/gates/*.pid
# Local build output
platform-token-runtime/platform-token-runtime

View File

@@ -1,7 +1,7 @@
# 唯一验收门禁表Single Source of Truth
- 版本v1.1
- 日期2026-03-24
- 版本v1.2
- 日期2026-03-27
- 状态:生效
- 用途:统一 S0/S1/S2 的验收阈值、判定逻辑与阻断动作,消除多文档阈值漂移。
@@ -35,12 +35,19 @@
| M-014 | `platform_credential_ingress_coverage_pct` | 使用平台凭证入站请求/总入站请求 | =100% | <100% | 鉴权日志/网关审计 |
| M-015 | `direct_supplier_call_by_consumer_events` | 需求方绕过平台直连供应方事件数 | =0 | >0 即 P0 | 出网审计/安全事件中心 |
| M-016 | `query_key_external_reject_rate_pct` | 外部 query key 被拒绝数/外部 query key 请求总数 | =100% | <100% | 网关拦截日志 |
| M-017 | `dependency_compat_audit_pass_pct` | 依赖版本兼容审计通过项/应审计项 | =100% | <100% | 依赖审计报告 |
| M-018 | `stage_quality_gate_pass_pct` | 分阶段质量门禁通过项/应通过项 | =100% | <100% | CI Gate 报告 |
| M-019 | `requirement_traceability_coverage_pct` | 已追踪需求数/应追踪需求数 | =100% | <100% | 追踪矩阵 |
| M-020 | `escaped_p0_defects_count` | 生产放出后 P0 缺陷数 | =0 | >0 | 生产事故平台 |
| M-021 | `token_runtime_readiness_pct` | token 运行态必备能力已实现项/应实现项 | =100% | <100% | TOK 验收报告 |
口径补充:
1. 主路径端点集合固定为 `/v1/chat/completions``/v1/messages``/v1/responses``/v1beta/*`
2. `/responses` 等 alias 入口必须在 Ingress 归一后再统计。
3. 国内平台分类来源固定为配置表 `gateway_cn_platforms`,禁止 SQL 硬编码。
4. 需求方仅可使用平台签发凭证访问平台入口,禁止获取供应方上游凭证。
5. 依赖兼容审计口径固定为“SBOM + 锁文件差异 + 兼容矩阵 + 风险清单”四件套,缺一视为未通过。
6. 分阶段质量门禁固定为 G0/G1/G2/G3/G4/G5禁止跳阶段放行。
---
@@ -58,17 +65,19 @@
| Gate ID | 场景 | 必达条件 | 不通过动作 | 责任人 |
|---|---|---|---|---|
| G-S1-1 | 灰度7天上线门禁 | M-001>=99.9%M-004<=0.1%30分钟内回滚演练通过 | 不得升至全量;维持灰度并整改 | 平台+SRE |
| G-S1-2 | 发布前兼容门禁 | Schema/Behavior/Performance 三重Gate全部通过 | 阻断发布 | 架构+QA |
| G-S1-2 | 发布前兼容门禁 | Schema/Behavior/Performance 三重Gate全部通过,且 M-017=100% | 阻断发布 | 架构+QA |
| G-S1-3 | 凭证边界门禁 | M-013=0M-014=100%M-016=100% | 阻断发布并触发安全复盘 | 安全+平台 |
| G-S1-4 | 分阶段质量门禁 | M-018=100%M-019=100% | 阻断发布并回到失败阶段整改 | 架构+QA+PMO |
| G-S1-5 | token 运行态门禁 | M-021=100% | 阻断发布并冻结 SUP 升波 | 架构+安全+平台 |
### 3.3 S2 阶段门禁(替换核心)
| Gate ID | 阶段 | 必达条件 | 升波条件 | 阻断条件 | 不通过动作 |
|---|---|---|---|---|---|
| G-S2-A | 10% | M-001>=99.9%M-002<=60M-010>=99%M-004<=0.1%M-009>=95%M-014=100% | 连续2周达标 | 任一红线触发 | 回切 subapi修复后重试 |
| G-S2-B | 30% | M-001>=99.95%M-003<=100M-009>=97%M-014=100% | 连续2周达标 | M-004>0.1% 或 P0事故 | 暂停升波,补救后复核 |
| G-S2-C1 | 40%中间检查点 | M-001>=99.95%M-002<=60M-003<=100M-010>=99.5%M-004<=0.1%M-009>=97%M-013=0M-014=100%M-015=0 | GO/CONDITIONAL GO | 任一红灯阈值 | 决策会:继续/附条件继续/回滚 |
| G-S2-C2 | 60%终验 | M-006>=60%M-007=100%M-004<=0.1%M-005<=0.01%M-008>=99.9%M-001>=99.95%M-013=0M-014=100%M-015=0M-016=100% | 通过后S2完成 | 任一硬门槛不满足 | 延长S2并冻结升波不降终验目标 |
| G-S2-A | 10% | M-001>=99.9%M-002<=60M-010>=99%M-004<=0.1%M-009>=95%M-014=100%M-017=100%M-018=100%M-021=100% | 连续2周达标 | 任一红线触发 | 回切 subapi修复后重试 |
| G-S2-B | 30% | M-001>=99.95%M-003<=100M-009>=97%M-014=100%M-017=100%M-021=100% | 连续2周达标 | M-004>0.1% 或 P0事故 | 暂停升波,补救后复核 |
| G-S2-C1 | 40%中间检查点 | M-001>=99.95%M-002<=60M-003<=100M-010>=99.5%M-004<=0.1%M-009>=97%M-013=0M-014=100%M-015=0M-018=100%M-019=100%M-021=100% | GO/CONDITIONAL GO | 任一红灯阈值 | 决策会:继续/附条件继续/回滚 |
| G-S2-C2 | 60%终验 | M-006>=60%M-007=100%M-004<=0.1%M-005<=0.01%M-008>=99.9%M-001>=99.95%M-013=0M-014=100%M-015=0M-016=100%M-017=100%M-018=100%M-019=100%M-020=0M-021=100% | 通过后S2完成 | 任一硬门槛不满足 | 延长S2并冻结升波不降终验目标 |
---
@@ -84,6 +93,9 @@
6. `supplier_credential_exposure_events > 0`
7. `direct_supplier_call_by_consumer_events > 0`
8. `platform_credential_ingress_coverage_pct < 100%``query_key_external_reject_rate_pct < 100%`
9. `dependency_compat_audit_pass_pct < 100%`
10. `stage_quality_gate_pass_pct < 100%``requirement_traceability_coverage_pct < 100%`
11. `token_runtime_readiness_pct < 100%`
处理动作:
1. 立即停止升波。

View File

@@ -0,0 +1,23 @@
# API 命名策略:`/supply` vs `/supplier`v1.0
- 日期2026-03-27
- 决策类型:命名规范与兼容策略
- 适用范围:供应侧控制台与平台账务相关 API
## 1. 决策
1. 规范主路径统一采用:`/api/v1/supply/*`
2. 历史兼容路径 `/api/v1/supplier/*` 保留为 alias并标记 `deprecated`
3. 新增接口禁止使用 `/supplier` 前缀。
## 2. 兼容策略
1. 别名路径只做兼容,不扩展新字段。
2. 响应体增加迁移提示字段(如 `deprecation_notice`)或在文档标注迁移窗口。
3. S2 阶段评估 alias 下线时间,提前至少一个版本公告。
## 3. 验收标准
1. OpenAPI 同时存在 canonical 路径与 alias 路径声明。
2. alias 路径标记 `deprecated: true`
3. 追踪矩阵 `api_alias` 字段可定位所有 alias 使用点。

View File

@@ -0,0 +1,128 @@
# 数据库跨域模型与治理基线v1.0
- 版本v1.0
- 日期2026-03-27
- 状态:生效(数据库设计 SSOT 补丁)
- 适用范围S0-S2 执行与验收
- 关联文档:
- `docs/llm_gateway_prd_v1_2026-03-25.md`
- `docs/supply_technical_design_enhanced_v1_2026-03-25.md`
- `docs/technical_architecture_optimized_v2_2026-03-18.md`
- `sql/postgresql/supply_schema_v1.sql`
- `sql/postgresql/supply_schema_v1_patch_2026-03-27.sql`
- `sql/postgresql/platform_core_schema_v1.sql`
---
## 1. 本次补齐的缺口
1. 仅有 `supply_*` 表,缺少 PRD P0/P1 的核心域(租户/项目/鉴权 key/账务总账/审计事件)。
2. 供应域缺少统一加密元数据字段无法审计算法、KMS Key 版本与轮换状态。
3. 缺少统一单位字段quota/cost/amount unit跨域统计口径不稳定。
4. 审计字段不完整request_id、trace_id、IP、operator、version
5. 索引以单列为主,未覆盖高频组合查询(租户+状态+时间)。
---
## 2. 最小跨域表模型(按 PRD P0/P1
| 域 | 表 | 说明 |
|---|---|---|
| Core | `core_tenants` | 组织/租户主实体 |
| Core | `core_projects` | 项目/成本归因单元 |
| IAM | `iam_users` | 用户身份与角色 |
| Auth | `auth_platform_api_keys` | 平台签发凭证(仅 hash不存明文 |
| Billing | `billing_accounts` | 预算账户与余额 |
| Billing | `billing_ledger_entries` | 借贷分录与请求级对账 |
| Routing | `routing_policies` | 策略版本、优先级、生效窗口 |
| Security | `security_kms_key_registry` | KMS Key 与加密算法版本登记 |
| Audit | `audit_events` | 全域审计事件(配置/账务/安全) |
DDL`sql/postgresql/platform_core_schema_v1.sql`
---
## 3. 供应域字段补齐(在 v1 基础上增量)
### 3.1 加密字段(必须)
1. `*_cipher_algo`:默认 `AES-256-GCM`
2. `*_kms_key_alias`KMS key alias非 key 明文)
3. `*_key_version`key 版本号
4. `*_fingerprint`:凭证摘要(不可逆)
5. `last_rotation_at`:上次轮换时间
### 3.2 单位与币种字段(必须)
1. `quota_unit``token/request/credit`
2. `price_unit``per_1m_tokens`
3. `amount_unit``minor`(分/厘)
4. `currency_code`ISO 4217 三位码
### 3.3 审计与并发字段(必须)
1. `request_id`
2. `idempotency_key`
3. `audit_trace_id`
4. `created_ip` / `updated_ip`
5. `version`(乐观锁)
DDL`sql/postgresql/supply_schema_v1_patch_2026-03-27.sql`
---
## 4. 索引策略(高频查询优先)
### 4.1 组合索引
1. `supply_accounts(user_id, status, updated_at desc)`
2. `supply_packages(user_id, status, updated_at desc)`
3. `supply_orders(buyer_user_id, status, created_at desc)`
4. `supply_settlements(user_id, status, updated_at desc)`
5. `billing_ledger_entries(billing_account_id, occurred_at desc)`
### 4.2 部分索引
1. `supply_packages` 的 active 查询(仅 `status=active`
2. `supply_settlements` 的处理中唯一约束(仅 `status=processing`
### 4.3 可观测索引
1. `request_id`
2. `trace_id`
3. `audit_trace_id`
说明:所有关键事件必须具备 request 级反查路径,满足“从告警到原始账务分录”单跳可达。
---
## 5. 迁移顺序与回滚策略
1. Phase-A执行 `platform_core_schema_v1.sql`(新增表,无破坏性)。
2. Phase-B执行 `supply_schema_v1_patch_2026-03-27.sql`(增列+增索引)。
3. Phase-C灰度写入新字段双写不读取
4. Phase-D回填历史数据按日批带校验
5. Phase-E切换读路径到新字段并开启质量门禁。
回滚原则:
1. 新字段只增不删,读路径可切回旧字段。
2. 新索引可独立回退,不影响主流程事务。
3. 任一阶段失败立即冻结下一阶段,不跨阶段带病推进。
---
## 6. 质量验收清单DB
1. 结构验收:新增表/列/索引全部存在,且命名符合规范。
2. 安全验收无明文凭证列hash/指纹字段可用。
3. 一致性验收:账务分录借贷平衡,提现处理中单一约束生效。
4. 审计验收:关键写接口 100% 带 `request_id + trace_id`
5. 性能验收:高频查询 P95 无劣化(对比 patch 前后)。
---
## 7. 约束声明
1. 本文与两个 SQL 文件共同构成数据库实施 SSOT。
2. 任何新增业务功能必须先选择所属域,再定义表/字段/索引,不允许“先代码后补库”。
3. 未通过本清单第 6 章,禁止进入发布门禁 `SUP-008` 与全局 `GO` 评审。

View File

@@ -0,0 +1,93 @@
# 依赖版本兼容性审计基线v1.0
- 版本v1.0
- 日期2026-03-27
- 状态:生效(发布前强制 Gate
- 目标:把“依赖可用”升级为“依赖可审计、可回滚、可阻断”
- 关联文档:
- `docs/technical_architecture_optimized_v2_2026-03-18.md`
- `docs/acceptance_gate_single_source_v1_2026-03-18.md`
- `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
---
## 1. 审计对象与冻结策略
| 层 | 对象 | 冻结规则 |
|---|---|---|
| Runtime | Go / Node / JDK / Python | 仅允许 LTS 或已验证小版本 |
| Data | PostgreSQL / Redis | 生产固定主版本,升级必须灰度 |
| 服务依赖 | subapi / provider SDK | 固定精确版本(`X.Y.Z` |
| 第三方库 | go mod / npm / maven | 锁文件变更必须触发兼容测试 |
| OS 镜像 | 基础镜像 digest | 必须可追溯到 SBOM |
---
## 2. 必交付证据
每次发布候选版本必须提供:
1. `SBOM``reports/dependency/sbom_<date>.spdx.json`
2. `锁文件差异``reports/dependency/lockfile_diff_<date>.md`
3. `兼容矩阵``reports/dependency/compat_matrix_<date>.md`
4. `风险清单``reports/dependency/risk_register_<date>.md`
无上述四项,发布门禁直接阻断。
---
## 3. 兼容性审计流程(分阶段)
### 3.1 Pre-Merge开发合并前
1. 检查 `go.mod/go.sum``package-lock.json/pnpm-lock.yaml``pom.xml` 变化。
2. 依赖变更自动分类Patch/Minor/Major。
3. Major 变更必须附“兼容影响评估 + 回滚预案”。
### 3.2 Nightly每日
1. 运行依赖漏洞扫描CVE/SCA
2. 运行契约回归Schema/Behavior
3. 生成依赖健康趋势(新增高危漏洞数)。
### 3.3 Pre-Release发布前
1. 运行完整兼容回归(兼容三重 Gate + SUP Gate
2. 校验运行时与数据层版本匹配矩阵。
3. 通过后冻结候选构建包与镜像 digest。
### 3.4 Post-Release发布后 24h
1. 监控新增依赖告警、崩溃、性能回退。
2. 若触发 P0/P1 依赖事故,执行自动回滚到上一稳定版本。
---
## 4. 阻断规则(必须)
1. `dependency_compat_audit_pass_pct < 100%`:阻断发布。
2. 新增 Critical CVE 且无缓解:阻断发布。
3. Major 依赖变更无回滚演练记录:阻断发布。
4. subapi/provider SDK 精确版本未锁定:阻断发布。
5. 依赖清单与运行镜像不一致:阻断发布。
---
## 5. 推荐版本兼容矩阵(首版)
| 组件 | 基线版本 | 兼容范围 | 备注 |
|---|---|---|---|
| Go | 1.21.x | 1.21.x | 不跨主版本 |
| PostgreSQL | 15.x | 15.x | SQL 与索引以 PG15 语法为准 |
| Redis | 7.x | 7.x | 限流与缓存行为基于 Redis7 验证 |
| subapi | 精确 `X.Y.Z` | 同 patch | Minor 升级需完整回归 |
| Node前端 | 20.x LTS | 20.x | 锁文件必须纳入审计 |
---
## 6. 与发布门禁对齐
1. 依赖兼容审计结果接入 `acceptance_gate_single_source` 指标 `M-017`
2. 分阶段测试质量接入指标 `M-018`
3. 任一未达标,不得进入 `GO` 结论。

View File

@@ -193,18 +193,23 @@
1. 供应商接入稳定性
2. 计费与账单口径统一
3. 告警渠道与通知系统稳定
4. 数据库跨域模型完整Core/IAM/Auth/Billing/Supply/Audit
5. 依赖版本兼容审计机制可执行SBOM + 锁文件差异 + 兼容矩阵 + 风险清单)
## 9.2 主要风险
1. 功能边界过宽导致首发延期
2. 缺少真实客户数据导致价值表达不足
3. 定价方案与客户价值感知不匹配
4. 依赖版本漂移导致线上兼容故障
5. 阶段门禁不完整导致实现偏离需求主线
## 9.3 风险缓解
1. 坚持 P0 边界P1/P2 延后
2. 以设计合作伙伴反馈驱动迭代
3. 发布前完成小规模定价验证访谈
4. 将依赖兼容审计和分阶段质量门禁纳入发布前阻断条件
## 10. 已冻结决策v1.0 生效)

View File

@@ -0,0 +1,126 @@
# 规划设计闭环执行任务清单Superpowers v2
- 日期2026-03-30
- 基线来源:`docs/plans/2026-03-25-superpowers-execution-tasklist-v1.md`
- 目标:基于最新真实证据重排执行优先级,持续推进到 staging 可复核、可签署。
---
## 1. 实际状态复盘(以证据为准)
### 1.1 已闭环工作流(开发阶段)
1. `WG-A/WG-B/WG-C` 已完成需求冻结、OpenAPI 契约对齐、追踪矩阵一致化。
2. `WG-F/WG-G` 已完成:全局 P0 映射、命名策略、跨文档一致性与最终决议草稿链路。
3. TOK 链路已完成开发闭环(`TOK-002 ~ TOK-007`):包含 runtime、门禁汇总、复审与候选稿生成。
关键证据:
1. `reports/superpowers_execution_progress_2026-03-27.md`
2. `reports/alignment_validation_checkpoint_12_2026-03-30.md` ~ `reports/alignment_validation_checkpoint_27_2026-03-30.md`
3. `reports/gates/superpowers_stage_validation_2026-03-30_212426.md`
### 1.2 未闭环工作流(真实环境)
1. `WG-D/WG-E` 仍未完成真实 staging 证据闭环,当前仅有 local/mock 与 dry-run 证据。
2. 最终签署决议当前为 `NO-GO`,核心阻塞集中在 `F-01/F-02/F-04`P0`F-03`P1
关键证据:
1. `review/final_decision_2026-03-31.md`
2. `reports/supply_gate_review_2026-03-31.md`
3. `reports/token_runtime_implementation_gap_review_2026-03-30.md`
---
## 2. 状态矩阵v2
| 工作流 | 状态 | 说明 | 下一动作 |
|---|---|---|---|
| WG-A 需求冻结 | DONE | 已完成冻结与决议映射 | 仅维护 |
| WG-B 契约对齐 | DONE | OpenAPI 与幂等头已落地 | 仅维护 |
| WG-C 测试矩阵 | DONE | 路径一致化与规则文档已落地 | 仅维护 |
| WG-D 真实联调 | BLOCKED外部依赖 | 缺真实 staging 地址与有效短期 token | 优先解锁 F-01/F-02/F-04 |
| WG-E 报告签署 | BLOCKED依赖 WG-D | 缺真实证据,无法转 GO | 与 WG-D 同步推进 |
| WG-F 一致性收尾 | DONE | 命名策略与映射补齐完成 | 仅维护 |
| WG-G 全局校验 | DONE开发口径 | 校验链路可执行,决议一致性脚本已在跑 | 补真实口径复核 |
| TOK 运行态链路 | DONE开发口径 | M-021 开发阶段 100% | 需 staging 实证回填 |
---
## 3. P0/P1 阻塞项(从最终决议回填)
| 编号 | 等级 | 阻塞描述 | Owner | 截止日期 | 退出条件 |
|---|---|---|---|---|---|
| F-01 | P0 | staging DNS 与 `API_BASE_URL` 可达性修复,重跑 SUP-004~007 | PLAT + QA | 2026-04-01 | `staging_precheck_and_run.sh` 在真实环境 PASS |
| F-02 | P0 | 补齐 M-013~M-016 staging 实测值 | SEC + QA | 2026-04-01 | `sec_sup_boundary_report` 回填真实 PASS |
| F-04 | P0 | token runtime staging 联调取证 | ARCH + PLAT + SEC | 2026-04-03 | `M-021` 与边界指标 staging 证据齐全 |
| F-03 | P1 | M-017/M-018/M-019 连续 7 天趋势证据 | PLAT + PMO | 2026-04-05 | 趋势报告满足 7 天口径 |
---
## 4. 批次执行计划(从 2026-03-30 起)
### Batch-MON-01当前批次先做“可持续执行”能力
1. `MON-001`:新增 Minimax 7 日趋势脚本(监控链路补齐)。
2. `MON-002`:将 Minimax 日快照接入 `superpowers_release_pipeline.sh`(可选、默认关闭、非阻断)。
3. `MON-003`:更新命令手册,补齐执行与断言说明。
4. `MON-004`产出对齐验证报告Checkpoint-28
执行结果2026-03-30
| 任务 | 状态 | 证据 |
|---|---|---|
| MON-001 | DONE | `scripts/ci/minimax_upstream_trend_report.sh` + `reports/gates/minimax_upstream_trend_7d_2026-03-30.md` |
| MON-002 | DONE | `scripts/ci/superpowers_release_pipeline.sh` + `reports/gates/superpowers_release_pipeline_2026-03-30_235224.md` |
| MON-003 | DONE | `docs/supply_gate_command_playbook_v1_2026-03-25.md` |
| MON-004 | DONE | `reports/alignment_validation_checkpoint_28_2026-03-30.md` |
### Batch-STG-01下一批次真实环境解锁
1. `STG-001`:确认真实 staging 网关地址并更新 `scripts/supply-gate/.env`
2. `STG-002`:注入有效 owner/viewer/admin 短期 token禁用占位值
3. `STG-003`:执行 `scripts/ci/staging_release_pipeline.sh`(真实环境,不允许 local/mock
4. `STG-004`:回填 `F-01/F-02/F-04` 证据到最终决议与评审报告。
当前门禁检查2026-03-30
1. `scripts/supply-gate/.env``API_BASE_URL` 仍处于阻塞态(非真实 staging 可发布地址)。
2.`STG-001` 未完成,`STG-003` 当前不得触发真实放行判定。
本机开发测试续跑结果2026-03-31
1. `local/mock` 口径 `staging_release_pipeline` 已复跑通过:`reports/gates/staging_release_pipeline_2026-03-31_100116.md`
2. STG 本地续跑中识别并修复 `PHASE-10` 阻塞M-021 smoke 端口冲突与控制流提前退出)。
3. 修复后 `superpowers_release_pipeline``tok007` 复审链路恢复,结论维持 `CONDITIONAL_GO`
4. `STG-001/STG-002`(真实 staging 地址与真 token仍未完成真实放行证据仍阻塞。
本机端口基线固化结果2026-03-31
1. 已清理蚊子残留进程与冲突端口占用,详见 `reports/gates/local_dev_port_baseline_2026-03-31.md`
2. 清理后再次复测 `staging_release_pipeline``reports/gates/staging_release_pipeline_2026-03-31_100942.md`PASS
3. 对齐验证补充:`reports/alignment_validation_checkpoint_30_2026-03-31.md`
真实 STG 前置自动化补齐2026-03-31
1. 已新增本地 `.env.staging-real` 一键生成脚本:`scripts/ci/generate_local_staging_env.sh`
2. 已新增真实 STG 就绪度检查脚本:`scripts/ci/staging_real_readiness_check.sh`
3. 当前 `.env.staging-real` 就绪检查结论为 `BLOCKED``reports/gates/staging_real_readiness_2026-03-31_110213.md`
4. 阻塞原因聚焦在 `STG-RDY-004/008`API_BASE_URL 仍为本地地址且无真实外网可达性)。
完整开发测试续跑结果2026-03-31 12:31
1. 已重新生成 `.env.staging-real` 且三类 token 均为非占位值:`reports/gates/local_staging_env_generation_2026-03-31_123102.md`
2. `local/mock` 口径 `staging_release_pipeline` 再次通过:`reports/gates/staging_release_pipeline_2026-03-31_123148.md`
3. `superpowers_release_pipeline``tok007` 复审链路再次通过,机判维持 `CONDITIONAL_GO``reports/gates/superpowers_release_pipeline_2026-03-31_123150.md``review/outputs/tok007_release_recheck_2026-03-31_123153.md`
4. 真实 STG 就绪度检查仍为 `BLOCKED``reports/gates/staging_real_readiness_2026-03-31_123159.md``STG-RDY-004/008` 未关闭)。
5. Minimax 上游 smoke 继续保持 `PASS``reports/gates/minimax_upstream_smoke_2026-03-31_123210.md`
---
## 5. 执行约束
1. `local/mock` 结果仅可作为开发演练证据,不可替代 staging 放行证据。
2. 任何 `P0` 项未关闭,最终结论不得上调为 `GO`
3. 所有阶段结论以脚本返回码 + 报告产物双重校验为准。
---
## 6. 与 v1 的关系
1. `v1` 保留原子任务定义A~G
2. `v2` 作为执行态总控视图,负责状态、批次与阻塞跟踪。

View File

@@ -0,0 +1,450 @@
openapi: 3.0.3
info:
title: Platform Token Runtime API Contract Draft
version: 1.0.0-draft
description: |
TOK-002/TOK-003/TOK-004 对应的 token 运行态接口草案。
关键边界:
1) 仅平台内部可调用 issue/revoke。
2) 不暴露上游供应方凭证信息。
servers:
- url: https://api.example.com
description: Production
security:
- BearerAuth: []
tags:
- name: PlatformToken
paths:
/api/v1/platform/tokens/issue:
post:
tags: [PlatformToken]
summary: 签发平台短期 token
operationId: issuePlatformToken
parameters:
- $ref: '#/components/parameters/XRequestIdHeader'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/IssueTokenRequest'
responses:
'201':
description: 签发成功
content:
application/json:
schema:
$ref: '#/components/schemas/IssueTokenResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'409':
$ref: '#/components/responses/Conflict'
/api/v1/platform/tokens/{tokenId}/refresh:
post:
tags: [PlatformToken]
summary: 刷新 token 过期时间
operationId: refreshPlatformToken
parameters:
- $ref: '#/components/parameters/TokenIdParam'
- $ref: '#/components/parameters/XRequestIdHeader'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RefreshTokenRequest'
responses:
'200':
description: 刷新成功
content:
application/json:
schema:
$ref: '#/components/schemas/RefreshTokenResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
/api/v1/platform/tokens/{tokenId}/revoke:
post:
tags: [PlatformToken]
summary: 吊销 token
operationId: revokePlatformToken
parameters:
- $ref: '#/components/parameters/TokenIdParam'
- $ref: '#/components/parameters/XRequestIdHeader'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RevokeTokenRequest'
responses:
'200':
description: 吊销成功
content:
application/json:
schema:
$ref: '#/components/schemas/RevokeTokenResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
/api/v1/platform/tokens/introspect:
post:
tags: [PlatformToken]
summary: token 校验与解析
operationId: introspectPlatformToken
parameters:
- $ref: '#/components/parameters/XRequestIdHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/IntrospectTokenRequest'
responses:
'200':
description: 校验成功
content:
application/json:
schema:
$ref: '#/components/schemas/IntrospectTokenResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'422':
$ref: '#/components/responses/BusinessError'
/api/v1/platform/tokens/audit-events:
get:
tags: [PlatformToken]
summary: 查询 token 审计事件
operationId: listPlatformTokenAuditEvents
parameters:
- $ref: '#/components/parameters/XRequestIdHeader'
- name: request_id
in: query
required: false
schema:
type: string
maxLength: 128
- name: token_id
in: query
required: false
schema:
type: string
maxLength: 128
- name: subject_id
in: query
required: false
schema:
type: string
maxLength: 128
- name: event_name
in: query
required: false
schema:
type: string
maxLength: 128
- name: result_code
in: query
required: false
schema:
type: string
maxLength: 64
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 500
default: 100
responses:
'200':
description: 查询成功
content:
application/json:
schema:
$ref: '#/components/schemas/AuditEventsResponse'
'400':
$ref: '#/components/responses/BadRequest'
'501':
$ref: '#/components/responses/BusinessError'
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
parameters:
XRequestIdHeader:
name: X-Request-Id
in: header
required: true
schema:
type: string
minLength: 8
maxLength: 128
IdempotencyKeyHeader:
name: Idempotency-Key
in: header
required: true
schema:
type: string
minLength: 8
maxLength: 128
TokenIdParam:
name: tokenId
in: path
required: true
schema:
type: string
minLength: 8
maxLength: 128
responses:
BadRequest:
description: 参数错误
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Unauthorized:
description: 认证失败
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Forbidden:
description: 权限不足
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
NotFound:
description: 资源不存在
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Conflict:
description: 状态冲突
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
BusinessError:
description: 业务校验失败
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
schemas:
IssueTokenRequest:
type: object
required: [subject_id, role, ttl_seconds, scope]
properties:
subject_id:
type: string
minLength: 1
maxLength: 128
role:
type: string
enum: [owner, viewer, admin]
ttl_seconds:
type: integer
minimum: 60
maximum: 259200
scope:
type: array
minItems: 1
items:
type: string
metadata:
type: object
additionalProperties: true
IssueTokenResponse:
type: object
required: [request_id, data]
properties:
request_id:
type: string
data:
type: object
required: [token_id, access_token, issued_at, expires_at, status]
properties:
token_id:
type: string
access_token:
type: string
writeOnly: true
issued_at:
type: string
format: date-time
expires_at:
type: string
format: date-time
status:
type: string
enum: [active]
RefreshTokenRequest:
type: object
required: [ttl_seconds]
properties:
ttl_seconds:
type: integer
minimum: 60
maximum: 259200
RefreshTokenResponse:
type: object
required: [request_id, data]
properties:
request_id:
type: string
data:
type: object
required: [token_id, previous_expires_at, expires_at, status]
properties:
token_id:
type: string
previous_expires_at:
type: string
format: date-time
expires_at:
type: string
format: date-time
status:
type: string
enum: [active]
RevokeTokenRequest:
type: object
required: [reason]
properties:
reason:
type: string
minLength: 1
maxLength: 256
RevokeTokenResponse:
type: object
required: [request_id, data]
properties:
request_id:
type: string
data:
type: object
required: [token_id, status, revoked_at]
properties:
token_id:
type: string
status:
type: string
enum: [revoked]
revoked_at:
type: string
format: date-time
IntrospectTokenRequest:
type: object
required: [token]
properties:
token:
type: string
minLength: 8
IntrospectTokenResponse:
type: object
required: [request_id, data]
properties:
request_id:
type: string
data:
type: object
required: [token_id, subject_id, role, status, scope, issued_at, expires_at]
properties:
token_id:
type: string
subject_id:
type: string
role:
type: string
enum: [owner, viewer, admin]
status:
type: string
enum: [active, revoked, expired]
scope:
type: array
items:
type: string
issued_at:
type: string
format: date-time
expires_at:
type: string
format: date-time
AuditEventsResponse:
type: object
required: [request_id, data]
properties:
request_id:
type: string
data:
type: object
required: [total, items]
properties:
total:
type: integer
minimum: 0
items:
type: array
items:
$ref: '#/components/schemas/AuditEventItem'
AuditEventItem:
type: object
required: [event_id, event_name, request_id, route, result_code, created_at]
properties:
event_id:
type: string
event_name:
type: string
request_id:
type: string
token_id:
type: string
subject_id:
type: string
route:
type: string
result_code:
type: string
client_ip:
type: string
created_at:
type: string
format: date-time
ErrorResponse:
type: object
required: [request_id, error]
properties:
request_id:
type: string
error:
type: object
required: [code, message]
properties:
code:
type: string
message:
type: string
details:
type: object
additionalProperties: true

View File

@@ -0,0 +1,20 @@
# 全局 P0 到供应侧/平台侧映射表v1.0
- 日期2026-03-27
- 来源:`docs/llm_gateway_prd_v1_2026-03-25.md`(第 11 节及第 4.1 节)
- 目标:避免“供应侧完成=全局P0完成”的误判。
| 全局 P0 ID | 全局能力 | 供应侧入口(按钮/页面) | 平台侧入口(控制面/API | 当前状态 |
|---|---|---|---|---|
| PRD-P0-01 | 供应账号挂载与验证 | SUP-PAGE-001`BTN-ACC-001~006` | 平台鉴权与审计服务 | 已覆盖 |
| PRD-P0-02 | 套餐发布与上下架 | SUP-PAGE-002`BTN-PKG-001~006` | 平台路由与策略中心 | 已覆盖 |
| PRD-P0-03 | 收益结算与提现 | SUP-PAGE-003`BTN-SET-001~005` | 账务与结算服务 | 已覆盖 |
| PRD-P0-04 | 凭证边界强制约束 | 三页面全局约束与 SEC-SUP 用例 | 网关鉴权、出网审计、告警中心 | 已覆盖(待 staging 实证) |
| PRD-P0-05 | 预算与配额 | 供应侧仅展示配额结果,不做全局预算配置 | 平台预算中心(组织/项目级阈值) | 待平台侧实现证据 |
| PRD-P0-06 | 告警与通知 | 供应侧展示告警状态与处理入口 | 平台告警中心(规则、路由、升级) | 待平台侧实现证据 |
| PRD-P0-07 | 账单导出 | 供应侧提供结算单导出(供应方视角) | 平台统一账单导出(组织级) | 部分覆盖(需双侧对齐) |
## 补充说明F-002
1. “预算/告警/统一账单导出”属于平台控制面能力,供应侧仅保留入口与只读结果,不承担策略主配置职责。
2. 供应侧“结算单导出”与平台“组织级账单导出”不是同一能力,必须双轨验收。

View File

@@ -0,0 +1,17 @@
# 供应侧按钮 PRD 待拍板项决议映射v1.0
- 日期2026-03-27
- 来源文档:`docs/supply_button_level_prd_v1_2026-03-25.md`
- 目标:将“待拍板项”转为“可执行决议”,用于实施与验收对齐。
| 映射ID | 待拍板项 | 决议 | 责任角色 | 交付动作 | 验收证据 |
|---|---|---|---|---|---|
| MAP-001 | `POST /api/v1/supply/*` 命名是否冻结 | 冻结 `/api/v1/supply/*` 为主路径;`/api/v1/supplier/billing` 保留兼容 | ARCH + PLAT | 在 OpenAPI 记录主路径/兼容路径声明 | OpenAPI 变更记录 |
| MAP-002 | 提现金额阈值与冷却期 | 单笔 `<=50000 CNY`;单日 `<=200000 CNY`;冷却期 `15 min` | FIN + ARCH + QA | 在结算规则与测试断言同步阈值 | 测试用例与执行报告 |
| MAP-003 | 下架 vs 暂停财务口径 | `pause` 阻断新购、存量订单继续;`unlist` 阻断新购并触发 T+1 核对 | FIN + 产品 + QA | 更新状态机语义与审计事件说明 | PRD 条目 + 审计字段检查 |
| MAP-004 | 批量导入账号是否进入 S0/S1 | 不进入 S0/S1作为 S2 评审项,白名单灰度 | 产品 + ARCH | 从当前门禁移除,纳入路线图 | 路线图条目与门禁范围声明 |
## 变更控制
1. 本映射作为 `A-003` 产物,任何变更需记录变更单号与审批人。
2. 未经审批,不得恢复“待拍板项”状态。

View File

@@ -1,7 +1,7 @@
# Subapi 集成风险控制实施任务单两周执行版v1.4
# Subapi 集成风险控制实施任务单两周执行版v1.5
- 版本v1.4
- 日期2026-03-25
- 版本v1.5
- 日期2026-03-27
- 执行窗口2026-03-18 至 2026-03-31两周
- 关联文档:
- `subapi_integration_compat_security_reliability_design_v1_2026-03-17.md`
@@ -11,13 +11,15 @@
- `router_core_s2_acceptance_test_cases_v1_2026-03-17.md`
- `acceptance_gate_single_source_v1_2026-03-18.md`v1.1, 2026-03-24
- `llm_gateway_subapi_evolution_plan_v4_2_2026-03-24.md`
- `supply_button_level_prd_v1_2026-03-25.md`
- `supply_button_level_prd_v1_2026-03-25.md`v1.1 冻结2026-03-27
- `supply_api_contract_openapi_draft_v1_2026-03-25.yaml`
- `supply_ui_test_cases_executable_v1_2026-03-25.md`
- `supply_gate_command_playbook_v1_2026-03-25.md`
- `supply_technical_design_enhanced_v1_2026-03-25.md`
- `supply_test_plan_enhanced_v1_2026-03-25.md`
- `supply_uiux_design_spec_v1_2026-03-25.md`
- `database_domain_model_and_governance_v1_2026-03-27.md`
- `dependency_compatibility_audit_baseline_v1_2026-03-27.md`
- `tests/supply/ui_design_qa_cases_v1_2026-03-25.md`
- `reports/supply_gate_preflight_2026-03-25.md`
- `review/multi_expert_planning_review_v1_2026-03-25.md`
@@ -31,6 +33,7 @@
5. 建立“凭证边界”硬门禁:需求方仅用平台凭证,供应方上游凭证零外发。
6. 建立供应侧发布门禁链路SUP账号挂载 -> 套餐发布 -> 结算提现全链路可验收。
7. 建立四专家整改发布链路XR技术/测试/UIUX/业主条款与门禁统一闭环。
8. 建立 token 运行态交付链路TOK从实现、部署到门禁验收可追踪闭环。
## 2. 责任角色映射实名RACI
@@ -138,7 +141,7 @@
| 任务ID | 任务 | Owner | 截止日期 | 依赖 | 验收标准 | 证据产物 |
|---|---|---|---|---|---|---|
| SUP-001 | 供应侧按钮级 PRD 冻结3 页面) | `产品` + `ARCH` | 2026-03-26 | 无 | 页面字段、按钮、状态机、错误码冻结 | `docs/supply_button_level_prd_v1_2026-03-25.md` |
| SUP-001 | 供应侧按钮级 PRD 冻结3 页面) | `产品` + `ARCH` | 2026-03-26 | 无 | 页面字段、按钮、状态机、错误码冻结 | `docs/supply_button_level_prd_v1_2026-03-25.md`v1.1 冻结) |
| SUP-002 | 供应侧 OpenAPI 契约冻结3 页面) | `PLAT` + `ARCH` | 2026-03-26 | SUP-001 | 请求/响应字段、枚举、错误码冻结 | `docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml` |
| SUP-003 | UI-SUP 可执行用例评审通过 | `QA` + `产品` | 2026-03-27 | SUP-001, SUP-002 | `UI-SUP-*` + `UI-DESIGN-QA-*` 全量可执行,覆盖按钮/状态/权限/可访问性 | `docs/supply_ui_test_cases_executable_v1_2026-03-25.md` + `tests/supply/ui_design_qa_cases_v1_2026-03-25.md` |
| SUP-004 | 账号挂载链路联调(验证/创建/激活/暂停) | `PLAT` + `QA` | 2026-03-28 | SUP-002, SUP-003 | `UI-SUP-ACC-001~006` 通过率 100% | `scripts/supply-gate/sup004_accounts.sh` + `tests/supply/ui_sup_acc_report_2026-03-28.md` |
@@ -152,11 +155,34 @@
| 任务ID | 任务 | Owner | 截止日期 | 依赖 | 验收标准 | 证据产物 |
|---|---|---|---|---|---|---|
| XR-001 | 供应侧技术设计增强落地(幂等/并发/不变量/事务) | `ARCH` + `PLAT` | 2026-03-26 | SUP-002 | 关键写路径均具备双键幂等和冲突语义 | `docs/supply_technical_design_enhanced_v1_2026-03-25.md` |
| XR-002 | 供应侧测试方案增强落地(追踪矩阵+并发重放) | `QA` + `ARCH` | 2026-03-27 | XR-001 | Requirement->API->Test->Metric->Gate 全量可追踪 | `docs/supply_test_plan_enhanced_v1_2026-03-25.md` + `reports/supply_traceability_matrix_2026-03-25.csv` + `reports/supply_flaky_budget_2026-03-25.md` |
| XR-002 | 供应侧测试方案增强落地(追踪矩阵+并发重放) | `QA` + `ARCH` | 2026-03-27 | XR-001 | Requirement->API->Test->Metric->Gate 全量可追踪,且路径一致性检查通过 | `docs/supply_test_plan_enhanced_v1_2026-03-25.md` + `reports/supply_traceability_matrix_2026-03-25.csv` + `docs/supply_traceability_matrix_generation_rules_v1_2026-03-27.md` + `reports/supply_flaky_budget_2026-03-25.md` |
| XR-003 | 供应侧 UI/UX 规范与设计验收清单落地 | `产品` + `UIUX` + `QA` | 2026-03-27 | SUP-003 | DQA P0=0P1 通过率>=95% | `docs/supply_uiux_design_spec_v1_2026-03-25.md` |
| XR-004 | 业主 SLA/申诉/赔付条款并入门禁验收 | `产品` + `CS` + `FIN` | 2026-03-28 | XR-002, XR-003 | 条款可执行可测且签字确认 | `docs/product/owner_sla_dispute_compensation_rules_v1.md` |
| XR-005 | 四专家再次对齐复核并形成发布结论 | `ARCH` + `QA` + `产品` + `UIUX` | 2026-03-28 | XR-001~XR-004 | 复核结论明确GO/CONDITIONAL GO/NO-GO | `review/multi_expert_alignment_recheck_v1_2026-03-25.md` |
## 4.9 Workstream I数据库与依赖质量闭环新增
| 任务ID | 任务 | Owner | 截止日期 | 依赖 | 验收标准 | 证据产物 |
|---|---|---|---|---|---|---|
| DB-001 | 跨域核心表基线落地Core/IAM/Auth/Billing/Audit | `ARCH` + `PLAT` | 2026-03-27 | XR-001 | `platform_core_schema_v1.sql` 可执行且评审通过 | `sql/postgresql/platform_core_schema_v1.sql` |
| DB-002 | 供应域加密/单位/审计字段与索引补齐 | `PLAT` + `QA` | 2026-03-28 | DB-001 | patch 可幂等执行,关键查询计划不回退 | `sql/postgresql/supply_schema_v1_patch_2026-03-27.sql` |
| DB-003 | 数据模型与迁移策略文档并入 SSOT | `ARCH` | 2026-03-28 | DB-001, DB-002 | 迁移顺序、回滚策略、验收清单完整 | `docs/database_domain_model_and_governance_v1_2026-03-27.md` |
| DEP-001 | 依赖兼容审计四件套接入发布流程 | `PLAT` + `SEC` | 2026-03-28 | COMP-005 | SBOM/锁差异/兼容矩阵/风险清单缺一阻断 | `docs/dependency_compatibility_audit_baseline_v1_2026-03-27.md` |
| DEP-002 | 分阶段质量门禁G0-G5接入 CI | `QA` + `PLAT` | 2026-03-29 | DEP-001, XR-002 | `M-018``M-019` 自动计算并阻断 | CI 记录 + Gate 汇总 |
| DEP-003 | 需求-设计-测试漂移日检机制上线 | `PMO` + `QA` | 2026-03-29 | DEP-002 | 发现漂移 24h 内闭环,周报可追踪 | `reports/design_drift_daily_*.md` |
## 4.10 Workstream Jtoken 运行态实现与验收闭环TOK新增
| 任务ID | 任务 | Owner | 截止日期 | 依赖 | 验收标准 | 证据产物 |
|---|---|---|---|---|---|---|
| TOK-001 | token 能力最小实现清单冻结(签发/校验/吊销/续期/审计) | `ARCH` + `SEC` + `PLAT` | 2026-03-28 | SUP-002 | 功能边界、接口与状态机冻结,禁止再口头变更 | `docs/token_runtime_minimal_spec_v1.md` |
| TOK-002 | 平台鉴权与 token 校验中间件实现(仅平台凭证入站) | `PLAT` + `SEC` | 2026-03-30 | TOK-001 | 外部请求必须通过平台凭证校验,覆盖率=100% | 开发阶段:`docs/token_auth_middleware_design_v1_2026-03-29.md` + `docs/platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml`;联调阶段:实现代码 + 单测报告 |
| TOK-003 | token 生命周期实现(签发/短期TTL/吊销/轮换) | `PLAT` | 2026-03-31 | TOK-001 | 生命周期状态可追踪,吊销生效延迟满足阈值 | 开发阶段:`docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md`;联调阶段:实现代码 + 集成测试报告 |
| TOK-004 | 安全审计与事件入库(签发/鉴权失败/吊销/越权) | `SEC` + `PLAT` | 2026-03-31 | TOK-002, TOK-003 | 审计事件完整入库,可按租户/角色追踪 | 开发阶段:`docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md`;联调阶段:审计表样例 + 查询结果 |
| TOK-005 | 凭证边界联调SUP-007 合并复测) | `SEC` + `QA` | 2026-04-01 | TOK-002~TOK-004 | M-013~M-016 在 staging 实测全部达标 | 开发阶段:`scripts/supply-gate/tok005_boundary_dryrun.sh` + `reports/gates/tok005_dryrun_*.md`;联调阶段:`tests/supply/sec_sup_boundary_report_2026-03-30.md`staging回填 |
| TOK-006 | staging 一键回归SUP-004~SUP-007 + TOK | `QA` + `PLAT` | 2026-04-01 | TOK-005 | 全链路通过且无 mock 依赖 | 开发阶段:`scripts/supply-gate/tok006_gate_bundle.sh` + `scripts/ci/superpowers_stage_validate.sh` + `reports/gates/tok006_gate_bundle_*.md` + `reports/gates/superpowers_stage_validation_*.md` + `reports/gates/tok006_release_decision_onepager_template_v1_2026-03-30.md`;联调阶段:`reports/gates/sup_run_all_staging_*.log` + 实测单页判定报告 |
| TOK-007 | 发布门禁复审(并入 EXP-006 决议) | `ARCH` + `QA` + `SEC` | 2026-04-03 | TOK-006 | F-04 关闭,生产决议可重新评估 | 开发阶段:`scripts/ci/tok007_release_recheck.sh` + `scripts/ci/final_decision_consistency_check.sh` + `scripts/ci/tok007_generate_final_decision_candidate.sh` + `review/outputs/tok007_release_recheck_*.md` + `review/outputs/final_decision_candidate_from_tok007_*.md` + `reports/gates/final_decision_consistency_*.md`;联调阶段:`review/final_decision_2026-03-31.md`(复审回填) |
## 5. 验收门禁(每日/每周)
## 5.1 Daily Gate每日 18:00
@@ -174,6 +200,10 @@
11. 供应侧 UI Gate 是否全绿(`UI-SUP-ACC-* / UI-SUP-PKG-* / UI-SUP-SET-*`)。
12. 供应侧凭证边界专项(`SEC-SUP-*`)是否全绿(失败即 P0
13. 四专家整改链路XR-001~XR-003是否全绿未完成即禁止进入 SUP-008 结论环节)。
14. 数据库补丁任务DB-001~DB-003是否按阶段达成未完成即禁止升波
15. 依赖兼容审计四件套是否完整(缺任一项即阻断发布)。
16. 分阶段质量门禁 `M-018/M-019` 是否持续 = 100%(否则回退到失败阶段)。
17. token 运行态链路TOK-002~TOK-006是否完成未完成即禁止生产 GO
## 5.2 Weekly Gate2026-03-24 / 2026-03-31
@@ -184,6 +214,10 @@
5. 是否完成当周专家评审并关闭必须整改项。
6. 供应侧 GateSUP-004~SUP-008是否完成并出具结论。
7. 四专家复核链路XR-001~XR-005是否完成并形成签署结论。
8. DB/依赖质量链路DB-* / DEP-*)是否全量关闭。
9. 依赖兼容审计指标 `M-017` 是否连续 7 天达标。
10. 阶段质量与追踪覆盖指标 `M-018/M-019` 是否连续 7 天达标。
11. token 运行态审计缺口(`TOK-REAL-001~003`)是否全部关闭。
## 6. 风险与阻断规则

View File

@@ -7,6 +7,9 @@ info:
安全边界要求:
1) 仅接受平台鉴权头Authorization不接受 query key 鉴权。
2) 任何响应不得返回可复用上游凭证明文片段。
变更日志:
- 2026-03-27新增幂等请求头组件与写操作挂载补充 409/202 幂等语义示例。
- 2026-03-27命名策略调整为 `/supply` 主路径;`/supplier` 保留为兼容 alias。
servers:
- url: https://api.example.com
description: Production
@@ -48,6 +51,9 @@ paths:
tags: [SupplyAccounts]
summary: 创建供应账号
operationId: createSupplyAccount
parameters:
- $ref: '#/components/parameters/XRequestIdHeader'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
requestBody:
required: true
content:
@@ -61,10 +67,14 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/CreateSupplyAccountResponse'
'202':
$ref: '#/components/responses/AcceptedInProgress'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/BusinessError'
/api/v1/supply/accounts/{accountId}/activate:
@@ -175,6 +185,8 @@ paths:
operationId: publishSupplyPackage
parameters:
- $ref: '#/components/parameters/PackageIdParam'
- $ref: '#/components/parameters/XRequestIdHeader'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
responses:
'200':
description: 发布成功
@@ -182,6 +194,8 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/SupplyPackageStatusResponse'
'202':
$ref: '#/components/responses/AcceptedInProgress'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
@@ -233,6 +247,9 @@ paths:
tags: [SupplyPackages]
summary: 批量调价
operationId: batchUpdateSupplyPackagePrice
parameters:
- $ref: '#/components/parameters/XRequestIdHeader'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
requestBody:
required: true
content:
@@ -246,6 +263,8 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/BatchUpdateSupplyPackagePriceResponse'
'202':
$ref: '#/components/responses/AcceptedInProgress'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@@ -271,11 +290,35 @@ paths:
'404':
$ref: '#/components/responses/NotFound'
/api/v1/supply/billing:
get:
tags: [SupplierBilling]
summary: 查询供应方账单汇总canonical
operationId: getSupplyBilling
parameters:
- $ref: '#/components/parameters/StartDateParam'
- $ref: '#/components/parameters/EndDateParam'
- $ref: '#/components/parameters/PageParam'
- $ref: '#/components/parameters/PageSizeParam'
responses:
'200':
description: 查询成功
content:
application/json:
schema:
$ref: '#/components/schemas/SupplierBillingResponse'
'401':
$ref: '#/components/responses/Unauthorized'
/api/v1/supplier/billing:
get:
tags: [SupplierBilling]
summary: 查询供应方账单汇总
operationId: getSupplierBilling
summary: 查询供应方账单汇总alias兼容路径
description: |
Deprecated alias of `/api/v1/supply/billing`.
仅用于历史客户端兼容,不新增能力字段。
deprecated: true
operationId: getSupplierBillingAlias
parameters:
- $ref: '#/components/parameters/StartDateParam'
- $ref: '#/components/parameters/EndDateParam'
@@ -296,6 +339,9 @@ paths:
tags: [SupplySettlements]
summary: 发起提现申请
operationId: createSupplySettlementWithdraw
parameters:
- $ref: '#/components/parameters/XRequestIdHeader'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
requestBody:
required: true
content:
@@ -309,6 +355,8 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/CreateWithdrawResponse'
'202':
$ref: '#/components/responses/AcceptedInProgress'
'400':
$ref: '#/components/responses/BadRequest'
'401':
@@ -322,6 +370,8 @@ paths:
operationId: cancelSupplySettlementWithdraw
parameters:
- $ref: '#/components/parameters/SettlementIdParam'
- $ref: '#/components/parameters/XRequestIdHeader'
- $ref: '#/components/parameters/IdempotencyKeyHeader'
responses:
'200':
description: 撤销成功
@@ -329,6 +379,8 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/SupplySettlementStatusResponse'
'202':
$ref: '#/components/responses/AcceptedInProgress'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
@@ -428,6 +480,24 @@ components:
minimum: 1
maximum: 200
default: 20
XRequestIdHeader:
name: X-Request-Id
in: header
required: true
description: 客户端请求幂等追踪ID全链路唯一
schema:
type: string
minLength: 8
maxLength: 128
IdempotencyKeyHeader:
name: Idempotency-Key
in: header
required: true
description: 写操作幂等键(同资源同动作语义唯一)
schema:
type: string
minLength: 8
maxLength: 128
responses:
BadRequest:
description: 参数错误
@@ -453,6 +523,34 @@ components:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
idempotencyPayloadMismatch:
summary: 幂等键命中但请求体不一致
value:
request_id: req_20260327_001
error:
code: IDEMPOTENCY_PAYLOAD_MISMATCH
message: idempotency key replay with different payload
details:
retryable: false
expected_action: reuse_same_payload_or_new_idempotency_key
AcceptedInProgress:
description: 首次请求仍在处理,请按建议间隔重试
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
idempotencyInProgress:
summary: 幂等处理中重放
value:
request_id: req_20260327_002
error:
code: IDEMPOTENCY_IN_PROGRESS
message: request is processing
details:
retry_after_ms: 2000
retryable: true
BusinessError:
description: 业务校验失败
content:

View File

@@ -1,7 +1,7 @@
# 供应侧按钮级 PRD 分解(首批 3 个核心页面)
- 版本v1.0草案
- 日期2026-03-25
- 版本v1.1冻结
- 日期2026-03-27
- 适用范围:供应侧 S0/S1 首批上线页面
- 关联 SSOT
- `llm_gateway_subapi_evolution_plan_v4_2_2026-03-24.md`
@@ -233,9 +233,15 @@
---
## 9. 待拍板项(进入 PRD v1.0 前必须确认
## 9. 已决议项2026-03-27
1. `POST /api/v1/supply/*` 系列接口是否按本稿命名冻结。
2. 提现金额风控阈值(单笔/单日)与冷却期。
3. 套餐“下架”与“暂停”的财务影响口径是否一致。
4. 供应方是否允许批量导入账号(当前建议 S1 后)。
决议依据:
1. `docs/product/supply_prd_pending_to_decision_map_v1_2026-03-27.md`
2. `review/outputs/supply_prd_decision_meeting_minutes_2026-03-27.md`
| 决议ID | 原待拍板项 | 决议结论 | 执行动作 |
|---|---|---|---|
| DEC-001 | `POST /api/v1/supply/*` 系列接口是否按本稿命名冻结 | 冻结 `/api/v1/supply/*` 为供应侧主路径;`/api/v1/supplier/billing` 作为兼容路径保留,待 F 阶段统一命名策略 | 在 OpenAPI 变更日志记录主路径与兼容路径策略 |
| DEC-002 | 提现金额风控阈值(单笔/单日)与冷却期 | S1 阶段阈值冻结:单笔 `<= 50,000 CNY`,单日累计 `<= 200,000 CNY`,同账户提现冷却期 `15 分钟` | 在结算风控与测试用例中同步阈值断言 |
| DEC-003 | 套餐“下架”与“暂停”的财务影响口径是否一致 | 不一致:`暂停`仅阻断新购,存量订单不变;`下架`阻断新购并触发 T+1 财务核对任务 | 在结算页与审计事件中区分 `pause/unlist` 财务语义 |
| DEC-004 | 供应方是否允许批量导入账号 | 不允许进入 S0/S1 主路径;改为 S2 评审项,仅可在受控灰度与白名单下试点 | 移出当前发布门禁范围,纳入后续路线图 |

View File

@@ -51,6 +51,18 @@ cd "/home/long/project/立交桥"
bash "scripts/supply-gate/run_all.sh"
```
真实 staging 推荐使用(含占位值与可达性预检):
```bash
cd "/home/long/project/立交桥"
bash "scripts/supply-gate/staging_precheck_and_run.sh" "scripts/supply-gate/.env"
```
说明:
1. `staging_precheck_and_run.sh` 默认会先执行 `M-021` 预检token runtime readiness
2. `staging_precheck_and_run.sh` 默认会再执行 `TOK-005 dry-run`
3. 如需临时跳过可设置:`ENABLE_M021_PRECHECK=0``ENABLE_TOK005_DRYRUN=0`
---
## 2. SUP-004 账号挂载链路UI-SUP-ACC-001~006
@@ -163,3 +175,536 @@ bash "scripts/supply-gate/sup007_boundary.sh"
1. 结论PASS/FAIL/BLOCKED
2. 证据路径json/screenshot/log
3. 责任人签字
---
## 7. 依赖兼容审计命令M-017
执行脚本:
```bash
cd "/home/long/project/立交桥"
./scripts/ci/dependency-audit-check.sh 2026-03-27
```
最低断言:
1. 四件套文件存在且非空:
1. `reports/dependency/sbom_2026-03-27.spdx.json`
2. `reports/dependency/lockfile_diff_2026-03-27.md`
3. `reports/dependency/compat_matrix_2026-03-27.md`
4. `reports/dependency/risk_register_2026-03-27.md`
2. 输出结果为 `PASS`,并生成 `dependency_audit_result_2026-03-27.md`
---
## 8. 分阶段门禁失败回退演练M-018/M-019
执行脚本:
```bash
cd "/home/long/project/立交桥"
./scripts/ci/stage-gate-drill.sh G3 2026-03-27
```
最低断言:
1. G3 失败后必须触发回退到 G2。
2. 后续阶段冻结,不允许继续升波。
3. 生成原始日志与演练报告:
1. `reports/gates/stage_gate_drill_2026-03-27.log`
2. `reports/gates/stage_gate_drift_drill_report_2026-03-27.md`
---
## 9. 本地 Mock 联调模式(仅演练)
执行命令:
```bash
cd "/home/long/project/立交桥"
python3 "scripts/mock/supply_gateway_mock_server.py"
```
另开终端执行:
```bash
cd "/home/long/project/立交桥"
bash "scripts/supply-gate/run_all.sh" "scripts/supply-gate/.env.local-mock"
```
说明:
1. 本模式仅用于脚本联调与产物验证,不代表 staging/生产可发布。
2. 生产放行仍需在真实 staging 地址与真实短期 token 下复跑并验收。
---
## 10. TOK-005 凭证边界 Dry-Run开发阶段
执行命令:
```bash
cd "/home/long/project/立交桥"
bash "scripts/supply-gate/tok005_boundary_dryrun.sh" "scripts/supply-gate/.env"
```
最低断言:
1. `go test ./...``platform-token-runtime` 通过。
2. Query key 外拒规则存在(`key/api_key/token`)。
3. 审计脱敏断言存在且通过(禁止敏感值落审计)。
4. TOK 用例可执行覆盖完整(`TOK-LIFE-001~008``TOK-AUD-001~007`)。
5. staging 就绪性检查结果可追溯NO 时需明确阻塞原因)。
证据输出:
1. `reports/gates/tok005_dryrun_*.md`
2. `reports/gates/tok005_dryrun_*.log`
3. `tests/supply/artifacts/tok005_dryrun_*/go_test_output.txt`
说明:
1. Dry-run 仅用于开发阶段门禁前置验证,不可替代真实 staging 联调结论。
2. 真实放行仍以 `staging_precheck_and_run.sh` + `SUP-007/TOK-005` 实测结果为准。
---
## 11. TOK-006 统一 Gate 汇总Dry-Run + SUP-004~007
执行命令:
```bash
cd "/home/long/project/立交桥"
bash "scripts/supply-gate/tok006_gate_bundle.sh" "scripts/supply-gate/.env"
```
可选开关:
```bash
# 默认 1先执行 TOK-005 dry-run
ENABLE_TOK005_DRYRUN=1
# 默认 0仅汇总现有 SUP 报告,不触发 run_all
ENABLE_SUP_RUN=0
```
最低断言:
1. 输出单页 gate 汇总报告(含 TOK-005 + SUP-004~007
2. 生成明确发布判定:`GO / CONDITIONAL_GO / NO_GO`
3. 若存在 mock 证据或 `staging readiness != YES`,不得输出 GO。
证据输出:
1. `reports/gates/tok006_gate_bundle_*.md`
2. `reports/gates/tok006_gate_bundle_*.log`
3. `reports/gates/tok006_release_decision_onepager_template_v1_2026-03-30.md`(模板)
---
## 12. Superpowers 严格分阶段验证(代码+脚本+门禁)
执行命令:
```bash
cd "/home/long/project/立交桥"
bash "scripts/ci/superpowers_stage_validate.sh"
```
阶段定义(当前实现):
1. PHASE-01TOK 运行时代码测试Go 单测)
2. PHASE-02SUP-004~SUP-007 本地 mock 脚本联调
3. PHASE-03TOK-005 凭证边界 dry-runmock 环境)
4. PHASE-04TOK-006 统一 Gate 汇总
5. PHASE-05依赖兼容审计门禁M-017
6. PHASE-06分阶段回退演练门禁M-018/M-019
7. PHASE-07真实 staging 预检(无真值时应 DEFERRED
8. PHASE-08每日指标快照生成M-017/M-018/M-019
9. PHASE-097日趋势报告生成M-017/M-018/M-019
10. PHASE-10token 运行态就绪度检查M-021
结果判定:
1. 任一阶段 FAIL => `NO_GO`
2. 无 FAIL 且存在 DEFERRED => `CONDITIONAL_GO`
3. 全部 PASS => `GO`
可选环境变量:
```bash
# PHASE-07 使用的环境文件,默认 scripts/supply-gate/.env
STAGING_ENV_FILE="scripts/supply-gate/.env"
```
证据输出:
1. `reports/gates/superpowers_stage_validation_*.md`
2. `reports/gates/superpowers_stage_validation_*.log`
3. `tests/supply/artifacts/superpowers_stage_validation_*/phase*.log`
---
## 13. TOK-007 发布门禁复审(自动汇总)
执行命令:
```bash
cd "/home/long/project/立交桥"
bash "scripts/ci/tok007_release_recheck.sh"
```
最低断言:
1. 自动读取最新 `TOK-006` 汇总报告。
2. 自动读取最新 `Superpowers` 阶段验证报告。
3. 自动读取 `SUP Gate` 汇总评审结论。
4. 输出复审结论(`GO / CONDITIONAL GO / NO-GO`)与动作建议。
证据输出:
1. `review/outputs/tok007_release_recheck_*.md`
2. `reports/gates/tok007_release_recheck_*.log`
---
## 14. 最终决议一致性校验Final vs TOK-007
执行命令:
```bash
cd "/home/long/project/立交桥"
bash "scripts/ci/final_decision_consistency_check.sh"
```
最低断言:
1. 可解析 `final_decision``TOK-007``superpowers_stage_validation` 三类结论。
2.`final_decision``TOK-007` 不一致,输出 `WARN`(不自动覆盖签署结论)。
3. 若任一来源不可解析,输出 `FAIL` 并阻断自动流程。
证据输出:
1. `reports/gates/final_decision_consistency_*.md`
2. `reports/gates/final_decision_consistency_*.log`
---
## 15. 最终决议候选稿生成(不覆盖签署原件)
执行命令:
```bash
cd "/home/long/project/立交桥"
bash "scripts/ci/tok007_generate_final_decision_candidate.sh"
```
最低断言:
1. 输入源必须包括 `review/final_decision_2026-03-31.md` 与最新 `tok007_release_recheck_*.md`
2. 输出文件位于 `review/outputs/final_decision_candidate_from_tok007_*.md`
3. 仅生成候选稿,不覆盖原签署文件。
证据输出:
1. `review/outputs/final_decision_candidate_from_tok007_*.md`
2. `reports/gates/tok007_generate_candidate_*.log`
---
## 16. M-021 Token Runtime 就绪度检查
执行命令:
```bash
cd "/home/long/project/立交桥"
bash "scripts/ci/token_runtime_readiness_check.sh" "$(date +%F)"
```
可选开关:
```bash
# 默认 0跳过本地端口冒烟适配受限沙箱环境
ENABLE_TOKEN_RUNTIME_SMOKE=0
# 置 1执行本地服务启动 + issue + audit-events 冒烟
ENABLE_TOKEN_RUNTIME_SMOKE=1
# 可选:指定冒烟起始端口(默认 18082若被占用会自动顺延
TOKEN_RUNTIME_SMOKE_PORT=18082
```
最低断言:
1. 输出 `token_runtime_readiness_*.md` 报告并给出百分比结果。
2. 运行态代码与契约工件完整API入口/HTTP处理/OpenAPI/Dockerfile
3. `platform-token-runtime` 测试与构建均通过。
4. 若就绪度 `<100%`,脚本必须返回失败并阻断后续门禁。
证据输出:
1. `reports/gates/token_runtime_readiness_*.md`
2. `reports/gates/token_runtime_readiness_*.log`
3. `reports/gates/token_runtime_go_test_*.log`
4. `reports/gates/token_runtime_go_build_*.log`
---
## 17. Token 审计事件查询TOK-REAL-002
本地服务启动:
```bash
cd "/home/long/project/立交桥/platform-token-runtime"
export PATH="/home/long/project/立交桥/.tools/go-current/bin:$PATH"
go run ./cmd/platform-token-runtime
```
审计查询示例:
```bash
curl -sS "http://127.0.0.1:18081/api/v1/platform/tokens/audit-events?limit=20" \
-H "X-Request-Id: req-audit-query-demo"
```
最低断言:
1. 返回 `200`,且结构包含 `request_id/data.total/data.items`
2. 返回项包含 `event_id/event_name/request_id/route/result_code/created_at`
3. 响应不包含 `access_token` 或上游敏感凭证明文。
证据输出:
1. `platform-token-runtime/internal/httpapi/token_api_test.go`(自动化用例)
2. `reports/gates/token_runtime_readiness_*.md`(检查项 `TOK-REAL-002-C1/C2`
---
## 18. Staging 证据自动回填草稿
执行命令:
```bash
cd "/home/long/project/立交桥"
bash "scripts/ci/staging_evidence_autofill.sh"
```
可选参数(用于锁定本次流水证据,避免误取历史 latest
```bash
bash "scripts/ci/staging_evidence_autofill.sh" \
--staging-run-log "reports/gates/staging_run_2026-03-30_184432.log" \
--stage-report "reports/gates/superpowers_stage_validation_2026-03-30_184433.md" \
--token-readiness "reports/gates/token_runtime_readiness_2026-03-30_184435.md" \
--tok007-report "review/outputs/tok007_release_recheck_2026-03-30_184436.md" \
--pipeline-report "reports/gates/superpowers_release_pipeline_2026-03-30_184434.md"
```
最低断言:
1. 自动抽取 `PHASE-07``M-013~M-016``M-021` 与 TOK-007 机判结论。
2. 输出证据路径清单,便于人工补齐与签署。
3. 不得自动上调为 GO仅生成草稿。
证据输出:
1. `reports/gates/staging_token_go_evidence_autofill_*.md`
2. `reports/gates/staging_token_go_evidence_autofill_*.log`
---
## 19. 一键 Staging 发布流水
执行命令:
```bash
cd "/home/long/project/立交桥"
bash "scripts/ci/staging_release_pipeline.sh" "scripts/supply-gate/.env"
```
local/mock 防误跑(默认拦截):
```bash
# 仅当明确要做 local/mock 演练时启用
ALLOW_LOCAL_MOCK_STAGING=1 \
bash "scripts/ci/staging_release_pipeline.sh" "scripts/supply-gate/.env.local-mock"
```
说明:
1. STEP-01`staging_precheck_and_run.sh`(含 M-021/TOK-005/SUP run_all
2. STEP-02`superpowers_release_pipeline.sh`(使用 `STAGING_ENV_FILE`)。
3. STEP-03`staging_evidence_autofill.sh` 自动生成回填草稿(显式绑定本次流水证据文件)。
4. 检测到 local/mock env 且未设置 `ALLOW_LOCAL_MOCK_STAGING=1` 时,脚本应直接失败,防止误把演练结果当成真实 staging 证据。
可选监控(默认关闭、非阻断):
```bash
ENABLE_MINIMAX_MONITORING=1 \
MINIMAX_ENV_FILE="scripts/supply-gate/.env.minimax-dev" \
MINIMAX_RUN_ACTIVE_SMOKE=0 \
bash "scripts/ci/superpowers_release_pipeline.sh"
```
说明:
1. 开启后会在 `STEP-05` 额外执行 Minimax 每日快照 + 7 日趋势生成。
2. 该步骤是监控辅助项,失败仅记 `WARN`,不阻断 SUP 主门禁判定。
证据输出:
1. `reports/gates/staging_release_pipeline_*.md`
2. `reports/gates/staging_release_pipeline_*.log`
---
## 20. Minimax 上游独立 Smoke不并入 SUP 发布门禁)
执行命令:
```bash
cd "/home/long/project/立交桥"
bash "scripts/supply-gate/minimax_upstream_smoke.sh" "scripts/supply-gate/.env.minimax-dev"
```
可选环境变量:
```bash
# 默认 /v1/messages
MINIMAX_SMOKE_PATH="/v1/messages"
# 默认 minimax-smoke-model可替换为实际模型
MINIMAX_SMOKE_MODEL="your-model-id"
# 默认 20 秒
MINIMAX_TIMEOUT_SECONDS=20
```
最低断言:
1. 输出 `reports/gates/minimax_upstream_smoke_*.md` 报告。
2. 报告必须包含 base 连通探测与 active 鉴权探测两段结果。
3. 分类规则需区分:`PASS / PASS_AUTH_REACHED / FAIL_AUTH / FAIL_PATH / FAIL_NETWORK`
说明:
1. 该脚本仅用于“上游Minimax连通与鉴权可达性”验证。
2. 该脚本不参与 `SUP-004~SUP-007` 业务契约发布门禁判定。
3. 若 Minimax 返回 `404/405`,优先检查 `API_BASE_URL + MINIMAX_SMOKE_PATH` 组合是否正确。
---
## 21. Minimax 上游每日快照CI 汇总)
执行命令:
```bash
cd "/home/long/project/立交桥"
bash "scripts/ci/minimax_upstream_daily_snapshot.sh" "$(date +%F)" "scripts/supply-gate/.env.minimax-dev"
```
可选环境变量:
```bash
# 默认 0仅汇总最新 smoke 报告,不触发网络请求
RUN_ACTIVE_SMOKE=0
# 置 1执行一次实时 smoke 后再汇总
RUN_ACTIVE_SMOKE=1
```
最低断言:
1. 生成 `reports/gates/minimax_upstream_daily_snapshot_*.md`
2. 生成/更新 `reports/gates/minimax_upstream_daily_snapshots.csv`
3. 明确标注 `RUN_ACTIVE_SMOKE` 取值,区分“实时探测”与“仅汇总”。
4. 默认优先汇总“非 dry-run”最新报告避免将联调证据误当真实上游证据。
说明:
1. 该快照是“上游可达性趋势”证据,不替代 SUP 发布门禁。
2. 建议在定时任务中默认 `RUN_ACTIVE_SMOKE=0`,将实时探测作为受控任务执行。
3. 若仅存在 `PASS_DRY_RUN` 报告,快照状态应为 `CONDITIONAL_PASS`
---
## 22. Minimax 上游 7 日趋势报告
执行命令:
```bash
cd "/home/long/project/立交桥"
bash "scripts/ci/minimax_upstream_trend_report.sh" "$(date +%F)"
```
最低断言:
1. 生成 `reports/gates/minimax_upstream_trend_7d_*.md`
2. 报告包含最近 7 条(不足 7 条按实际)快照的状态统计。
3. 趋势状态遵循 `PASS_7D / CONDITIONAL_7D / NOT_READY / INSUFFICIENT_DATA`
说明:
1. 该趋势报告用于 F-03连续观测证据收敛不替代 staging 发布门禁。
2. 建议与第 21 节每日快照搭配执行,形成“日报 + 周趋势”组合。
---
## 23. 一键生成本地 STG 环境owner/viewer/admin token
执行命令:
```bash
cd "/home/long/project/立交桥"
bash "scripts/ci/generate_local_staging_env.sh" "scripts/supply-gate/.env.staging-real"
```
可选环境变量:
```bash
# 默认 http://127.0.0.1:18080
API_BASE_URL_VALUE="http://127.0.0.1:18080"
# 默认 http://127.0.0.1:18081
TOKEN_RUNTIME_URL="http://127.0.0.1:18081"
# 默认 7200 秒2小时
TOKEN_TTL_SECONDS=7200
# 默认 1若 token runtime 不可用则自动拉起临时实例
START_RUNTIME_IF_NEEDED=1
```
最低断言:
1. 生成 `scripts/supply-gate/.env.staging-real`(权限 `600`)。
2. 文件包含 `OWNER_BEARER_TOKEN / VIEWER_BEARER_TOKEN / ADMIN_BEARER_TOKEN` 三类 token。
3. 生成摘要报告 `reports/gates/local_staging_env_generation_*.md`(仅 hash不泄露明文 token
说明:
1. 该脚本生成的是“本地开发/联调用”平台 token非外部 LLM 厂商 key。
2. 切换真实 staging 时,只需替换 `API_BASE_URL_VALUE` 并重新执行脚本即可刷新 token 与 env。
---
## 24. 真实 STG 就绪度检查(地址+token+可达性)
执行命令:
```bash
cd "/home/long/project/立交桥"
bash "scripts/ci/staging_real_readiness_check.sh" "scripts/supply-gate/.env.staging-real"
```
最低断言:
1. `API_BASE_URL` 非占位值,且不是 `localhost/127.0.0.1`
2. 三类 token 非空且非占位值。
3. `API_BASE_URL` 基础可达性检查通过(`curl -I``000`)。
4. 生成报告 `reports/gates/staging_real_readiness_*.md`
说明:
1. 结果为 `READY` 才建议进入真实 STG 放行口径验证。
2. 结果为 `BLOCKED` 时,应先修复地址或 token再执行 `staging_release_pipeline.sh`

View File

@@ -1,7 +1,7 @@
# 供应侧技术设计增强版XR-001
- 版本v1.0
- 日期2026-03-25
- 版本v1.1
- 日期2026-03-27
- 状态:生效(实施基线)
- 目标:补齐供应侧关键写路径的幂等、并发、事务、不变量与可靠性闭环
- 关联 SSOT
@@ -9,6 +9,7 @@
- `acceptance_gate_single_source_v1_2026-03-18.md`
- `supply_button_level_prd_v1_2026-03-25.md`
- `supply_api_contract_openapi_draft_v1_2026-03-25.yaml`
- `database_domain_model_and_governance_v1_2026-03-27.md`
---
@@ -38,6 +39,7 @@
2. Header 必填:`Idempotency-Key`(长度 16-128
3. 幂等作用域:`tenant_id + operator_id + api_path + idempotency_key`
4. 幂等有效期:`24h`(提现类可扩展到 `72h`
5. 契约落地状态:已在 OpenAPI 写操作路径挂载上述 header并补充 `409/202` 幂等语义示例2026-03-27
## 2.3 语义规范
@@ -91,7 +93,7 @@ create table if not exists supply_idempotency_record (
```sql
create unique index if not exists uq_settlement_supplier_processing
on supply_settlement(supplier_id)
on supply_settlements(user_id)
where status = 'processing';
```
@@ -160,12 +162,12 @@ where status = 'processing';
| 页面按钮 | API | SLI | SLO | Error Budget |
|---|---|---|---|---|
| BTN-ACC-001 立即验证 | `/accounts/verify` | 可用率 + P95 | 可用率 >= 99.9%P95 <= 800ms | 月度 0.1% |
| BTN-ACC-002 提交挂载 | `/accounts` | 成功率 | 成功率 >= 99.5% | 月度 0.5% |
| BTN-PKG-002 发布上架 | `/packages/{id}/publish` | 成功率 + 冲突率 | 成功率 >= 99.5%,冲突率 <= 0.3% | 月度 0.5% |
| BTN-PKG-005 批量调价 | `/packages/batch-price` | 局部成功可解释率 | 明细可解释率 = 100% | 0 |
| BTN-SET-002 发起提现 | `/settlements/withdraw` | 一致性 + 时延 | `billing_error_rate_pct<=0.1%`P95<=1200ms | 与 M-004 联动 |
| BTN-SET-003 撤销申请 | `/settlements/{id}/cancel` | 成功率 | 成功率 >= 99.9% | 月度 0.1% |
| BTN-ACC-001 立即验证 | `/api/v1/supply/accounts/verify` | 可用率 + P95 | 可用率 >= 99.9%P95 <= 800ms | 月度 0.1% |
| BTN-ACC-002 提交挂载 | `/api/v1/supply/accounts` | 成功率 | 成功率 >= 99.5% | 月度 0.5% |
| BTN-PKG-002 发布上架 | `/api/v1/supply/packages/{id}/publish` | 成功率 + 冲突率 | 成功率 >= 99.5%,冲突率 <= 0.3% | 月度 0.5% |
| BTN-PKG-005 批量调价 | `/api/v1/supply/packages/batch-price` | 局部成功可解释率 | 明细可解释率 = 100% | 0 |
| BTN-SET-002 发起提现 | `/api/v1/supply/settlements/withdraw` | 一致性 + 时延 | `billing_error_rate_pct<=0.1%`P95<=1200ms | 与 M-004 联动 |
| BTN-SET-003 撤销申请 | `/api/v1/supply/settlements/{id}/cancel` | 成功率 | 成功率 >= 99.9% | 月度 0.1% |
---
@@ -191,3 +193,18 @@ where status = 'processing';
6. 证据层:执行日志、指标截图、审计抽样、签署记录齐全。
达到以上 6 项即视为 XR-001 关闭。
---
## 10. 跨域数据库约束(新增)
1. 供应域不是独立孤岛,必须依赖 Core/IAM/Auth/Billing/Audit 五域主表。
2. 供应域关键表必须补齐三类字段:
1. 加密字段:`*_cipher_algo``*_kms_key_alias``*_key_version``*_fingerprint`
2. 单位字段:`quota_unit``price_unit``amount_unit``currency_code`
3. 审计字段:`request_id``idempotency_key``audit_trace_id``created_ip``updated_ip``version`
3. 数据库实施顺序固定:
1. `platform_core_schema_v1.sql`
2. `supply_schema_v1.sql`
3. `supply_schema_v1_patch_2026-03-27.sql`
4. 未完成上述顺序与字段补齐,不得判定 XR-001 关闭。

View File

@@ -1,7 +1,7 @@
# 供应侧测试方案增强版XR-002
- 版本v1.0
- 日期2026-03-25
- 版本v1.1
- 日期2026-03-27
- 状态:生效(测试执行基线)
- 目标:形成“需求-接口-测试-指标-门禁”全链路闭环,补齐并发与重放风险覆盖
- 关联文档:
@@ -35,22 +35,26 @@
## 2. 测试追踪矩阵Requirement -> API -> Test -> Metric -> Gate
| 需求ID | 需求描述 | API | 测试用例 | 验收指标 | 门禁映射 |
|---|---|---|---|---|---|
| R-ACC-001 | 账号凭证验证成功可视化 | `POST /accounts/verify` | UI-SUP-ACC-001 | 验证成功率 >=99.5% | SUP-004 |
| R-ACC-002 | 挂载需风险确认与审计 | `POST /accounts` | UI-SUP-ACC-002 | 审计覆盖率=100% | SUP-004 |
| R-ACC-003 | 账号状态不跳态 | `POST /accounts/{id}/activate/suspend` | UI-SUP-ACC-003/004 + INT-ACC-STATE-001 | 冲突可解释率=100% | SUP-004 |
| R-ACC-004 | 活跃账号不可删除 | `DELETE /accounts/{id}` | UI-SUP-ACC-005 | 违规删除成功率=0 | SUP-004 |
| R-PKG-001 | 草稿保存可追踪 | `POST /packages/draft` | UI-SUP-PKG-001 | 保存成功率>=99.5% | SUP-005 |
| R-PKG-002 | 套餐发布满足保护价与状态约束 | `POST /packages/{id}/publish` | UI-SUP-PKG-002 + INT-PKG-INV-001 | 保护价违规放行率=0 | SUP-005 |
| R-PKG-003 | 批量调价部分失败可回执 | `POST /packages/batch-price` | UI-SUP-PKG-005 | 明细完备率=100% | SUP-005 |
| R-SET-001 | 提现发起防重复防双扣 | `POST /settlements/withdraw` | UI-SUP-SET-002 + CON-SET-001 | M-004/M-005 达标 | SUP-006 |
| R-SET-002 | 处理中/已完成不可撤销 | `POST /settlements/{id}/cancel` | UI-SUP-SET-003 + INT-SET-STATE-001 | 跳态成功率=0 | SUP-006 |
| R-SET-003 | 对账单导出不泄露敏感信息 | `GET /settlements/{id}/statement` | UI-SUP-SET-004 + SEC-SUP-001 | M-013=0 | SUP-006/SUP-007 |
| R-SEC-001 | 仅平台凭证入站 | 全部北向 API | SEC-SUP-002 | M-014=100% | SUP-007 |
| R-SEC-002 | 外部 query key 全拒绝 | 全部北向 API | SEC-SUP-002 | M-016=100% | SUP-007 |
| R-SEC-003 | 需求方不可绕平台直连 | 出网策略与告警 | SEC-SUP-002 + SEC-DIRECT-001 | M-015=0 | SUP-007 |
| R-UX-001 | 按钮可见性和禁用规则正确 | 三页面全部按钮 | UI-DESIGN-QA-001~020 | 按钮规则通过率=100% | SUP-003/SUP-008 |
| 需求ID | 需求描述 | API | api_alias | 测试用例 | 验收指标 | 门禁映射 |
|---|---|---|---|---|---|---|
| R-ACC-001 | 账号凭证验证成功可视化 | `POST /api/v1/supply/accounts/verify` | - | UI-SUP-ACC-001 | 验证成功率 >=99.5% | SUP-004 |
| R-ACC-002 | 挂载需风险确认与审计 | `POST /api/v1/supply/accounts` | - | UI-SUP-ACC-002 | 审计覆盖率=100% | SUP-004 |
| R-ACC-003 | 账号状态不跳态 | `POST /api/v1/supply/accounts/{accountId}/activate` / `POST /api/v1/supply/accounts/{accountId}/suspend` | `POST /api/v1/supply/accounts/{id}/activate` / `POST /api/v1/supply/accounts/{id}/suspend` | UI-SUP-ACC-003/004 + INT-ACC-STATE-001 | 冲突可解释率=100% | SUP-004 |
| R-ACC-004 | 活跃账号不可删除 | `DELETE /api/v1/supply/accounts/{accountId}` | `DELETE /api/v1/supply/accounts/{id}` | UI-SUP-ACC-005 | 违规删除成功率=0 | SUP-004 |
| R-PKG-001 | 草稿保存可追踪 | `POST /api/v1/supply/packages/draft` | - | UI-SUP-PKG-001 | 保存成功率>=99.5% | SUP-005 |
| R-PKG-002 | 套餐发布满足保护价与状态约束 | `POST /api/v1/supply/packages/{packageId}/publish` | `POST /api/v1/supply/packages/{id}/publish` | UI-SUP-PKG-002 + INT-PKG-INV-001 | 保护价违规放行率=0 | SUP-005 |
| R-PKG-003 | 批量调价部分失败可回执 | `POST /api/v1/supply/packages/batch-price` | - | UI-SUP-PKG-005 | 明细完备率=100% | SUP-005 |
| R-SET-001 | 提现发起防重复防双扣 | `POST /api/v1/supply/settlements/withdraw` | - | UI-SUP-SET-002 + CON-SET-001 | M-004/M-005 达标 | SUP-006 |
| R-SET-002 | 处理中/已完成不可撤销 | `POST /api/v1/supply/settlements/{settlementId}/cancel` | `POST /api/v1/supply/settlements/{id}/cancel` | UI-SUP-SET-003 + INT-SET-STATE-001 | 跳态成功率=0 | SUP-006 |
| R-SET-003 | 对账单导出不泄露敏感信息 | `GET /api/v1/supply/settlements/{settlementId}/statement` | `GET /api/v1/supply/settlements/{id}/statement` | UI-SUP-SET-004 + SEC-SUP-001 | M-013=0 | SUP-006/SUP-007 |
| R-SEC-001 | 仅平台凭证入站 | 全部北向 API | - | SEC-SUP-002 | M-014=100% | SUP-007 |
| R-SEC-002 | 外部 query key 全拒绝 | 全部北向 API | - | SEC-SUP-002 | M-016=100% | SUP-007 |
| R-SEC-003 | 需求方不可绕平台直连 | 出网策略与告警 | - | SEC-SUP-002 + SEC-DIRECT-001 | M-015=0 | SUP-007 |
| R-UX-001 | 按钮可见性和禁用规则正确 | 三页面全部按钮 | - | UI-DESIGN-QA-001~020 | 按钮规则通过率=100% | SUP-003/SUP-008 |
跨域映射补充:
1. 全局 P0 中预算/告警/组织级账单导出映射见:`docs/product/global_p0_to_supply_platform_mapping_v1_2026-03-27.md`
2. 对应追踪项已并入:`reports/supply_traceability_matrix_2026-03-25.csv``R-PLAT-001~003`)。
---
@@ -143,6 +147,16 @@
4. `SEC-SUP Gate`:凭证边界与泄露扫描(阻断)。
5. `PERF/REL Gate`:每晚定时跑,异常进入发布前强制复核。
## 7.3 分阶段质量门禁(防偏航)
1. G0 Requirement Gate检查 PRD/OpenAPI/按钮清单版本一致,任一漂移阻断开发。
2. G1 Design Gate检查 DDL、状态机、不变量、审计字段齐套缺一阻断联调。
3. G2 Dev Gate单测与契约测试达标后才允许合并。
4. G3 Integration GateDB/Redis/Outbox/权限链路通过后才允许提测。
5. G4 Release GateSUP-004~SUP-007 与安全门禁全绿才允许发布。
6. G5 Post Gate发布后 24h 观察窗口出现 P0/P1 立即冻结后续升波。
7. 指标约束:`M-018=100%``M-019=100%`,否则回退到失败阶段整改。
## 7.2 失败策略
1. P0 用例失败:立即阻断发布 + 当日复盘。
@@ -158,6 +172,7 @@
1. PRD 按钮级规格冻结。
2. OpenAPI 字段冻结。
3. 技术增强稿XR-001已落地。
4. 路径一致性检查通过API 字段与 OpenAPI 主路径一致alias 映射完整)。
## 8.2 退出Exit

View File

@@ -0,0 +1,37 @@
# 供应侧追踪矩阵生成规则v1.0
- 日期2026-03-27
- 适用文件:`reports/supply_traceability_matrix_2026-03-25.csv`
- 目标:保证 Requirement -> API -> Test -> Metric -> Gate 的自动化可追踪与口径一致。
## 1. 字段规范
1. `requirement_id`:唯一且稳定,不得复用。
2. `api`:必须使用 OpenAPI 主路径与精确参数名(如 `{accountId}``{packageId}``{settlementId}`)。
3. `api_alias`:仅记录历史兼容路径;无兼容值填写 `-`
4. `test_case`:使用 `|` 连接多个用例 ID顺序按主路径优先。
5. `metric`:使用 SSOT 中的统一指标名,禁止自造同义词。
6. `gate`:映射 SUP/SEC/XR 门禁,多个值用 `|` 分隔。
7. `status``PLANNED/RUNNING/PASS/FAIL/BLOCKED` 五态。
## 2. 生成流程
1. 从按钮级 PRD 抽取需求项并形成 `requirement_id`
2. 从 OpenAPI 提取接口主路径,填入 `api`
3. 对历史路径或迁移路径填入 `api_alias`
4. 绑定测试用例、指标、门禁并指定 owner。
5. 由 QA 执行完整性检查后发布 CSV。
## 3. 校验规则
1. `api` 必须可在 OpenAPI 中检索命中。
2. `api_alias` 不得与 `api` 完全相同。
3. `gate` 必须在任务单中存在对应条目。
4. 每条记录必须有 `evidence_path`
5. 任一校验失败,`M-019` 计为不通过。
## 4. 变更治理
1. 修改 `api` 视为高风险变更,必须同步更新用例与门禁映射。
2. 新增 alias 必须附迁移原因和下线计划。
3. 每次变更后需执行一次路径一致性检查并留痕。

View File

@@ -1,7 +1,7 @@
# 优化技术架构设计(最小可运营栈 + 触发式扩容)
- 版本v2.0
- 日期2026-03-18
- 版本v2.1
- 日期2026-03-27
- 目标:降低 S0/S1 运维复杂度,同时保证 S2 替换目标可达。
---
@@ -110,6 +110,8 @@ Internet
- `llm_gateway_subapi_evolution_plan_v4_2_2026-03-24.md`
- `acceptance_gate_single_source_v1_2026-03-18.md`
- `test_plan_go_aligned_v1_2026-03-18.md`
- `dependency_compatibility_audit_baseline_v1_2026-03-27.md`
- `database_domain_model_and_governance_v1_2026-03-27.md`
---
@@ -120,3 +122,42 @@ Internet
3. 发布扩容触发条件评审模板(无触发条件不得引入组件)。
4. 将运维看板与门禁阈值绑定到唯一验收门禁表。
5. 完成一次“升级 + 灰度 + 自动回滚”全链路演练。
---
## 8. 依赖兼容性审计(新增强制门禁)
1. 发布前必须产出四类证据SBOM、锁文件差异、兼容矩阵、风险清单。
2.`subapi/provider SDK` 执行精确版本锁定(`X.Y.Z`),禁止“仅锁主次版本”。
3. 任一依赖发生 major 变更,必须附兼容影响评估与回滚演练记录。
4. 依赖审计结果接入门禁指标 `M-017`,要求 `dependency_compat_audit_pass_pct=100%`
5. 运行时、数据层、构建镜像三类版本必须可追溯到同一发布包,禁止“文档版本”和“运行版本”漂移。
---
## 9. 分阶段质量检查(防偏离主线)
### 9.1 阶段门禁定义
| 阶段 | Gate | 必达条件 | 阻断动作 |
|---|---|---|---|
| G0 需求冻结 | Requirement Gate | P0/P1 需求、按钮、接口状态全部冻结 | 禁止进入开发 |
| G1 设计冻结 | Design Gate | 数据模型、OpenAPI、状态机与审计字段齐套 | 禁止进入联调 |
| G2 开发自检 | Dev Gate | 单元/契约测试通过,覆盖率达标 | 禁止合并 |
| G3 集成验证 | Integration Gate | DB/缓存/外部依赖集成测试通过 | 禁止预发布 |
| G4 发布演练 | Release Gate | 回滚演练、性能门禁、安全门禁通过 | 禁止生产发布 |
| G5 发布观察 | Post Gate | 24h 指标稳定,无 P0/P1 回归 | 冻结后续升波 |
### 9.2 防偏航机制
1. 需求追踪覆盖率(`M-019`)必须 100%,每条 P0 需求都能映射到 API/测试/指标/Gate。
2. 阶段通过率(`M-018`)必须 100%,任一阶段失败禁止“跳阶段推进”。
3. 每日执行“需求-设计-测试-门禁”一致性巡检,发现漂移 24h 内关闭。
4. 所有变更按 `request_id + trace_id` 留痕,确保故障可逆向定位到需求与提交。
---
## 10. 本版补充结论
1. 架构基线从“最小可运营栈”扩展为“最小可运营栈 + 依赖可审计 + 分阶段质量闭环”。
2. 未完成依赖兼容审计或阶段门禁的变更,不得进入 `GO` 决策。

View File

@@ -0,0 +1,122 @@
# 平台鉴权与 Token 校验中间件设计TOK-002
- 版本v1.0
- 日期2026-03-29
- 状态:开发实施设计基线
- 依赖:`docs/token_runtime_minimal_spec_v1.md`
- 目标:实现“仅平台凭证入站”,并为 M-014/M-016/M-021 提供可验证链路。
## 1. 设计目标
1. 所有北向请求必须通过平台凭证校验。
2. 外部 `query key` 入站一律拒绝并记录审计事件。
3. 鉴权结果可追踪到 `request_id + subject_id + token_id`
4. 在不泄露上游凭证的前提下返回标准错误码。
## 2. 适用范围
1. 路由范围:`/api/v1/supply/*``/api/v1/platform/*`
2. 鉴权头:仅支持 `Authorization: Bearer <token>`
3. 排除范围:健康检查、内部探针、公开静态资源。
## 3. 中间件链路
## 3.1 处理顺序
1. `RequestIdMiddleware`
2. `QueryKeyRejectMiddleware`
3. `BearerExtractMiddleware`
4. `TokenVerifyMiddleware`
5. `TokenStatusCheckMiddleware`
6. `ScopeRoleAuthzMiddleware`
7. `AuditEmitMiddleware`
## 3.2 关键规则
1. `QueryKeyRejectMiddleware`
- 拒绝任意 `?key=``?api_key=``?token=` 形式外部参数。
- 返回 `401 QUERY_KEY_NOT_ALLOWED`
2. `BearerExtractMiddleware`
-`Authorization` 直接 `401 AUTH_MISSING_BEARER`
3. `TokenVerifyMiddleware`
- 校验签名、`iss``aud``exp``nbf``jti`
- 签名失败返回 `401 AUTH_INVALID_TOKEN`
4. `TokenStatusCheckMiddleware`
- 查询 token 状态缓存(`active/revoked/expired`)。
- `revoked/expired` 返回 `401 AUTH_TOKEN_INACTIVE`
5. `ScopeRoleAuthzMiddleware`
- 按路由匹配 scope不足返回 `403 AUTH_SCOPE_DENIED`
## 4. 数据与缓存策略
1. 状态源:`platform_token_registry`(运行态主表)。
2. 热缓存:`token_status_cache`TTL 30s
3. 吊销传播:
- 吊销事件写入总线后1~5 秒内刷新缓存。
- 验收阈值:吊销生效延迟 `<= 5s`
## 5. 错误语义
| 场景 | HTTP | error.code | 说明 |
|---|---|---|---|
| 缺失 Bearer | 401 | AUTH_MISSING_BEARER | 请求头缺失 |
| query key 外部入站 | 401 | QUERY_KEY_NOT_ALLOWED | 边界拒绝 |
| token 无效/签名失败 | 401 | AUTH_INVALID_TOKEN | 校验失败 |
| token 已吊销/过期 | 401 | AUTH_TOKEN_INACTIVE | 状态不可用 |
| scope 不足 | 403 | AUTH_SCOPE_DENIED | 权限不足 |
## 6. 审计事件TOK-004 依赖)
1. `token.authn.success`
2. `token.authn.fail`
3. `token.authz.denied`
4. `token.query_key.rejected`
最小字段:
1. `event_id`
2. `request_id`
3. `token_id`(可空,提取失败时为空)
4. `subject_id`(可空)
5. `route`
6. `result_code`
7. `client_ip`
8. `created_at`
## 7. 伪代码(实现参考)
```text
onRequest(req):
reqId = ensureRequestId(req)
if hasExternalQueryKey(req):
emitAudit("token.query_key.rejected", reqId, route, clientIp)
return 401 QUERY_KEY_NOT_ALLOWED
bearer = parseBearer(req.headers.Authorization)
if bearer is null:
emitAudit("token.authn.fail", reqId, route, "AUTH_MISSING_BEARER")
return 401 AUTH_MISSING_BEARER
claims = verifyToken(bearer)
if verify failed:
emitAudit("token.authn.fail", reqId, route, "AUTH_INVALID_TOKEN")
return 401 AUTH_INVALID_TOKEN
status = getTokenStatus(claims.jti)
if status != active:
emitAudit("token.authn.fail", reqId, route, "AUTH_TOKEN_INACTIVE")
return 401 AUTH_TOKEN_INACTIVE
if !checkScopeRole(claims.scope, claims.role, route):
emitAudit("token.authz.denied", reqId, route, "AUTH_SCOPE_DENIED")
return 403 AUTH_SCOPE_DENIED
attachPrincipal(req, claims)
emitAudit("token.authn.success", reqId, route, "OK")
pass
```
## 8. 开发阶段验收(设计级)
1.`TOK-001` 角色、状态机、审计字段一致。
2.`M-014/M-016` 指标定义一致。
3. 与 OpenAPI token 契约草案字段一致。

View File

@@ -0,0 +1,76 @@
# TOK-003/TOK-004 测试断言清单(生命周期 + 审计事件)
- 版本v1.0
- 日期2026-03-29
- 状态:开发实施测试基线
- 适用任务:`TOK-003``TOK-004`
## 1. 测试范围
1. TOK-003签发、续期、吊销、过期生命周期。
2. TOK-004签发/校验失败/吊销/越权事件入库与可追踪。
## 2. 前置数据
1. 租户:`tenant_id=1001`
2. 主体:
- `subject_owner=2001`
- `subject_viewer=2002`
3. 角色策略:
- owner: `supply:*`
- viewer: `supply:read`
4. 观测阈值:
- 吊销生效延迟 `<=5s`
- 审计事件落库延迟 `<=3s`
## 3. TOK-003 生命周期断言
| 用例ID | 场景 | 步骤 | 断言 |
|---|---|---|---|
| TOK-LIFE-001 | 签发成功 | 1) 调用 `POST /tokens/issue` 2) 记录返回 | 1) `status=active` 2) `expires_at>issued_at` 3) `token_id` 唯一 |
| TOK-LIFE-002 | 签发参数非法 | 1) `ttl_seconds` 超上限 2) 调用签发 | 1) 返回 `400` 2) 不落 active token |
| TOK-LIFE-003 | 同键幂等签发重放 | 1) 相同 `Idempotency-Key` 重复提交 | 1) 返回同一 `token_id` 2) 无重复写入 |
| TOK-LIFE-004 | 续期成功 | 1) 调用 `POST /tokens/{tokenId}/refresh` | 1) `expires_at` 延后 2) `status=active` |
| TOK-LIFE-005 | 吊销成功 | 1) 调用 `POST /tokens/{tokenId}/revoke` 2) 立刻 introspect | 1) 最终 `status=revoked` 2) 生效延迟 <=5s |
| TOK-LIFE-006 | 吊销后访问受限接口 | 1) 使用被吊销 token 访问受保护路由 | 1) 返回 `401 AUTH_TOKEN_INACTIVE` |
| TOK-LIFE-007 | 过期自动失效 | 1) 签发短 TTL token 2) 等待过期 3) introspect | 1) `status=expired` 2) 返回不可用错误 |
| TOK-LIFE-008 | viewer 越权写操作 | 1) viewer token 调用写接口 | 1) 返回 `403 AUTH_SCOPE_DENIED` 2) 无写入副作用 |
## 4. TOK-004 审计事件断言
| 用例ID | 场景 | 步骤 | 断言 |
|---|---|---|---|
| TOK-AUD-001 | 签发成功事件 | 执行 TOK-LIFE-001 | 1) 存在 `token.issue.success` 2) 字段齐全 |
| TOK-AUD-002 | 签发失败事件 | 执行 TOK-LIFE-002 | 1) 存在 `token.issue.fail` 2) `result_code` 准确 |
| TOK-AUD-003 | 鉴权失败事件 | 无效 token 访问受保护路由 | 1) `token.authn.fail` 入库 2) 含 `request_id` |
| TOK-AUD-004 | 越权事件 | 执行 TOK-LIFE-008 | 1) `token.authz.denied` 入库 2) 含 `subject_id` |
| TOK-AUD-005 | 吊销事件 | 执行 TOK-LIFE-005 | 1) `token.revoke.success` 入库 2) 含 `token_id` |
| TOK-AUD-006 | query key 拒绝事件 | 使用 query key 访问接口 | 1) `token.query_key.rejected` 入库 2) 不出现敏感值 |
| TOK-AUD-007 | 事件不可篡改 | 重复读取同 `event_id` | 1) 核心字段不可变 2) 时间顺序正确 |
## 5. 字段级硬断言
每条审计事件必须包含:
1. `event_id`
2. `request_id`
3. `result_code`
4. `route`
5. `created_at`
可选字段规则:
1. `token_id`:提取失败场景可空,其余场景必填。
2. `subject_id`:匿名失败场景可空,其余场景必填。
禁止项:
1. 不得写入上游供应方凭证明文。
2. 不得写入完整 `access_token` 明文(仅允许哈希或指纹)。
## 6. 结果判定
1. TOK-003 通过标准:
- `TOK-LIFE-*` 全通过
- 吊销延迟阈值满足 `<=5s`
2. TOK-004 通过标准:
- `TOK-AUD-*` 全通过
- 审计字段完整率 `=100%`
- 敏感数据泄露事件 `=0`

View File

@@ -0,0 +1,92 @@
# Token 运行态最小实现规格TOK-001
- 版本v1.0
- 日期2026-03-27
- 状态:开发实施基线
- 对应任务:`TOK-001`
## 1. 目标
在不依赖真实 staging 参数的前提下,定义可落地的 token 运行态最小能力集,为后续 TOK-002~TOK-007 提供统一实施输入。
## 2. 最小能力范围MVP
1. 平台签发:短期访问 tokenowner/viewer/admin
2. 入站校验:仅平台凭证有效,拒绝 query key 外部入站。
3. 生命周期:签发、续期、吊销、过期。
4. 边界审计:签发/校验失败/吊销/越权事件全量入审计。
5. 指标可观测:可计算 M-013~M-016 与 M-021。
## 3. 角色与权限
| 角色 | 能力 | 约束 |
|---|---|---|
| owner | 管理供应侧账号、套餐、结算 | 不可读取上游凭证明文 |
| viewer | 只读查询 | 不可执行写操作 |
| admin | 风控与审计管理 | 仅平台内部可用 |
## 4. Token 数据模型(最小字段)
| 字段 | 类型 | 说明 |
|---|---|---|
| token_id | string | 平台内部唯一标识 |
| subject_id | string | 用户/服务主体ID |
| role | string | owner/viewer/admin |
| issued_at | datetime | 签发时间 |
| expires_at | datetime | 过期时间 |
| status | string | active/revoked/expired |
| scope | string[] | 授权范围 |
| request_id | string | 请求追踪ID |
| revoked_reason | string | 吊销原因(可空) |
## 5. 生命周期状态机
`active -> revoked -> expired`
规则:
1. `revoked` 不可恢复为 `active`,需重新签发。
2. `expires_at` 到期自动进入 `expired`
3. 续期只能对 `active` token 生效。
## 6. 核心接口(草案)
1. `POST /api/v1/platform/tokens/issue`
2. `POST /api/v1/platform/tokens/{tokenId}/refresh`
3. `POST /api/v1/platform/tokens/{tokenId}/revoke`
4. `POST /api/v1/platform/tokens/introspect`
5. `GET /api/v1/platform/tokens/audit-events`
返回要求:
1. 不回传任何上游供应方凭证。
2. 错误码需区分:无效、过期、越权、吊销。
3. 审计查询接口仅返回审计字段,不返回 access token 或任何上游凭证明文。
## 7. 安全约束
1. token 存储需采用哈希或加密指纹,禁止明文落库。
2. 校验路径必须记录 `request_id` 与调用来源。
3. 外部 query key 入站请求必须拒绝并记录事件。
4. 任一泄露事件触发 P0。
## 8. 审计事件最小集
1. `token.issue.success/fail`
2. `token.introspect.success/fail`
3. `token.refresh.success/fail`
4. `token.revoke.success/fail`
5. `token.authz.denied`
审计字段:
1. `event_id`
2. `request_id`
3. `operator_id`
4. `subject_id`
5. `token_id`
6. `result_code`
7. `created_at`
## 9. 验收标准TOK-001 关闭条件)
1. 本规格被 `ARCH + SEC + PLAT` 确认并引用到执行任务单。
2. 后续 TOK-002~TOK-004 的实现字段与本规格一致。
3. 不得新增“直接向终端用户分发上游 token”的路径。

View File

@@ -0,0 +1,6 @@
.git
.tools
reports
review
tests
**/*_test.go

View File

@@ -0,0 +1,13 @@
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/platform-token-runtime ./cmd/platform-token-runtime
FROM gcr.io/distroless/static-debian12
WORKDIR /app
COPY --from=builder /out/platform-token-runtime /app/platform-token-runtime
EXPOSE 18081
ENTRYPOINT ["/app/platform-token-runtime"]

View File

@@ -0,0 +1,41 @@
# platform-token-runtimeTOK-002/003/004 开发实现)
本目录用于承载 token 运行态的开发阶段实现,不依赖真实 staging 参数。
## 文件说明
1. `cmd/platform-token-runtime/main.go`可执行服务入口HTTP + 健康检查)。
2. `internal/httpapi/token_api.go``issue/refresh/revoke/introspect` 接口处理。
3. `internal/httpapi/token_api_test.go`HTTP 接口单测。
4. `internal/auth/middleware/*`TOK-002 中间件与单测。
2. `internal/auth/service/token_verifier.go`:鉴权依赖接口、错误码、审计事件常量。
3. `internal/auth/service/inmemory_runtime.go`:开发阶段最小可运行内存实现(签发/续期/吊销/introspect + 鉴权接口实现)。
4. `internal/token/*_template_test.go`TOK-003/004 测试模板(按 `TOK-LIFE-*`/`TOK-AUD-*` 对齐)。
5. `internal/token/*_executable_test.go`:已转可执行用例(`TOK-LIFE-001~008``TOK-AUD-001~007`)。
6. `Dockerfile`:运行时镜像构建工件。
## 设计边界
1. 仅支持 `Authorization: Bearer <token>` 入站。
2. 外部 query key (`key/api_key/token`) 一律拒绝。
3. 不在任何响应或审计字段中输出上游凭证明文。
## 本地测试
```bash
cd "/home/long/project/立交桥/platform-token-runtime"
export PATH="/home/long/project/立交桥/.tools/go-current/bin:$PATH"
export GOCACHE="/tmp/go-cache"
export GOPATH="/tmp/go"
go test ./...
```
## 本地运行
```bash
cd "/home/long/project/立交桥/platform-token-runtime"
export PATH="/home/long/project/立交桥/.tools/go-current/bin:$PATH"
go run ./cmd/platform-token-runtime
```
服务默认监听 `:18081`,可通过 `TOKEN_RUNTIME_ADDR` 覆盖。

View File

@@ -0,0 +1,63 @@
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"lijiaoqiao/platform-token-runtime/internal/auth/service"
"lijiaoqiao/platform-token-runtime/internal/httpapi"
)
func main() {
addr := envOrDefault("TOKEN_RUNTIME_ADDR", ":18081")
runtime := service.NewInMemoryTokenRuntime(nil)
auditor := service.NewMemoryAuditEmitter()
api := httpapi.NewTokenAPI(runtime, auditor, time.Now)
mux := http.NewServeMux()
mux.HandleFunc("/actuator/health", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"UP"}`))
})
api.Register(mux)
srv := &http.Server{
Addr: addr,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 30 * time.Second,
}
go func() {
log.Printf("platform-token-runtime listening on %s", addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen failed: %v", err)
}
}()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("graceful shutdown failed: %v", err)
}
}
func envOrDefault(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}

View File

@@ -0,0 +1,3 @@
module lijiaoqiao/platform-token-runtime
go 1.22

View File

@@ -0,0 +1,51 @@
package middleware
import (
"net/http"
"strings"
"time"
"lijiaoqiao/platform-token-runtime/internal/auth/service"
)
var disallowedQueryKeys = []string{"key", "api_key", "token"}
func QueryKeyRejectMiddleware(next http.Handler, auditor service.AuditEmitter, now func() time.Time) http.Handler {
if next == nil {
next = http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
}
if now == nil {
now = defaultNowFunc
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, exists := externalQueryKey(r)
if !exists {
next.ServeHTTP(w, r)
return
}
requestID := ensureRequestID(r, now)
emitAuditEvent(r.Context(), auditor, service.AuditEvent{
EventName: service.EventTokenQueryKeyRejected,
RequestID: requestID,
Route: r.URL.Path,
ResultCode: service.CodeQueryKeyNotAllowed,
ClientIP: extractClientIP(r),
CreatedAt: now(),
})
writeError(w, http.StatusUnauthorized, requestID, service.CodeQueryKeyNotAllowed, "query key ingress is not allowed")
})
}
func externalQueryKey(r *http.Request) (string, bool) {
values := r.URL.Query()
for key := range values {
lowered := strings.ToLower(key)
for _, disallowed := range disallowedQueryKeys {
if lowered == disallowed {
return key, true
}
}
}
return "", false
}

View File

@@ -0,0 +1,270 @@
package middleware
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
"time"
"lijiaoqiao/platform-token-runtime/internal/auth/model"
"lijiaoqiao/platform-token-runtime/internal/auth/service"
)
const requestIDHeader = "X-Request-Id"
var defaultNowFunc = time.Now
type contextKey string
const (
requestIDKey contextKey = "request_id"
principalKey contextKey = "principal"
)
type AuthMiddlewareConfig struct {
Verifier service.TokenVerifier
StatusResolver service.TokenStatusResolver
Authorizer service.RouteAuthorizer
Auditor service.AuditEmitter
ProtectedPrefixes []string
ExcludedPrefixes []string
Now func() time.Time
}
func BuildTokenAuthChain(cfg AuthMiddlewareConfig, next http.Handler) http.Handler {
handler := TokenAuthMiddleware(cfg)(next)
handler = QueryKeyRejectMiddleware(handler, cfg.Auditor, cfg.Now)
handler = RequestIDMiddleware(handler, cfg.Now)
return handler
}
func RequestIDMiddleware(next http.Handler, now func() time.Time) http.Handler {
if next == nil {
return http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
}
if now == nil {
now = defaultNowFunc
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := ensureRequestID(r, now)
w.Header().Set(requestIDHeader, requestID)
next.ServeHTTP(w, r)
})
}
func TokenAuthMiddleware(cfg AuthMiddlewareConfig) func(http.Handler) http.Handler {
cfg = cfg.withDefaults()
return func(next http.Handler) http.Handler {
if next == nil {
next = http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !cfg.shouldProtect(r.URL.Path) {
next.ServeHTTP(w, r)
return
}
requestID := ensureRequestID(r, cfg.Now)
if cfg.Verifier == nil || cfg.StatusResolver == nil || cfg.Authorizer == nil {
writeError(w, http.StatusServiceUnavailable, requestID, service.CodeAuthNotReady, "auth middleware dependencies are not ready")
return
}
rawToken, ok := extractBearerToken(r.Header.Get("Authorization"))
if !ok {
emitAuditEvent(r.Context(), cfg.Auditor, service.AuditEvent{
EventName: service.EventTokenAuthnFail,
RequestID: requestID,
Route: r.URL.Path,
ResultCode: service.CodeAuthMissingBearer,
ClientIP: extractClientIP(r),
CreatedAt: cfg.Now(),
})
writeError(w, http.StatusUnauthorized, requestID, service.CodeAuthMissingBearer, "missing bearer token")
return
}
claims, err := cfg.Verifier.Verify(r.Context(), rawToken)
if err != nil {
emitAuditEvent(r.Context(), cfg.Auditor, service.AuditEvent{
EventName: service.EventTokenAuthnFail,
RequestID: requestID,
Route: r.URL.Path,
ResultCode: service.CodeAuthInvalidToken,
ClientIP: extractClientIP(r),
CreatedAt: cfg.Now(),
})
writeError(w, http.StatusUnauthorized, requestID, service.CodeAuthInvalidToken, "invalid bearer token")
return
}
tokenStatus, err := cfg.StatusResolver.Resolve(r.Context(), claims.TokenID)
if err != nil || tokenStatus != service.TokenStatusActive {
emitAuditEvent(r.Context(), cfg.Auditor, service.AuditEvent{
EventName: service.EventTokenAuthnFail,
RequestID: requestID,
TokenID: claims.TokenID,
SubjectID: claims.SubjectID,
Route: r.URL.Path,
ResultCode: service.CodeAuthTokenInactive,
ClientIP: extractClientIP(r),
CreatedAt: cfg.Now(),
})
writeError(w, http.StatusUnauthorized, requestID, service.CodeAuthTokenInactive, "token is inactive")
return
}
if !cfg.Authorizer.Authorize(r.URL.Path, r.Method, claims.Scope, claims.Role) {
emitAuditEvent(r.Context(), cfg.Auditor, service.AuditEvent{
EventName: service.EventTokenAuthzDenied,
RequestID: requestID,
TokenID: claims.TokenID,
SubjectID: claims.SubjectID,
Route: r.URL.Path,
ResultCode: service.CodeAuthScopeDenied,
ClientIP: extractClientIP(r),
CreatedAt: cfg.Now(),
})
writeError(w, http.StatusForbidden, requestID, service.CodeAuthScopeDenied, "scope denied")
return
}
principal := model.Principal{
RequestID: requestID,
TokenID: claims.TokenID,
SubjectID: claims.SubjectID,
Role: claims.Role,
Scope: append([]string(nil), claims.Scope...),
}
ctx := context.WithValue(r.Context(), principalKey, principal)
ctx = context.WithValue(ctx, requestIDKey, requestID)
emitAuditEvent(ctx, cfg.Auditor, service.AuditEvent{
EventName: service.EventTokenAuthnSuccess,
RequestID: requestID,
TokenID: claims.TokenID,
SubjectID: claims.SubjectID,
Route: r.URL.Path,
ResultCode: "OK",
ClientIP: extractClientIP(r),
CreatedAt: cfg.Now(),
})
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func RequestIDFromContext(ctx context.Context) (string, bool) {
if ctx == nil {
return "", false
}
value, ok := ctx.Value(requestIDKey).(string)
return value, ok
}
func PrincipalFromContext(ctx context.Context) (model.Principal, bool) {
if ctx == nil {
return model.Principal{}, false
}
value, ok := ctx.Value(principalKey).(model.Principal)
return value, ok
}
func (cfg AuthMiddlewareConfig) withDefaults() AuthMiddlewareConfig {
if cfg.Now == nil {
cfg.Now = defaultNowFunc
}
if len(cfg.ProtectedPrefixes) == 0 {
cfg.ProtectedPrefixes = []string{"/api/v1/supply", "/api/v1/platform"}
}
if len(cfg.ExcludedPrefixes) == 0 {
cfg.ExcludedPrefixes = []string{"/healthz", "/metrics", "/readyz"}
}
return cfg
}
func (cfg AuthMiddlewareConfig) shouldProtect(path string) bool {
for _, prefix := range cfg.ExcludedPrefixes {
if strings.HasPrefix(path, prefix) {
return false
}
}
for _, prefix := range cfg.ProtectedPrefixes {
if strings.HasPrefix(path, prefix) {
return true
}
}
return false
}
func ensureRequestID(r *http.Request, now func() time.Time) string {
if now == nil {
now = defaultNowFunc
}
if requestID, ok := RequestIDFromContext(r.Context()); ok && requestID != "" {
return requestID
}
requestID := strings.TrimSpace(r.Header.Get(requestIDHeader))
if requestID == "" {
requestID = fmt.Sprintf("req-%d", now().UnixNano())
}
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
*r = *r.WithContext(ctx)
return requestID
}
func extractBearerToken(authHeader string) (string, bool) {
const bearerPrefix = "Bearer "
if !strings.HasPrefix(authHeader, bearerPrefix) {
return "", false
}
token := strings.TrimSpace(strings.TrimPrefix(authHeader, bearerPrefix))
return token, token != ""
}
func emitAuditEvent(ctx context.Context, auditor service.AuditEmitter, event service.AuditEvent) {
if auditor == nil {
return
}
_ = auditor.Emit(ctx, event)
}
type errorResponse struct {
RequestID string `json:"request_id"`
Error errorPayload `json:"error"`
}
type errorPayload struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]any `json:"details,omitempty"`
}
func writeError(w http.ResponseWriter, status int, requestID, code, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
payload := errorResponse{
RequestID: requestID,
Error: errorPayload{
Code: code,
Message: message,
},
}
_ = json.NewEncoder(w).Encode(payload)
}
func extractClientIP(r *http.Request) string {
xForwardedFor := strings.TrimSpace(r.Header.Get("X-Forwarded-For"))
if xForwardedFor != "" {
parts := strings.Split(xForwardedFor, ",")
return strings.TrimSpace(parts[0])
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err == nil {
return host
}
return r.RemoteAddr
}

View File

@@ -0,0 +1,244 @@
package middleware
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"lijiaoqiao/platform-token-runtime/internal/auth/model"
"lijiaoqiao/platform-token-runtime/internal/auth/service"
)
var fixedNow = func() time.Time {
return time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC)
}
type fakeVerifier struct {
token service.VerifiedToken
err error
}
func (f *fakeVerifier) Verify(context.Context, string) (service.VerifiedToken, error) {
return f.token, f.err
}
type fakeStatusResolver struct {
status service.TokenStatus
err error
}
func (f *fakeStatusResolver) Resolve(context.Context, string) (service.TokenStatus, error) {
return f.status, f.err
}
type fakeAuthorizer struct {
allowed bool
}
func (f *fakeAuthorizer) Authorize(string, string, []string, string) bool {
return f.allowed
}
type fakeAuditor struct {
events []service.AuditEvent
}
func (f *fakeAuditor) Emit(_ context.Context, event service.AuditEvent) error {
f.events = append(f.events, event)
return nil
}
func TestQueryKeyRejectMiddleware(t *testing.T) {
auditor := &fakeAuditor{}
nextCalled := false
next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
nextCalled = true
})
handler := QueryKeyRejectMiddleware(next, auditor, fixedNow)
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts?api_key=secret", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if nextCalled {
t.Fatalf("next handler should not be called when query key exists")
}
if rec.Code != http.StatusUnauthorized {
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusUnauthorized)
}
if got := decodeErrorCode(t, rec); got != service.CodeQueryKeyNotAllowed {
t.Fatalf("unexpected error code: got=%s want=%s", got, service.CodeQueryKeyNotAllowed)
}
if len(auditor.events) != 1 {
t.Fatalf("unexpected audit event count: got=%d want=1", len(auditor.events))
}
if auditor.events[0].EventName != service.EventTokenQueryKeyRejected {
t.Fatalf("unexpected event name: got=%s want=%s", auditor.events[0].EventName, service.EventTokenQueryKeyRejected)
}
}
func TestTokenAuthMiddleware(t *testing.T) {
baseToken := service.VerifiedToken{
TokenID: "tok-001",
SubjectID: "subject-001",
Role: model.RoleOwner,
Scope: []string{"supply:*"},
IssuedAt: fixedNow(),
ExpiresAt: fixedNow().Add(time.Hour),
}
cases := []struct {
name string
path string
authHeader string
verifierErr error
status service.TokenStatus
statusErr error
allowed bool
wantStatus int
wantErrorCode string
wantEvent string
wantNext bool
}{
{
name: "missing bearer",
path: "/api/v1/supply/packages",
wantStatus: http.StatusUnauthorized,
wantErrorCode: service.CodeAuthMissingBearer,
wantEvent: service.EventTokenAuthnFail,
},
{
name: "invalid token",
path: "/api/v1/supply/packages",
authHeader: "Bearer invalid-token",
verifierErr: errors.New("invalid signature"),
wantStatus: http.StatusUnauthorized,
wantErrorCode: service.CodeAuthInvalidToken,
wantEvent: service.EventTokenAuthnFail,
},
{
name: "inactive token",
path: "/api/v1/supply/packages",
authHeader: "Bearer active-token",
status: service.TokenStatusRevoked,
wantStatus: http.StatusUnauthorized,
wantErrorCode: service.CodeAuthTokenInactive,
wantEvent: service.EventTokenAuthnFail,
},
{
name: "scope denied",
path: "/api/v1/supply/packages",
authHeader: "Bearer active-token",
status: service.TokenStatusActive,
allowed: false,
wantStatus: http.StatusForbidden,
wantErrorCode: service.CodeAuthScopeDenied,
wantEvent: service.EventTokenAuthzDenied,
},
{
name: "authn success",
path: "/api/v1/supply/packages",
authHeader: "Bearer active-token",
status: service.TokenStatusActive,
allowed: true,
wantStatus: http.StatusNoContent,
wantEvent: service.EventTokenAuthnSuccess,
wantNext: true,
},
{
name: "excluded path bypasses auth",
path: "/healthz",
wantStatus: http.StatusNoContent,
wantNext: true,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
auditor := &fakeAuditor{}
verifier := &fakeVerifier{
token: baseToken,
err: tc.verifierErr,
}
resolver := &fakeStatusResolver{
status: tc.status,
err: tc.statusErr,
}
authorizer := &fakeAuthorizer{allowed: tc.allowed}
nextCalled := false
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextCalled = true
if tc.wantNext && strings.HasPrefix(tc.path, "/api/v1/") {
principal, ok := PrincipalFromContext(r.Context())
if !ok {
t.Fatalf("principal should be attached when auth succeeded")
}
if principal.TokenID != baseToken.TokenID {
t.Fatalf("unexpected principal token id: got=%s want=%s", principal.TokenID, baseToken.TokenID)
}
}
w.WriteHeader(http.StatusNoContent)
})
handler := TokenAuthMiddleware(AuthMiddlewareConfig{
Verifier: verifier,
StatusResolver: resolver,
Authorizer: authorizer,
Auditor: auditor,
ProtectedPrefixes: []string{"/api/v1/supply/", "/api/v1/platform/"},
ExcludedPrefixes: []string{"/healthz"},
Now: fixedNow,
})(next)
req := httptest.NewRequest(http.MethodGet, tc.path, nil)
if tc.authHeader != "" {
req.Header.Set("Authorization", tc.authHeader)
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tc.wantStatus {
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, tc.wantStatus)
}
if tc.wantErrorCode != "" {
if got := decodeErrorCode(t, rec); got != tc.wantErrorCode {
t.Fatalf("unexpected error code: got=%s want=%s", got, tc.wantErrorCode)
}
}
if nextCalled != tc.wantNext {
t.Fatalf("unexpected next call state: got=%v want=%v", nextCalled, tc.wantNext)
}
if tc.wantEvent == "" {
return
}
if len(auditor.events) == 0 {
t.Fatalf("audit event should be emitted")
}
lastEvent := auditor.events[len(auditor.events)-1]
if lastEvent.EventName != tc.wantEvent {
t.Fatalf("unexpected event name: got=%s want=%s", lastEvent.EventName, tc.wantEvent)
}
})
}
}
type errorEnvelope struct {
Error struct {
Code string `json:"code"`
} `json:"error"`
}
func decodeErrorCode(t *testing.T, rec *httptest.ResponseRecorder) string {
t.Helper()
var envelope errorEnvelope
if err := json.Unmarshal(rec.Body.Bytes(), &envelope); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
return envelope.Error.Code
}

View File

@@ -0,0 +1,35 @@
package model
import "strings"
const (
RoleOwner = "owner"
RoleViewer = "viewer"
RoleAdmin = "admin"
)
type Principal struct {
RequestID string
TokenID string
SubjectID string
Role string
Scope []string
}
func (p Principal) HasScope(required string) bool {
if required == "" {
return true
}
for _, scope := range p.Scope {
if scope == required {
return true
}
if strings.HasSuffix(scope, ":*") {
prefix := strings.TrimSuffix(scope, "*")
if strings.HasPrefix(required, prefix) {
return true
}
}
}
return false
}

View File

@@ -0,0 +1,491 @@
package service
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"net/http"
"sort"
"strings"
"sync"
"time"
"lijiaoqiao/platform-token-runtime/internal/auth/model"
)
type TokenRecord struct {
TokenID string
AccessToken string
SubjectID string
Role string
Scope []string
IssuedAt time.Time
ExpiresAt time.Time
Status TokenStatus
RequestID string
RevokedReason string
}
type IssueTokenInput struct {
SubjectID string
Role string
Scope []string
TTL time.Duration
RequestID string
IdempotencyKey string
}
type InMemoryTokenRuntime struct {
mu sync.RWMutex
now func() time.Time
records map[string]*TokenRecord
tokenToID map[string]string
idempotencyByKey map[string]idempotencyEntry
}
type idempotencyEntry struct {
RequestHash string
TokenID string
}
func NewInMemoryTokenRuntime(now func() time.Time) *InMemoryTokenRuntime {
if now == nil {
now = time.Now
}
return &InMemoryTokenRuntime{
now: now,
records: make(map[string]*TokenRecord),
tokenToID: make(map[string]string),
idempotencyByKey: make(map[string]idempotencyEntry),
}
}
func (r *InMemoryTokenRuntime) Issue(_ context.Context, input IssueTokenInput) (TokenRecord, error) {
if strings.TrimSpace(input.SubjectID) == "" {
return TokenRecord{}, errors.New("subject_id is required")
}
if strings.TrimSpace(input.Role) == "" {
return TokenRecord{}, errors.New("role is required")
}
if input.TTL <= 0 {
return TokenRecord{}, errors.New("ttl must be positive")
}
if len(input.Scope) == 0 {
return TokenRecord{}, errors.New("scope must not be empty")
}
idempotencyKey := strings.TrimSpace(input.IdempotencyKey)
requestHash := hashIssueInput(input)
issuedAt := r.now()
tokenID, err := generateTokenID()
if err != nil {
return TokenRecord{}, err
}
accessToken, err := generateAccessToken()
if err != nil {
return TokenRecord{}, err
}
record := TokenRecord{
TokenID: tokenID,
AccessToken: accessToken,
SubjectID: input.SubjectID,
Role: input.Role,
Scope: append([]string(nil), input.Scope...),
IssuedAt: issuedAt,
ExpiresAt: issuedAt.Add(input.TTL),
Status: TokenStatusActive,
RequestID: input.RequestID,
RevokedReason: "",
}
r.mu.Lock()
if idempotencyKey != "" {
entry, ok := r.idempotencyByKey[idempotencyKey]
if ok {
if entry.RequestHash != requestHash {
r.mu.Unlock()
return TokenRecord{}, errors.New("idempotency key payload mismatch")
}
existing, exists := r.records[entry.TokenID]
if exists {
r.mu.Unlock()
return cloneRecord(*existing), nil
}
}
}
r.records[tokenID] = &record
r.tokenToID[accessToken] = tokenID
if idempotencyKey != "" {
r.idempotencyByKey[idempotencyKey] = idempotencyEntry{
RequestHash: requestHash,
TokenID: tokenID,
}
}
r.mu.Unlock()
return record, nil
}
func (r *InMemoryTokenRuntime) Refresh(_ context.Context, tokenID string, ttl time.Duration) (TokenRecord, error) {
if ttl <= 0 {
return TokenRecord{}, errors.New("ttl must be positive")
}
r.mu.Lock()
defer r.mu.Unlock()
record, ok := r.records[tokenID]
if !ok {
return TokenRecord{}, errors.New("token not found")
}
r.applyExpiry(record)
if record.Status != TokenStatusActive {
return TokenRecord{}, errors.New("token is not active")
}
record.ExpiresAt = r.now().Add(ttl)
return cloneRecord(*record), nil
}
func (r *InMemoryTokenRuntime) Revoke(_ context.Context, tokenID, reason string) (TokenRecord, error) {
r.mu.Lock()
defer r.mu.Unlock()
record, ok := r.records[tokenID]
if !ok {
return TokenRecord{}, errors.New("token not found")
}
r.applyExpiry(record)
record.Status = TokenStatusRevoked
record.RevokedReason = strings.TrimSpace(reason)
return cloneRecord(*record), nil
}
func (r *InMemoryTokenRuntime) Introspect(_ context.Context, accessToken string) (TokenRecord, error) {
r.mu.Lock()
defer r.mu.Unlock()
tokenID, ok := r.tokenToID[accessToken]
if !ok {
return TokenRecord{}, errors.New("token not found")
}
record := r.records[tokenID]
r.applyExpiry(record)
return cloneRecord(*record), nil
}
func (r *InMemoryTokenRuntime) Lookup(_ context.Context, tokenID string) (TokenRecord, error) {
r.mu.Lock()
defer r.mu.Unlock()
record, ok := r.records[tokenID]
if !ok {
return TokenRecord{}, errors.New("token not found")
}
r.applyExpiry(record)
return cloneRecord(*record), nil
}
func (r *InMemoryTokenRuntime) Verify(_ context.Context, rawToken string) (VerifiedToken, error) {
r.mu.RLock()
tokenID, ok := r.tokenToID[rawToken]
if !ok {
r.mu.RUnlock()
return VerifiedToken{}, NewAuthError(CodeAuthInvalidToken, errors.New("token not found"))
}
record, ok := r.records[tokenID]
if !ok {
r.mu.RUnlock()
return VerifiedToken{}, NewAuthError(CodeAuthInvalidToken, errors.New("token record not found"))
}
claims := VerifiedToken{
TokenID: record.TokenID,
SubjectID: record.SubjectID,
Role: record.Role,
Scope: append([]string(nil), record.Scope...),
IssuedAt: record.IssuedAt,
ExpiresAt: record.ExpiresAt,
}
r.mu.RUnlock()
return claims, nil
}
func (r *InMemoryTokenRuntime) Resolve(_ context.Context, tokenID string) (TokenStatus, error) {
r.mu.Lock()
defer r.mu.Unlock()
record, ok := r.records[tokenID]
if !ok {
return "", NewAuthError(CodeAuthInvalidToken, errors.New("token not found"))
}
r.applyExpiry(record)
return record.Status, nil
}
func (r *InMemoryTokenRuntime) TokenCount() int {
r.mu.RLock()
defer r.mu.RUnlock()
return len(r.records)
}
func (r *InMemoryTokenRuntime) IssueAndAudit(ctx context.Context, input IssueTokenInput, auditor AuditEmitter) (TokenRecord, error) {
record, err := r.Issue(ctx, input)
if err != nil {
emitAudit(auditor, AuditEvent{
EventName: EventTokenIssueFail,
RequestID: input.RequestID,
SubjectID: input.SubjectID,
Route: "/api/v1/platform/tokens/issue",
ResultCode: "ISSUE_FAILED",
}, r.now)
return TokenRecord{}, err
}
emitAudit(auditor, AuditEvent{
EventName: EventTokenIssueSuccess,
RequestID: input.RequestID,
TokenID: record.TokenID,
SubjectID: record.SubjectID,
Route: "/api/v1/platform/tokens/issue",
ResultCode: "OK",
}, r.now)
return record, nil
}
func (r *InMemoryTokenRuntime) RevokeAndAudit(ctx context.Context, tokenID, reason, requestID, subjectID string, auditor AuditEmitter) (TokenRecord, error) {
record, err := r.Revoke(ctx, tokenID, reason)
if err != nil {
emitAudit(auditor, AuditEvent{
EventName: EventTokenRevokeFail,
RequestID: requestID,
TokenID: tokenID,
SubjectID: subjectID,
Route: "/api/v1/platform/tokens/revoke",
ResultCode: "REVOKE_FAILED",
}, r.now)
return TokenRecord{}, err
}
emitAudit(auditor, AuditEvent{
EventName: EventTokenRevokeSuccess,
RequestID: requestID,
TokenID: record.TokenID,
SubjectID: record.SubjectID,
Route: "/api/v1/platform/tokens/revoke",
ResultCode: "OK",
}, r.now)
return record, nil
}
func (r *InMemoryTokenRuntime) applyExpiry(record *TokenRecord) {
if record == nil {
return
}
if record.Status == TokenStatusActive && !record.ExpiresAt.IsZero() && !r.now().Before(record.ExpiresAt) {
record.Status = TokenStatusExpired
}
}
func cloneRecord(record TokenRecord) TokenRecord {
record.Scope = append([]string(nil), record.Scope...)
return record
}
func hashIssueInput(input IssueTokenInput) string {
scope := append([]string(nil), input.Scope...)
sort.Strings(scope)
joined := strings.Join(scope, ",")
data := strings.TrimSpace(input.SubjectID) + "|" +
strings.TrimSpace(input.Role) + "|" +
joined + "|" +
input.TTL.String()
sum := sha256.Sum256([]byte(data))
return hex.EncodeToString(sum[:])
}
func generateAccessToken() (string, error) {
var entropy [16]byte
if _, err := rand.Read(entropy[:]); err != nil {
return "", err
}
return "ptk_" + hex.EncodeToString(entropy[:]), nil
}
func generateTokenID() (string, error) {
var entropy [8]byte
if _, err := rand.Read(entropy[:]); err != nil {
return "", err
}
return "tok_" + hex.EncodeToString(entropy[:]), nil
}
type ScopeRoleAuthorizer struct{}
func NewScopeRoleAuthorizer() *ScopeRoleAuthorizer {
return &ScopeRoleAuthorizer{}
}
func (a *ScopeRoleAuthorizer) Authorize(path, method string, scopes []string, role string) bool {
if role == model.RoleAdmin {
return true
}
requiredScope := requiredScopeForRoute(path, method)
if requiredScope == "" {
return true
}
return hasScope(scopes, requiredScope)
}
func requiredScopeForRoute(path, method string) string {
if path == "/api/v1/supply" || strings.HasPrefix(path, "/api/v1/supply/") {
switch method {
case http.MethodGet, http.MethodHead, http.MethodOptions:
return "supply:read"
default:
return "supply:write"
}
}
if path == "/api/v1/platform" || strings.HasPrefix(path, "/api/v1/platform/") {
return "platform:admin"
}
return ""
}
func hasScope(scopes []string, required string) bool {
for _, scope := range scopes {
if scope == required {
return true
}
if strings.HasSuffix(scope, ":*") {
prefix := strings.TrimSuffix(scope, "*")
if strings.HasPrefix(required, prefix) {
return true
}
}
}
return false
}
type MemoryAuditEmitter struct {
mu sync.RWMutex
events []AuditEvent
now func() time.Time
}
func NewMemoryAuditEmitter() *MemoryAuditEmitter {
return &MemoryAuditEmitter{now: time.Now}
}
func (e *MemoryAuditEmitter) Emit(_ context.Context, event AuditEvent) error {
if event.EventID == "" {
eventID, err := generateEventID()
if err != nil {
return err
}
event.EventID = eventID
}
if event.CreatedAt.IsZero() {
event.CreatedAt = e.now()
}
e.mu.Lock()
e.events = append(e.events, event)
e.mu.Unlock()
return nil
}
func (e *MemoryAuditEmitter) Events() []AuditEvent {
e.mu.RLock()
defer e.mu.RUnlock()
copied := make([]AuditEvent, len(e.events))
copy(copied, e.events)
return copied
}
func (e *MemoryAuditEmitter) QueryEvents(_ context.Context, filter AuditEventFilter) ([]AuditEvent, error) {
e.mu.RLock()
defer e.mu.RUnlock()
limit := filter.Limit
if limit <= 0 {
limit = 100
}
if limit > 500 {
limit = 500
}
result := make([]AuditEvent, 0, minInt(limit, len(e.events)))
for idx := len(e.events) - 1; idx >= 0; idx-- {
ev := e.events[idx]
if !matchAuditFilter(ev, filter) {
continue
}
result = append(result, ev)
if len(result) >= limit {
break
}
}
// 按时间正序返回,便于前端/审计系统展示时间线。
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
return result, nil
}
func (e *MemoryAuditEmitter) LastEvent() (AuditEvent, bool) {
e.mu.RLock()
defer e.mu.RUnlock()
if len(e.events) == 0 {
return AuditEvent{}, false
}
return e.events[len(e.events)-1], true
}
func emitAudit(emitter AuditEmitter, event AuditEvent, now func() time.Time) {
if emitter == nil {
return
}
if now == nil {
now = time.Now
}
if event.CreatedAt.IsZero() {
event.CreatedAt = now()
}
_ = emitter.Emit(context.Background(), event)
}
func matchAuditFilter(ev AuditEvent, filter AuditEventFilter) bool {
if filter.RequestID != "" && ev.RequestID != filter.RequestID {
return false
}
if filter.TokenID != "" && ev.TokenID != filter.TokenID {
return false
}
if filter.SubjectID != "" && ev.SubjectID != filter.SubjectID {
return false
}
if filter.EventName != "" && ev.EventName != filter.EventName {
return false
}
if filter.ResultCode != "" && ev.ResultCode != filter.ResultCode {
return false
}
return true
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func generateEventID() (string, error) {
var entropy [8]byte
if _, err := rand.Read(entropy[:]); err != nil {
return "", err
}
return "evt_" + hex.EncodeToString(entropy[:]), nil
}

View File

@@ -0,0 +1,127 @@
package service
import (
"context"
"errors"
"fmt"
"time"
)
const (
CodeAuthMissingBearer = "AUTH_MISSING_BEARER"
CodeQueryKeyNotAllowed = "QUERY_KEY_NOT_ALLOWED"
CodeAuthInvalidToken = "AUTH_INVALID_TOKEN"
CodeAuthTokenInactive = "AUTH_TOKEN_INACTIVE"
CodeAuthScopeDenied = "AUTH_SCOPE_DENIED"
CodeAuthNotReady = "AUTH_NOT_READY"
)
const (
EventTokenAuthnSuccess = "token.authn.success"
EventTokenAuthnFail = "token.authn.fail"
EventTokenAuthzDenied = "token.authz.denied"
EventTokenQueryKeyRejected = "token.query_key.rejected"
EventTokenIssueSuccess = "token.issue.success"
EventTokenIssueFail = "token.issue.fail"
EventTokenIntrospectSuccess = "token.introspect.success"
EventTokenIntrospectFail = "token.introspect.fail"
EventTokenRefreshSuccess = "token.refresh.success"
EventTokenRefreshFail = "token.refresh.fail"
EventTokenRevokeSuccess = "token.revoke.success"
EventTokenRevokeFail = "token.revoke.fail"
)
type TokenStatus string
const (
TokenStatusActive TokenStatus = "active"
TokenStatusRevoked TokenStatus = "revoked"
TokenStatusExpired TokenStatus = "expired"
)
type VerifiedToken struct {
TokenID string
SubjectID string
Role string
Scope []string
IssuedAt time.Time
ExpiresAt time.Time
NotBefore time.Time
Issuer string
Audience string
}
type TokenVerifier interface {
Verify(ctx context.Context, rawToken string) (VerifiedToken, error)
}
type TokenStatusResolver interface {
Resolve(ctx context.Context, tokenID string) (TokenStatus, error)
}
type RouteAuthorizer interface {
Authorize(path, method string, scopes []string, role string) bool
}
type AuditEvent struct {
EventID string
EventName string
RequestID string
TokenID string
SubjectID string
Route string
ResultCode string
ClientIP string
CreatedAt time.Time
}
type AuditEmitter interface {
Emit(ctx context.Context, event AuditEvent) error
}
type AuditEventFilter struct {
RequestID string
TokenID string
SubjectID string
EventName string
ResultCode string
Limit int
}
type AuditEventQuerier interface {
QueryEvents(ctx context.Context, filter AuditEventFilter) ([]AuditEvent, error)
}
type AuthError struct {
Code string
Cause error
}
func (e *AuthError) Error() string {
if e == nil {
return ""
}
if e.Cause == nil {
return e.Code
}
return fmt.Sprintf("%s: %v", e.Code, e.Cause)
}
func (e *AuthError) Unwrap() error {
if e == nil {
return nil
}
return e.Cause
}
func NewAuthError(code string, cause error) *AuthError {
return &AuthError{Code: code, Cause: cause}
}
func IsAuthCode(err error, code string) bool {
var authErr *AuthError
if !errors.As(err, &authErr) {
return false
}
return authErr.Code == code
}

View File

@@ -0,0 +1,437 @@
package httpapi
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"lijiaoqiao/platform-token-runtime/internal/auth/model"
"lijiaoqiao/platform-token-runtime/internal/auth/service"
)
const (
tokenBasePath = "/api/v1/platform/tokens"
)
type Runtime interface {
IssueAndAudit(ctx context.Context, input service.IssueTokenInput, auditor service.AuditEmitter) (service.TokenRecord, error)
Refresh(ctx context.Context, tokenID string, ttl time.Duration) (service.TokenRecord, error)
RevokeAndAudit(ctx context.Context, tokenID, reason, requestID, subjectID string, auditor service.AuditEmitter) (service.TokenRecord, error)
Introspect(ctx context.Context, accessToken string) (service.TokenRecord, error)
Lookup(ctx context.Context, tokenID string) (service.TokenRecord, error)
}
type TokenAPI struct {
runtime Runtime
auditor service.AuditEmitter
now func() time.Time
}
func NewTokenAPI(runtime Runtime, auditor service.AuditEmitter, now func() time.Time) *TokenAPI {
if now == nil {
now = time.Now
}
return &TokenAPI{runtime: runtime, auditor: auditor, now: now}
}
func (a *TokenAPI) Register(mux *http.ServeMux) {
mux.HandleFunc(tokenBasePath+"/issue", a.handleIssue)
mux.HandleFunc(tokenBasePath+"/introspect", a.handleIntrospect)
mux.HandleFunc(tokenBasePath+"/audit-events", a.handleAuditEvents)
mux.HandleFunc(tokenBasePath+"/", a.handleTokenAction)
}
func (a *TokenAPI) handleTokenAction(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, tokenBasePath+"/") {
writeError(w, http.StatusNotFound, "NOT_FOUND", "route not found")
return
}
tail := strings.TrimPrefix(r.URL.Path, tokenBasePath+"/")
parts := strings.Split(tail, "/")
if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" {
writeError(w, http.StatusNotFound, "NOT_FOUND", "route not found")
return
}
tokenID := strings.TrimSpace(parts[0])
action := strings.TrimSpace(parts[1])
switch action {
case "refresh":
a.handleRefresh(w, r, tokenID)
case "revoke":
a.handleRevoke(w, r, tokenID)
default:
writeError(w, http.StatusNotFound, "NOT_FOUND", "route not found")
}
}
type issueRequest struct {
SubjectID string `json:"subject_id"`
Role string `json:"role"`
TTLSeconds int64 `json:"ttl_seconds"`
Scope []string `json:"scope"`
}
type refreshRequest struct {
TTLSeconds int64 `json:"ttl_seconds"`
}
type revokeRequest struct {
Reason string `json:"reason"`
}
type introspectRequest struct {
Token string `json:"token"`
}
type errorEnvelope struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func (a *TokenAPI) handleIssue(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
requestID := strings.TrimSpace(r.Header.Get("X-Request-Id"))
idempotencyKey := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
if requestID == "" || idempotencyKey == "" {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "missing X-Request-Id or Idempotency-Key")
return
}
var req issueRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
if err := validateIssueRequest(req); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
record, err := a.runtime.IssueAndAudit(r.Context(), service.IssueTokenInput{
SubjectID: req.SubjectID,
Role: req.Role,
Scope: req.Scope,
TTL: time.Duration(req.TTLSeconds) * time.Second,
RequestID: requestID,
IdempotencyKey: idempotencyKey,
}, a.auditor)
if err != nil {
if strings.Contains(err.Error(), "idempotency key payload mismatch") {
writeError(w, http.StatusConflict, "IDEMPOTENCY_CONFLICT", "idempotency key payload mismatch")
return
}
writeError(w, http.StatusUnprocessableEntity, "ISSUE_FAILED", err.Error())
return
}
writeJSON(w, http.StatusCreated, map[string]any{
"request_id": requestID,
"data": map[string]any{
"token_id": record.TokenID,
"access_token": record.AccessToken,
"issued_at": record.IssuedAt,
"expires_at": record.ExpiresAt,
"status": record.Status,
},
})
}
func (a *TokenAPI) handleRefresh(w http.ResponseWriter, r *http.Request, tokenID string) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
requestID := strings.TrimSpace(r.Header.Get("X-Request-Id"))
idempotencyKey := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
if requestID == "" || idempotencyKey == "" {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "missing X-Request-Id or Idempotency-Key")
return
}
var req refreshRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
if req.TTLSeconds < 60 {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "ttl_seconds must be >= 60")
return
}
before, err := a.runtime.Lookup(r.Context(), tokenID)
if err != nil {
before = service.TokenRecord{}
}
record, err := a.runtime.Refresh(r.Context(), tokenID, time.Duration(req.TTLSeconds)*time.Second)
if err != nil {
status, code := mapRuntimeError(err)
writeError(w, status, code, err.Error())
return
}
if a.auditor != nil {
_ = a.auditor.Emit(r.Context(), service.AuditEvent{
EventName: service.EventTokenRefreshSuccess,
RequestID: requestID,
TokenID: record.TokenID,
SubjectID: record.SubjectID,
Route: tokenBasePath + "/" + tokenID + "/refresh",
ResultCode: "OK",
CreatedAt: a.now(),
})
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": requestID,
"data": map[string]any{
"token_id": record.TokenID,
"previous_expires_at": before.ExpiresAt,
"expires_at": record.ExpiresAt,
"status": record.Status,
},
})
}
func (a *TokenAPI) handleRevoke(w http.ResponseWriter, r *http.Request, tokenID string) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
requestID := strings.TrimSpace(r.Header.Get("X-Request-Id"))
idempotencyKey := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
if requestID == "" || idempotencyKey == "" {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "missing X-Request-Id or Idempotency-Key")
return
}
var req revokeRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
if strings.TrimSpace(req.Reason) == "" {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "reason is required")
return
}
introspected, err := a.runtime.Lookup(r.Context(), tokenID)
subjectID := ""
if err == nil {
subjectID = introspected.SubjectID
}
record, err := a.runtime.RevokeAndAudit(r.Context(), tokenID, req.Reason, requestID, subjectID, a.auditor)
if err != nil {
status, code := mapRuntimeError(err)
writeError(w, status, code, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": requestID,
"data": map[string]any{
"token_id": record.TokenID,
"status": record.Status,
"revoked_at": a.now(),
},
})
}
func (a *TokenAPI) handleIntrospect(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
requestID := strings.TrimSpace(r.Header.Get("X-Request-Id"))
if requestID == "" {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "missing X-Request-Id")
return
}
var req introspectRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
if strings.TrimSpace(req.Token) == "" {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "token is required")
return
}
record, err := a.runtime.Introspect(r.Context(), req.Token)
if err != nil {
if a.auditor != nil {
_ = a.auditor.Emit(r.Context(), service.AuditEvent{
EventName: service.EventTokenIntrospectFail,
RequestID: requestID,
Route: tokenBasePath + "/introspect",
ResultCode: "INVALID_TOKEN",
CreatedAt: a.now(),
})
}
writeError(w, http.StatusUnprocessableEntity, "TOKEN_INVALID", err.Error())
return
}
if a.auditor != nil {
_ = a.auditor.Emit(r.Context(), service.AuditEvent{
EventName: service.EventTokenIntrospectSuccess,
RequestID: requestID,
TokenID: record.TokenID,
SubjectID: record.SubjectID,
Route: tokenBasePath + "/introspect",
ResultCode: "OK",
CreatedAt: a.now(),
})
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": requestID,
"data": map[string]any{
"token_id": record.TokenID,
"subject_id": record.SubjectID,
"role": record.Role,
"status": record.Status,
"scope": record.Scope,
"issued_at": record.IssuedAt,
"expires_at": record.ExpiresAt,
},
})
}
func (a *TokenAPI) handleAuditEvents(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
requestID := strings.TrimSpace(r.Header.Get("X-Request-Id"))
if requestID == "" {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "missing X-Request-Id")
return
}
querier, ok := a.auditor.(service.AuditEventQuerier)
if !ok {
writeError(w, http.StatusNotImplemented, "AUDIT_QUERY_NOT_READY", "audit query capability is not available")
return
}
limit := parseLimit(r.URL.Query().Get("limit"))
filter := service.AuditEventFilter{
RequestID: strings.TrimSpace(r.URL.Query().Get("request_id")),
TokenID: strings.TrimSpace(r.URL.Query().Get("token_id")),
SubjectID: strings.TrimSpace(r.URL.Query().Get("subject_id")),
EventName: strings.TrimSpace(r.URL.Query().Get("event_name")),
ResultCode: strings.TrimSpace(r.URL.Query().Get("result_code")),
Limit: limit,
}
events, err := querier.QueryEvents(r.Context(), filter)
if err != nil {
writeError(w, http.StatusInternalServerError, "AUDIT_QUERY_FAILED", err.Error())
return
}
items := make([]map[string]any, 0, len(events))
for _, ev := range events {
items = append(items, map[string]any{
"event_id": ev.EventID,
"event_name": ev.EventName,
"request_id": ev.RequestID,
"token_id": ev.TokenID,
"subject_id": ev.SubjectID,
"route": ev.Route,
"result_code": ev.ResultCode,
"client_ip": ev.ClientIP,
"created_at": ev.CreatedAt,
})
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": requestID,
"data": map[string]any{
"total": len(items),
"items": items,
},
})
}
func validateIssueRequest(req issueRequest) error {
if strings.TrimSpace(req.SubjectID) == "" {
return errors.New("subject_id is required")
}
if req.TTLSeconds < 60 {
return errors.New("ttl_seconds must be >= 60")
}
if len(req.Scope) == 0 {
return errors.New("scope is required")
}
switch req.Role {
case model.RoleOwner, model.RoleViewer, model.RoleAdmin:
return nil
default:
return fmt.Errorf("unsupported role: %s", req.Role)
}
}
func mapRuntimeError(err error) (int, string) {
msg := err.Error()
switch {
case strings.Contains(msg, "not found"):
return http.StatusNotFound, "TOKEN_NOT_FOUND"
case strings.Contains(msg, "not active"):
return http.StatusConflict, "TOKEN_NOT_ACTIVE"
case strings.Contains(msg, "idempotency key payload mismatch"):
return http.StatusConflict, "IDEMPOTENCY_CONFLICT"
default:
return http.StatusUnprocessableEntity, "BUSINESS_ERROR"
}
}
func decodeJSON(r *http.Request, out any) error {
defer r.Body.Close()
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(out); err != nil {
return err
}
return nil
}
func writeError(w http.ResponseWriter, status int, code, message string) {
var env errorEnvelope
env.Error.Code = code
env.Error.Message = message
writeJSON(w, status, env)
}
func writeJSON(w http.ResponseWriter, status int, payload any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}
func parseLimit(raw string) int {
if strings.TrimSpace(raw) == "" {
return 100
}
n, err := strconv.Atoi(strings.TrimSpace(raw))
if err != nil || n <= 0 {
return 100
}
if n > 500 {
return 500
}
return n
}

View File

@@ -0,0 +1,269 @@
package httpapi
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"lijiaoqiao/platform-token-runtime/internal/auth/service"
)
func TestTokenAPIIssueAndIntrospect(t *testing.T) {
t.Parallel()
runtime := service.NewInMemoryTokenRuntime(nil)
auditor := service.NewMemoryAuditEmitter()
api := NewTokenAPI(runtime, auditor, func() time.Time {
return time.Date(2026, 3, 30, 15, 50, 0, 0, time.UTC)
})
mux := http.NewServeMux()
api.Register(mux)
issueBody := map[string]any{
"subject_id": "2001",
"role": "owner",
"ttl_seconds": 600,
"scope": []string{"supply:*"},
}
issueReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/issue", mustJSON(t, issueBody))
issueReq.Header.Set("X-Request-Id", "req-api-001")
issueReq.Header.Set("Idempotency-Key", "idem-api-001")
issueRec := httptest.NewRecorder()
mux.ServeHTTP(issueRec, issueReq)
if issueRec.Code != http.StatusCreated {
t.Fatalf("unexpected issue status: got=%d want=%d body=%s", issueRec.Code, http.StatusCreated, issueRec.Body.String())
}
issueResp := decodeMap(t, issueRec.Body.Bytes())
data := issueResp["data"].(map[string]any)
accessToken := data["access_token"].(string)
if accessToken == "" {
t.Fatalf("access_token should not be empty")
}
introspectBody := map[string]any{"token": accessToken}
introReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/introspect", mustJSON(t, introspectBody))
introReq.Header.Set("X-Request-Id", "req-api-002")
introRec := httptest.NewRecorder()
mux.ServeHTTP(introRec, introReq)
if introRec.Code != http.StatusOK {
t.Fatalf("unexpected introspect status: got=%d want=%d body=%s", introRec.Code, http.StatusOK, introRec.Body.String())
}
introResp := decodeMap(t, introRec.Body.Bytes())
introData := introResp["data"].(map[string]any)
if introData["role"].(string) != "owner" {
t.Fatalf("unexpected role: got=%s want=owner", introData["role"].(string))
}
}
func TestTokenAPIIssueIdempotencyConflict(t *testing.T) {
t.Parallel()
runtime := service.NewInMemoryTokenRuntime(nil)
api := NewTokenAPI(runtime, service.NewMemoryAuditEmitter(), time.Now)
mux := http.NewServeMux()
api.Register(mux)
firstBody := map[string]any{
"subject_id": "2001",
"role": "owner",
"ttl_seconds": 600,
"scope": []string{"supply:*"},
}
secondBody := map[string]any{
"subject_id": "2001",
"role": "owner",
"ttl_seconds": 600,
"scope": []string{"supply:read"},
}
firstReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/issue", mustJSON(t, firstBody))
firstReq.Header.Set("X-Request-Id", "req-api-003-1")
firstReq.Header.Set("Idempotency-Key", "idem-api-003")
firstRec := httptest.NewRecorder()
mux.ServeHTTP(firstRec, firstReq)
if firstRec.Code != http.StatusCreated {
t.Fatalf("first issue should succeed: code=%d body=%s", firstRec.Code, firstRec.Body.String())
}
secondReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/issue", mustJSON(t, secondBody))
secondReq.Header.Set("X-Request-Id", "req-api-003-2")
secondReq.Header.Set("Idempotency-Key", "idem-api-003")
secondRec := httptest.NewRecorder()
mux.ServeHTTP(secondRec, secondReq)
if secondRec.Code != http.StatusConflict {
t.Fatalf("expected idempotency conflict: code=%d body=%s", secondRec.Code, secondRec.Body.String())
}
}
func TestTokenAPIRefreshAndRevoke(t *testing.T) {
t.Parallel()
now := time.Date(2026, 3, 30, 16, 0, 0, 0, time.UTC)
runtime := service.NewInMemoryTokenRuntime(func() time.Time { return now })
api := NewTokenAPI(runtime, service.NewMemoryAuditEmitter(), func() time.Time { return now })
mux := http.NewServeMux()
api.Register(mux)
issueReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/issue", mustJSON(t, map[string]any{
"subject_id": "2008",
"role": "owner",
"ttl_seconds": 120,
"scope": []string{"supply:*"},
}))
issueReq.Header.Set("X-Request-Id", "req-api-004-1")
issueReq.Header.Set("Idempotency-Key", "idem-api-004")
issueRec := httptest.NewRecorder()
mux.ServeHTTP(issueRec, issueReq)
if issueRec.Code != http.StatusCreated {
t.Fatalf("issue failed: code=%d body=%s", issueRec.Code, issueRec.Body.String())
}
issued := decodeMap(t, issueRec.Body.Bytes())
issuedData := issued["data"].(map[string]any)
tokenID := issuedData["token_id"].(string)
now = now.Add(10 * time.Second)
refreshReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/"+tokenID+"/refresh", mustJSON(t, map[string]any{"ttl_seconds": 300}))
refreshReq.Header.Set("X-Request-Id", "req-api-004-2")
refreshReq.Header.Set("Idempotency-Key", "idem-api-004-r")
refreshRec := httptest.NewRecorder()
mux.ServeHTTP(refreshRec, refreshReq)
if refreshRec.Code != http.StatusOK {
t.Fatalf("refresh failed: code=%d body=%s", refreshRec.Code, refreshRec.Body.String())
}
refreshResp := decodeMap(t, refreshRec.Body.Bytes())
refreshData := refreshResp["data"].(map[string]any)
if refreshData["previous_expires_at"] == nil {
t.Fatalf("previous_expires_at must not be nil")
}
revokeReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/"+tokenID+"/revoke", mustJSON(t, map[string]any{"reason": "operator_request"}))
revokeReq.Header.Set("X-Request-Id", "req-api-004-3")
revokeReq.Header.Set("Idempotency-Key", "idem-api-004-v")
revokeRec := httptest.NewRecorder()
mux.ServeHTTP(revokeRec, revokeReq)
if revokeRec.Code != http.StatusOK {
t.Fatalf("revoke failed: code=%d body=%s", revokeRec.Code, revokeRec.Body.String())
}
revokeResp := decodeMap(t, revokeRec.Body.Bytes())
revokeData := revokeResp["data"].(map[string]any)
if revokeData["status"].(string) != "revoked" {
t.Fatalf("unexpected status after revoke: got=%s", revokeData["status"].(string))
}
}
func TestTokenAPIMissingHeaders(t *testing.T) {
t.Parallel()
runtime := service.NewInMemoryTokenRuntime(nil)
api := NewTokenAPI(runtime, service.NewMemoryAuditEmitter(), time.Now)
mux := http.NewServeMux()
api.Register(mux)
req := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/issue", mustJSON(t, map[string]any{
"subject_id": "2001",
"role": "owner",
"ttl_seconds": 120,
"scope": []string{"supply:*"},
}))
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("missing headers must be rejected: code=%d body=%s", rec.Code, rec.Body.String())
}
}
func TestTokenAPIAuditEventsQuery(t *testing.T) {
t.Parallel()
runtime := service.NewInMemoryTokenRuntime(nil)
auditor := service.NewMemoryAuditEmitter()
api := NewTokenAPI(runtime, auditor, time.Now)
mux := http.NewServeMux()
api.Register(mux)
issueReq := httptest.NewRequest(http.MethodPost, "/api/v1/platform/tokens/issue", mustJSON(t, map[string]any{
"subject_id": "2010",
"role": "owner",
"ttl_seconds": 300,
"scope": []string{"supply:*"},
}))
issueReq.Header.Set("X-Request-Id", "req-audit-query-1")
issueReq.Header.Set("Idempotency-Key", "idem-audit-query-1")
issueRec := httptest.NewRecorder()
mux.ServeHTTP(issueRec, issueReq)
if issueRec.Code != http.StatusCreated {
t.Fatalf("issue failed: code=%d body=%s", issueRec.Code, issueRec.Body.String())
}
issueResp := decodeMap(t, issueRec.Body.Bytes())
tokenID := issueResp["data"].(map[string]any)["token_id"].(string)
queryReq := httptest.NewRequest(http.MethodGet, "/api/v1/platform/tokens/audit-events?token_id="+tokenID+"&limit=5", nil)
queryReq.Header.Set("X-Request-Id", "req-audit-query-2")
queryRec := httptest.NewRecorder()
mux.ServeHTTP(queryRec, queryReq)
if queryRec.Code != http.StatusOK {
t.Fatalf("audit query failed: code=%d body=%s", queryRec.Code, queryRec.Body.String())
}
resp := decodeMap(t, queryRec.Body.Bytes())
data := resp["data"].(map[string]any)
items := data["items"].([]any)
if len(items) == 0 {
t.Fatalf("audit query should return at least one event")
}
first := items[0].(map[string]any)
if first["token_id"].(string) != tokenID {
t.Fatalf("unexpected token_id in first item: got=%s want=%s", first["token_id"].(string), tokenID)
}
if strings.Contains(queryRec.Body.String(), "access_token") {
t.Fatalf("audit query response must not contain access_token")
}
}
func TestTokenAPIAuditEventsNotReady(t *testing.T) {
t.Parallel()
runtime := service.NewInMemoryTokenRuntime(nil)
api := NewTokenAPI(runtime, noopAuditEmitter{}, time.Now)
mux := http.NewServeMux()
api.Register(mux)
req := httptest.NewRequest(http.MethodGet, "/api/v1/platform/tokens/audit-events?limit=3", nil)
req.Header.Set("X-Request-Id", "req-audit-query-3")
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusNotImplemented {
t.Fatalf("expected not implemented: code=%d body=%s", rec.Code, rec.Body.String())
}
}
func mustJSON(t *testing.T, payload any) *bytes.Reader {
t.Helper()
buf, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal json failed: %v", err)
}
return bytes.NewReader(buf)
}
func decodeMap(t *testing.T, raw []byte) map[string]any {
t.Helper()
out := map[string]any{}
if err := json.Unmarshal(raw, &out); err != nil {
t.Fatalf("decode json failed: %v, raw=%s", err, string(raw))
}
return out
}
type noopAuditEmitter struct{}
func (noopAuditEmitter) Emit(context.Context, service.AuditEvent) error {
return nil
}

View File

@@ -0,0 +1,295 @@
package token_test
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"lijiaoqiao/platform-token-runtime/internal/auth/middleware"
"lijiaoqiao/platform-token-runtime/internal/auth/model"
"lijiaoqiao/platform-token-runtime/internal/auth/service"
)
func TestTOKAud001IssueSuccessEvent(t *testing.T) {
t.Parallel()
auditor := service.NewMemoryAuditEmitter()
rt := service.NewInMemoryTokenRuntime(nil)
record, err := rt.IssueAndAudit(context.Background(), service.IssueTokenInput{
SubjectID: "2001",
Role: model.RoleOwner,
Scope: []string{"supply:*"},
TTL: 10 * time.Minute,
RequestID: "req-aud-001",
}, auditor)
if err != nil {
t.Fatalf("issue with audit failed: %v", err)
}
event, ok := auditor.LastEvent()
if !ok {
t.Fatalf("expected issue success event")
}
if event.EventName != service.EventTokenIssueSuccess {
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenIssueSuccess)
}
assertAuditRequiredFields(t, event)
if event.TokenID != record.TokenID {
t.Fatalf("unexpected token_id in event: got=%s want=%s", event.TokenID, record.TokenID)
}
}
func TestTOKAud002IssueFailEvent(t *testing.T) {
t.Parallel()
auditor := service.NewMemoryAuditEmitter()
rt := service.NewInMemoryTokenRuntime(nil)
_, err := rt.IssueAndAudit(context.Background(), service.IssueTokenInput{
SubjectID: "2001",
Role: model.RoleOwner,
Scope: []string{"supply:*"},
TTL: 0,
RequestID: "req-aud-002",
}, auditor)
if err == nil {
t.Fatalf("expected issue failure")
}
event, ok := auditor.LastEvent()
if !ok {
t.Fatalf("expected issue fail event")
}
if event.EventName != service.EventTokenIssueFail {
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenIssueFail)
}
assertAuditRequiredFields(t, event)
if event.ResultCode != "ISSUE_FAILED" {
t.Fatalf("unexpected result_code: got=%s want=ISSUE_FAILED", event.ResultCode)
}
}
func TestTOKAud003AuthnFailEvent(t *testing.T) {
t.Parallel()
auditor := service.NewMemoryAuditEmitter()
rt := service.NewInMemoryTokenRuntime(nil)
authorizer := service.NewScopeRoleAuthorizer()
handler := middleware.BuildTokenAuthChain(middleware.AuthMiddlewareConfig{
Verifier: rt,
StatusResolver: rt,
Authorizer: authorizer,
Auditor: auditor,
}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts", nil)
req.Header.Set("Authorization", "Bearer invalid-token")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusUnauthorized)
}
event, ok := auditor.LastEvent()
if !ok {
t.Fatalf("expected audit event for authn failure")
}
if event.EventName != service.EventTokenAuthnFail {
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenAuthnFail)
}
if event.RequestID == "" {
t.Fatalf("request_id must not be empty")
}
}
func TestTOKAud004AuthzDeniedEvent(t *testing.T) {
t.Parallel()
auditor := service.NewMemoryAuditEmitter()
rt := service.NewInMemoryTokenRuntime(nil)
authorizer := service.NewScopeRoleAuthorizer()
ctx := context.Background()
viewer, err := rt.Issue(ctx, service.IssueTokenInput{
SubjectID: "2002",
Role: model.RoleViewer,
Scope: []string{"supply:read"},
TTL: 5 * time.Minute,
})
if err != nil {
t.Fatalf("issue viewer token failed: %v", err)
}
handler := middleware.BuildTokenAuthChain(middleware.AuthMiddlewareConfig{
Verifier: rt,
StatusResolver: rt,
Authorizer: authorizer,
Auditor: auditor,
}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodPost, "/api/v1/supply/packages", nil)
req.Header.Set("Authorization", "Bearer "+viewer.AccessToken)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusForbidden)
}
event, ok := auditor.LastEvent()
if !ok {
t.Fatalf("expected audit event for authz denial")
}
if event.EventName != service.EventTokenAuthzDenied {
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenAuthzDenied)
}
if event.SubjectID != viewer.SubjectID {
t.Fatalf("unexpected subject_id: got=%s want=%s", event.SubjectID, viewer.SubjectID)
}
}
func TestTOKAud005RevokeSuccessEvent(t *testing.T) {
t.Parallel()
auditor := service.NewMemoryAuditEmitter()
rt := service.NewInMemoryTokenRuntime(nil)
record, err := rt.Issue(context.Background(), service.IssueTokenInput{
SubjectID: "2001",
Role: model.RoleOwner,
Scope: []string{"supply:*"},
TTL: 8 * time.Minute,
})
if err != nil {
t.Fatalf("issue token failed: %v", err)
}
_, err = rt.RevokeAndAudit(context.Background(), record.TokenID, "operator_request", "req-aud-005", record.SubjectID, auditor)
if err != nil {
t.Fatalf("revoke with audit failed: %v", err)
}
event, ok := auditor.LastEvent()
if !ok {
t.Fatalf("expected revoke success event")
}
if event.EventName != service.EventTokenRevokeSuccess {
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenRevokeSuccess)
}
assertAuditRequiredFields(t, event)
if event.TokenID != record.TokenID {
t.Fatalf("unexpected token_id in event: got=%s want=%s", event.TokenID, record.TokenID)
}
}
func TestTOKAud006QueryKeyRejectedEvent(t *testing.T) {
t.Parallel()
auditor := service.NewMemoryAuditEmitter()
rt := service.NewInMemoryTokenRuntime(nil)
authorizer := service.NewScopeRoleAuthorizer()
handler := middleware.BuildTokenAuthChain(middleware.AuthMiddlewareConfig{
Verifier: rt,
StatusResolver: rt,
Authorizer: authorizer,
Auditor: auditor,
}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts?api_key=raw-secret-value", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusUnauthorized)
}
event, ok := auditor.LastEvent()
if !ok {
t.Fatalf("expected query key rejection audit event")
}
if event.EventName != service.EventTokenQueryKeyRejected {
t.Fatalf("unexpected event name: got=%s want=%s", event.EventName, service.EventTokenQueryKeyRejected)
}
serialized := strings.Join([]string{
event.EventID,
event.EventName,
event.RequestID,
event.TokenID,
event.SubjectID,
event.Route,
event.ResultCode,
event.ClientIP,
}, "|")
if strings.Contains(serialized, "raw-secret-value") {
t.Fatalf("audit event must not contain raw query key value")
}
}
func TestTOKAud007EventImmutability(t *testing.T) {
t.Parallel()
auditor := service.NewMemoryAuditEmitter()
rt := service.NewInMemoryTokenRuntime(nil)
issued, err := rt.IssueAndAudit(context.Background(), service.IssueTokenInput{
SubjectID: "2001",
Role: model.RoleOwner,
Scope: []string{"supply:*"},
TTL: 20 * time.Minute,
RequestID: "req-aud-007-1",
}, auditor)
if err != nil {
t.Fatalf("issue with audit failed: %v", err)
}
_, err = rt.RevokeAndAudit(context.Background(), issued.TokenID, "test", "req-aud-007-2", issued.SubjectID, auditor)
if err != nil {
t.Fatalf("revoke with audit failed: %v", err)
}
firstRead := auditor.Events()
secondRead := auditor.Events()
if len(firstRead) < 2 || len(secondRead) < 2 {
t.Fatalf("expected at least two audit events")
}
for idx := range firstRead {
if firstRead[idx].EventID != secondRead[idx].EventID ||
firstRead[idx].EventName != secondRead[idx].EventName ||
!firstRead[idx].CreatedAt.Equal(secondRead[idx].CreatedAt) {
t.Fatalf("event should be immutable across reads at index=%d", idx)
}
}
for idx := 1; idx < len(firstRead); idx++ {
if firstRead[idx].CreatedAt.Before(firstRead[idx-1].CreatedAt) {
t.Fatalf("event timeline should be ordered by created_at")
}
}
}
func assertAuditRequiredFields(t *testing.T, event service.AuditEvent) {
t.Helper()
if event.EventID == "" {
t.Fatalf("event_id must not be empty")
}
if event.RequestID == "" {
t.Fatalf("request_id must not be empty")
}
if event.ResultCode == "" {
t.Fatalf("result_code must not be empty")
}
if event.Route == "" {
t.Fatalf("route must not be empty")
}
if event.CreatedAt.IsZero() {
t.Fatalf("created_at must not be zero")
}
}

View File

@@ -0,0 +1,87 @@
package token_test
import "testing"
type auditTemplateCase struct {
ID string
Name string
TriggerCase string
Assertions []string
}
func TestTokenAuditTemplateCases(t *testing.T) {
t.Parallel()
cases := []auditTemplateCase{
{
ID: "TOK-AUD-001",
Name: "签发成功事件",
TriggerCase: "TOK-LIFE-001",
Assertions: []string{
"存在 token.issue.success",
"event_id/request_id/result_code/route/created_at 齐全",
},
},
{
ID: "TOK-AUD-002",
Name: "签发失败事件",
TriggerCase: "TOK-LIFE-002",
Assertions: []string{
"存在 token.issue.fail",
"result_code 准确",
},
},
{
ID: "TOK-AUD-003",
Name: "鉴权失败事件",
TriggerCase: "无效 token 访问受保护接口",
Assertions: []string{
"存在 token.authn.fail",
"包含 request_id",
},
},
{
ID: "TOK-AUD-004",
Name: "越权事件",
TriggerCase: "TOK-LIFE-008",
Assertions: []string{
"存在 token.authz.denied",
"包含 subject_id",
},
},
{
ID: "TOK-AUD-005",
Name: "吊销事件",
TriggerCase: "TOK-LIFE-005",
Assertions: []string{
"存在 token.revoke.success",
"包含 token_id",
},
},
{
ID: "TOK-AUD-006",
Name: "query key 拒绝事件",
TriggerCase: "query key 访问受保护接口",
Assertions: []string{
"存在 token.query_key.rejected",
"不含敏感值",
},
},
{
ID: "TOK-AUD-007",
Name: "事件不可篡改",
TriggerCase: "重复读取同 event_id",
Assertions: []string{
"核心字段不可变",
"时间顺序正确",
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.ID, func(t *testing.T) {
t.Skipf("模板用例,待接入实现: %s", tc.Name)
})
}
}

View File

@@ -0,0 +1,332 @@
package token_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"lijiaoqiao/platform-token-runtime/internal/auth/middleware"
"lijiaoqiao/platform-token-runtime/internal/auth/model"
"lijiaoqiao/platform-token-runtime/internal/auth/service"
)
func TestTOKLife001IssueSuccess(t *testing.T) {
t.Parallel()
rt := service.NewInMemoryTokenRuntime(nil)
ctx := context.Background()
first, err := rt.Issue(ctx, service.IssueTokenInput{
SubjectID: "2001",
Role: model.RoleOwner,
Scope: []string{"supply:*"},
TTL: 30 * time.Minute,
})
if err != nil {
t.Fatalf("issue token failed: %v", err)
}
second, err := rt.Issue(ctx, service.IssueTokenInput{
SubjectID: "2001",
Role: model.RoleOwner,
Scope: []string{"supply:*"},
TTL: 30 * time.Minute,
})
if err != nil {
t.Fatalf("issue second token failed: %v", err)
}
if first.Status != service.TokenStatusActive {
t.Fatalf("unexpected status: got=%s want=%s", first.Status, service.TokenStatusActive)
}
if !first.ExpiresAt.After(first.IssuedAt) {
t.Fatalf("expires_at must be greater than issued_at")
}
if first.TokenID == second.TokenID {
t.Fatalf("token_id should be unique")
}
}
func TestTOKLife002IssueInvalidInput(t *testing.T) {
t.Parallel()
rt := service.NewInMemoryTokenRuntime(nil)
ctx := context.Background()
_, err := rt.Issue(ctx, service.IssueTokenInput{
SubjectID: "2001",
Role: model.RoleOwner,
Scope: []string{"supply:*"},
TTL: 0,
})
if err == nil {
t.Fatalf("expected error for invalid ttl_seconds")
}
if got := rt.TokenCount(); got != 0 {
t.Fatalf("unexpected token count after invalid issue: got=%d want=0", got)
}
}
func TestTOKLife003IssueIdempotencyReplay(t *testing.T) {
t.Parallel()
rt := service.NewInMemoryTokenRuntime(nil)
ctx := context.Background()
first, err := rt.Issue(ctx, service.IssueTokenInput{
SubjectID: "2001",
Role: model.RoleOwner,
Scope: []string{"supply:*"},
TTL: 30 * time.Minute,
IdempotencyKey: "idem-life-003",
})
if err != nil {
t.Fatalf("first issue failed: %v", err)
}
second, err := rt.Issue(ctx, service.IssueTokenInput{
SubjectID: "2001",
Role: model.RoleOwner,
Scope: []string{"supply:*"},
TTL: 30 * time.Minute,
IdempotencyKey: "idem-life-003",
})
if err != nil {
t.Fatalf("replay issue failed: %v", err)
}
if first.TokenID != second.TokenID {
t.Fatalf("replayed issue must return same token_id: first=%s second=%s", first.TokenID, second.TokenID)
}
if got := rt.TokenCount(); got != 1 {
t.Fatalf("idempotent replay must not create duplicate token: got=%d want=1", got)
}
_, err = rt.Issue(ctx, service.IssueTokenInput{
SubjectID: "2001",
Role: model.RoleOwner,
Scope: []string{"supply:read"},
TTL: 30 * time.Minute,
IdempotencyKey: "idem-life-003",
})
if err == nil {
t.Fatalf("expected payload mismatch conflict for same idempotency key")
}
}
func TestTOKLife004RefreshSuccess(t *testing.T) {
t.Parallel()
rt := service.NewInMemoryTokenRuntime(nil)
ctx := context.Background()
issued, err := rt.Issue(ctx, service.IssueTokenInput{
SubjectID: "2001",
Role: model.RoleOwner,
Scope: []string{"supply:*"},
TTL: 1 * time.Minute,
})
if err != nil {
t.Fatalf("issue token failed: %v", err)
}
previousExpiresAt := issued.ExpiresAt
refreshed, err := rt.Refresh(ctx, issued.TokenID, 15*time.Minute)
if err != nil {
t.Fatalf("refresh token failed: %v", err)
}
if refreshed.Status != service.TokenStatusActive {
t.Fatalf("unexpected status after refresh: got=%s want=%s", refreshed.Status, service.TokenStatusActive)
}
if !refreshed.ExpiresAt.After(previousExpiresAt) {
t.Fatalf("expires_at should be delayed after refresh")
}
}
func TestTOKLife005RevokeSuccess(t *testing.T) {
t.Parallel()
start := time.Now()
rt := service.NewInMemoryTokenRuntime(nil)
ctx := context.Background()
issued, err := rt.Issue(ctx, service.IssueTokenInput{
SubjectID: "2001",
Role: model.RoleOwner,
Scope: []string{"supply:*"},
TTL: 10 * time.Minute,
})
if err != nil {
t.Fatalf("issue token failed: %v", err)
}
if _, err := rt.Revoke(ctx, issued.TokenID, "security_event"); err != nil {
t.Fatalf("revoke token failed: %v", err)
}
introspected, err := rt.Introspect(ctx, issued.AccessToken)
if err != nil {
t.Fatalf("introspect failed: %v", err)
}
if introspected.Status != service.TokenStatusRevoked {
t.Fatalf("unexpected status after revoke: got=%s want=%s", introspected.Status, service.TokenStatusRevoked)
}
if time.Since(start) > 5*time.Second {
t.Fatalf("revoke propagation exceeded 5 seconds in in-memory runtime")
}
}
func TestTOKLife006RevokedTokenAccessDenied(t *testing.T) {
t.Parallel()
auditor := service.NewMemoryAuditEmitter()
rt := service.NewInMemoryTokenRuntime(nil)
authorizer := service.NewScopeRoleAuthorizer()
ctx := context.Background()
issued, err := rt.Issue(ctx, service.IssueTokenInput{
SubjectID: "2001",
Role: model.RoleOwner,
Scope: []string{"supply:*"},
TTL: 5 * time.Minute,
})
if err != nil {
t.Fatalf("issue token failed: %v", err)
}
if _, err := rt.Revoke(ctx, issued.TokenID, "test_revoke"); err != nil {
t.Fatalf("revoke failed: %v", err)
}
handler := middleware.BuildTokenAuthChain(middleware.AuthMiddlewareConfig{
Verifier: rt,
StatusResolver: rt,
Authorizer: authorizer,
Auditor: auditor,
}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts", nil)
req.Header.Set("Authorization", "Bearer "+issued.AccessToken)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusUnauthorized)
}
if code := decodeMiddlewareErrorCode(t, rec); code != service.CodeAuthTokenInactive {
t.Fatalf("unexpected error code: got=%s want=%s", code, service.CodeAuthTokenInactive)
}
}
func TestTOKLife007ExpiredTokenInactive(t *testing.T) {
t.Parallel()
current := time.Date(2026, 3, 29, 15, 0, 0, 0, time.UTC)
rt := service.NewInMemoryTokenRuntime(func() time.Time { return current })
ctx := context.Background()
issued, err := rt.Issue(ctx, service.IssueTokenInput{
SubjectID: "2001",
Role: model.RoleOwner,
Scope: []string{"supply:*"},
TTL: 2 * time.Second,
})
if err != nil {
t.Fatalf("issue token failed: %v", err)
}
current = current.Add(3 * time.Second)
introspected, err := rt.Introspect(ctx, issued.AccessToken)
if err != nil {
t.Fatalf("introspect failed: %v", err)
}
if introspected.Status != service.TokenStatusExpired {
t.Fatalf("unexpected token status: got=%s want=%s", introspected.Status, service.TokenStatusExpired)
}
auditor := service.NewMemoryAuditEmitter()
authorizer := service.NewScopeRoleAuthorizer()
handler := middleware.BuildTokenAuthChain(middleware.AuthMiddlewareConfig{
Verifier: rt,
StatusResolver: rt,
Authorizer: authorizer,
Auditor: auditor,
}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodGet, "/api/v1/supply/accounts", nil)
req.Header.Set("Authorization", "Bearer "+issued.AccessToken)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusUnauthorized)
}
if code := decodeMiddlewareErrorCode(t, rec); code != service.CodeAuthTokenInactive {
t.Fatalf("unexpected error code: got=%s want=%s", code, service.CodeAuthTokenInactive)
}
}
func TestTOKLife008ViewerWriteDenied(t *testing.T) {
t.Parallel()
auditor := service.NewMemoryAuditEmitter()
rt := service.NewInMemoryTokenRuntime(nil)
authorizer := service.NewScopeRoleAuthorizer()
ctx := context.Background()
viewer, err := rt.Issue(ctx, service.IssueTokenInput{
SubjectID: "2002",
Role: model.RoleViewer,
Scope: []string{"supply:read"},
TTL: 10 * time.Minute,
})
if err != nil {
t.Fatalf("issue viewer token failed: %v", err)
}
nextCalled := false
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextCalled = true
w.WriteHeader(http.StatusNoContent)
})
handler := middleware.BuildTokenAuthChain(middleware.AuthMiddlewareConfig{
Verifier: rt,
StatusResolver: rt,
Authorizer: authorizer,
Auditor: auditor,
}, next)
req := httptest.NewRequest(http.MethodPost, "/api/v1/supply/packages", nil)
req.Header.Set("Authorization", "Bearer "+viewer.AccessToken)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("unexpected status code: got=%d want=%d", rec.Code, http.StatusForbidden)
}
if code := decodeMiddlewareErrorCode(t, rec); code != service.CodeAuthScopeDenied {
t.Fatalf("unexpected error code: got=%s want=%s", code, service.CodeAuthScopeDenied)
}
if nextCalled {
t.Fatalf("write handler should be blocked for viewer token")
}
}
type middlewareErrorEnvelope struct {
Error struct {
Code string `json:"code"`
} `json:"error"`
}
func decodeMiddlewareErrorCode(t *testing.T, rec *httptest.ResponseRecorder) string {
t.Helper()
var envelope middlewareErrorEnvelope
if err := json.Unmarshal(rec.Body.Bytes(), &envelope); err != nil {
t.Fatalf("failed to decode middleware error response: %v", err)
}
return envelope.Error.Code
}

View File

@@ -0,0 +1,132 @@
package token_test
import "testing"
// 说明:
// 1. 本文件保留完整 TOK-LIFE 模板清单作为覆盖基线。
// 2. 首批可执行用例已在 lifecycle_executable_test.go 落地:
// TOK-LIFE-001 / TOK-LIFE-004 / TOK-LIFE-005 / TOK-LIFE-008。
type lifecycleTemplateCase struct {
ID string
Name string
Preconditions []string
Steps []string
Assertions []string
}
func TestTokenLifecycleTemplateCases(t *testing.T) {
t.Parallel()
cases := []lifecycleTemplateCase{
{
ID: "TOK-LIFE-001",
Name: "签发成功",
Preconditions: []string{
"tenant_id=1001",
"subject_owner=2001",
},
Steps: []string{
"调用 POST /api/v1/platform/tokens/issue",
"记录 token_id/issued_at/expires_at/status",
},
Assertions: []string{
"status=active",
"expires_at>issued_at",
"token_id 唯一",
},
},
{
ID: "TOK-LIFE-002",
Name: "签发参数非法",
Preconditions: []string{
"ttl_seconds 超上限",
},
Steps: []string{
"调用 POST /api/v1/platform/tokens/issue",
},
Assertions: []string{
"返回 400",
"不落 active token",
},
},
{
ID: "TOK-LIFE-003",
Name: "幂等签发重放",
Steps: []string{
"相同 Idempotency-Key 重复调用签发接口",
},
Assertions: []string{
"返回同一 token_id",
"无重复写入",
},
},
{
ID: "TOK-LIFE-004",
Name: "续期成功",
Steps: []string{
"调用 POST /api/v1/platform/tokens/{tokenId}/refresh",
},
Assertions: []string{
"expires_at 延后",
"status=active",
},
},
{
ID: "TOK-LIFE-005",
Name: "吊销成功",
Steps: []string{
"调用 POST /api/v1/platform/tokens/{tokenId}/revoke",
"立即调用 introspect 查询状态",
},
Assertions: []string{
"status 最终为 revoked",
"吊销生效延迟 <=5s",
},
},
{
ID: "TOK-LIFE-006",
Name: "吊销后访问受限接口",
Steps: []string{
"使用已吊销 token 访问受保护接口",
},
Assertions: []string{
"返回 401 AUTH_TOKEN_INACTIVE",
},
},
{
ID: "TOK-LIFE-007",
Name: "过期自动失效",
Steps: []string{
"签发短 TTL token",
"等待 token 过期",
"调用 introspect 查询状态",
},
Assertions: []string{
"status=expired",
"返回不可用错误",
},
},
{
ID: "TOK-LIFE-008",
Name: "viewer 越权写操作",
Preconditions: []string{
"viewer scope=supply:read",
},
Steps: []string{
"viewer token 调用写接口",
},
Assertions: []string{
"返回 403 AUTH_SCOPE_DENIED",
"无写入副作用",
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.ID, func(t *testing.T) {
t.Skipf("模板用例,待接入实现: %s", tc.Name)
})
}
}

View File

@@ -0,0 +1,43 @@
# 规划设计对齐验证报告Checkpoint-01
- 日期2026-03-27
- 对齐触发条件:已完成 10 个子任务A-001~A-008, B-001~B-002
- 对齐范围:
- `docs/plans/2026-03-25-superpowers-execution-tasklist-v1.md`
- `docs/supply_button_level_prd_v1_2026-03-25.md`
- `docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml`
- `review/superpowers_comprehensive_planning_review_v1_2026-03-25.md`
- `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
- `docs/acceptance_gate_single_source_v1_2026-03-18.md`
## 1. 对齐结论
结论:**本检查点总体对齐,允许进入 B-003 后续执行。**
说明:
1. WG-A 目标“需求冻结”已形成可追溯证据链。
2. WG-B 当前处于“参数定义完成、路径挂载待完成”的中间态。
3. 门禁层SSOT未被破坏凭证边界主线保持一致。
## 2. 逐项核对
| 核对项 | 结果 | 证据 |
|---|---|---|
| 按钮 PRD 已从草案改为冻结 | PASS | `docs/supply_button_level_prd_v1_2026-03-25.md:3` |
| “待拍板项”已替换为“已决议项” | PASS | `docs/supply_button_level_prd_v1_2026-03-25.md:236` |
| 决议映射与会议纪要已形成双证据 | PASS | `docs/product/supply_prd_pending_to_decision_map_v1_2026-03-27.md``review/outputs/supply_prd_decision_meeting_minutes_2026-03-27.md` |
| 任务单已引用冻结 PRD 版本 | PASS | `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md:14` |
| P0-01 已在评审报告关闭 | PASS | `review/superpowers_comprehensive_planning_review_v1_2026-03-25.md:53` |
| OpenAPI 已定义幂等头参数组件 | PASS | `docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml:431``:440` |
| OpenAPI 写操作路径已挂载幂等头 | PARTIAL | 下一批次 B-003~B-007 |
## 3. 风险与约束
1. `P0-02` 仍未完全关闭:当前仅完成参数定义,尚未完成路径级 required 挂载与示例/校验。
2. 本次对齐只覆盖前 10 项,不代表 SUP staging 证据链完成。
3. `token` 运行态实现缺口TOK-REAL结论保持有效不因本批次文档修改而变化。
## 4. 准入建议
1. 允许进入下一批次B-003~B-010
2. 完成 B-010 后必须执行 Checkpoint-02 全面对齐验证。

View File

@@ -0,0 +1,39 @@
# 规划设计对齐验证报告Checkpoint-02
- 日期2026-03-27
- 对齐触发条件:累计完成 20 个子任务A-001~A-008, B-001~B-012
- 对齐目标:验证 WG-A 与 WG-B 输出是否与 SSOT、技术增强稿、评审结论一致
## 1. 总体结论
结论:**A/B 阶段已对齐,可进入 C 阶段执行。**
说明:
1. P0-01冻结状态冲突已闭环。
2. P0-02幂等头缺失已闭环。
3. P0-03执行环境阻塞仍未关闭不影响进入 C 阶段文档整改,但阻断最终发布。
## 2. 对齐矩阵
| 维度 | 检查项 | 结果 | 证据 |
|---|---|---|---|
| 需求冻结 | 按钮 PRD 状态为冻结,且不再保留待拍板 | PASS | `docs/supply_button_level_prd_v1_2026-03-25.md:3``:236` |
| 决议追踪 | 待拍板项有决议映射与会议纪要 | PASS | `docs/product/supply_prd_pending_to_decision_map_v1_2026-03-27.md``review/outputs/supply_prd_decision_meeting_minutes_2026-03-27.md` |
| 任务链路 | 执行任务单引用冻结 PRD | PASS | `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md:14` |
| 契约定义 | OpenAPI 定义 `X-Request-Id``Idempotency-Key` 参数组件 | PASS | `docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml:456``:465` |
| 契约挂载 | 5 个关键写接口全部挂载双 header | PASS | `docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml:46``:178``:242``:310``:339` |
| 冲突语义 | 409 payload mismatch 示例存在 | PASS | `docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml:500` |
| 重放语义 | 202 in-progress 示例存在,含 `retry_after_ms` | PASS | `docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml:510` |
| 设计对齐 | 技术增强稿已标注契约落地 | PASS | `docs/supply_technical_design_enhanced_v1_2026-03-25.md:42` |
| 评审闭环 | P0-02 已在 superpowers 评审报告关闭 | PASS | `review/superpowers_comprehensive_planning_review_v1_2026-03-25.md:66` |
| 门禁主线 | M-013~M-016 主线口径未偏移 | PASS | `docs/acceptance_gate_single_source_v1_2026-03-18.md` |
## 3. 未关闭项(跨阶段)
1. P0-03staging 环境与真实 token 证据链缺失。
2. TOK-REALtoken 运行态实现缺口仍在(与本次 A/B 文档对齐无冲突)。
## 4. 下一步准入
1. 进入 C-001~C-008测试路径与追踪矩阵一致化
2. C 阶段完成后执行 Checkpoint-03 对齐验证。

View File

@@ -0,0 +1,29 @@
# 规划设计对齐验证报告Checkpoint-03 / WG-C
- 日期2026-03-27
- 对齐触发条件:独立阶段 WG-CC-001~C-008完成
- 核心目标:验证“测试追踪矩阵路径口径”与 OpenAPI 主路径是否完全一致
## 1. 总体结论
结论:**WG-C 对齐通过,路径一致性缺口已关闭。**
## 2. 对齐核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| 测试矩阵 API 列使用 OpenAPI 精确参数名 | PASS | `docs/supply_test_plan_enhanced_v1_2026-03-25.md:42``:45``:48` |
| 历史路径兼容口径可追踪(`api_alias` | PASS | `docs/supply_test_plan_enhanced_v1_2026-03-25.md:38` |
| CSV 与测试方案字段结构一致 | PASS | `reports/supply_traceability_matrix_2026-03-25.csv:1` |
| 生成规则可复跑、可校验 | PASS | `docs/supply_traceability_matrix_generation_rules_v1_2026-03-27.md` |
| XR-002 验收项纳入路径一致性检查 | PASS | `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md:158` |
## 3. 仍未关闭的跨阶段项
1. D 阶段真实环境证据链staging 地址与短期 token仍缺。
2. token 运行态实现缺口TOK-REAL仍缺实现证据。
## 4. 准入建议
1. 允许进入 WG-DD-001~D-018
2. 若出现环境阻塞,优先输出阻塞清单与替代执行路径,保持任务推进不中断。

View File

@@ -0,0 +1,31 @@
# 规划设计对齐验证报告Checkpoint-04 / WG-D
- 日期2026-03-27
- 对齐触发条件:独立阶段 WG-D 启动后确认“开发阶段暂缓”
- 验证目标:确认暂缓原因与规划设计文档是否一致,避免误判
## 1. 结论
结论:**WG-D 暂缓与规划约束一致,不属于执行偏航。**
## 2. 一致性核对
| 核对项 | 结果 | 证据 |
|---|---|---|
| D 阶段要求真实 staging + 短期 token | PASS | `docs/plans/2026-03-25-superpowers-execution-tasklist-v1.md:62` |
| 预检脚本会拒绝占位 token/域名 | PASS | `scripts/supply-gate/staging_precheck_and_run.sh` |
| 当前 `.env` 仍为占位值 | PASS | `scripts/supply-gate/.env` |
| 运行结果确认为预检失败,且当前按阶段暂缓处理 | PASS | `reports/stage_d_blocker_report_2026-03-27.md` |
| 当前生产决议仍为 NO-GO | PASS | `review/final_decision_2026-03-31.md` |
## 3. 风险判定
1. 若在开发阶段将“暂缓”误判为“已验证通过”,将直接违反 SSOT 与决议门禁。
2. 当前最小正确动作是继续推进实现前置,待联调阶段再激活 D-007~D-018。
## 4. 准入条件
仅当下列条件全部满足WG-D 才从暂缓切换为执行:
1. `API_BASE_URL` 非占位且可达。
2. `OWNER/VIEWER/ADMIN` 三类短期 token 已写入 `.env`
3. `staging_precheck_and_run.sh` 预检通过。

View File

@@ -0,0 +1,20 @@
# 规划设计对齐验证报告Checkpoint-05 / WG-E
- 日期2026-03-27
- 对齐触发条件:独立阶段 WG-E 启动(依赖 D 阶段)后确认暂缓
## 1. 结论
结论:**WG-E 暂缓与任务依赖关系一致,不存在执行偏离。**
## 2. 依赖核对
| 核对项 | 结果 | 证据 |
|---|---|---|
| E-001~E-004 依赖 D 阶段产物 | PASS | `docs/plans/2026-03-25-superpowers-execution-tasklist-v1.md` |
| D 阶段当前为 DEFERRED | PASS | `reports/stage_d_blocker_report_2026-03-27.md` |
| E 阶段当前为 DEFERRED等待联调窗口 | PASS | `reports/stage_e_blocker_report_2026-03-27.md` |
## 3. 准入条件
仅当 D 阶段从暂缓切换为执行并产出 staging 实测证据后E 阶段才可继续执行。

View File

@@ -0,0 +1,32 @@
# 规划设计对齐验证报告Checkpoint-06 / F+G
- 日期2026-03-27
- 对齐触发条件:完成 10 个子任务F-001~F-007 + G-001~G-003
## 1. 总体结论
结论:**F/G 阶段对齐通过,治理与决策文档已补齐。**
## 2. 对齐核查
| 核查项 | 结果 | 证据 |
|---|---|---|
| 全局 P0 与供应/平台能力边界映射完整 | PASS | `docs/product/global_p0_to_supply_platform_mapping_v1_2026-03-27.md` |
| 预算/告警/账单导出映射到入口级 | PASS | 同上 `PRD-P0-05~07` |
| 追踪矩阵纳入平台侧 P0R-PLAT-001~003 | PASS | `reports/supply_traceability_matrix_2026-03-25.csv` |
| `/supply` 主路径策略与 `/supplier` alias 规则落地 | PASS | `docs/api_naming_strategy_supply_vs_supplier_v1_2026-03-27.md``docs/supply_api_contract_openapi_draft_v1_2026-03-25.yaml` |
| 复核报告补充 P1/P2 收敛状态 | PASS | `review/prd_tech_planning_recheck_v3_2026-03-27.md:66` |
| 链接完整性检查已执行并可追踪 | PASS | `reports/link_integrity_check_2026-03-27.md` |
| 门禁指标一致性检查已执行 | PASS | `reports/gate_metrics_consistency_check_2026-03-27.md` |
| 已生成新的最终决议稿 | PASS | `review/final_decision_draft_v2_2026-03-27.md` |
## 3. 未关闭关键暂缓项(不影响本阶段对齐结论)
1. WG-D真实 staging/短期 token 缺失DEFERRED
2. WG-E依赖 D 阶段产物,当前 DEFERRED。
3. TOK-REALtoken 运行态实现缺口未关闭。
## 4. 下一步
1. 仅剩 D/E 真实证据链路暂缓待激活。
2. 解锁后按 D-001 -> E-010 顺序继续,不允许跳步。

View File

@@ -0,0 +1,22 @@
# 规划设计对齐验证报告Checkpoint-07 / 开发阶段修订)
- 日期2026-03-27
- 触发条件:用户确认“当前仍在开发实施阶段,真实 URL/token 暂无”
## 1. 结论
结论:**执行口径已对齐开发阶段现实约束,主线未偏离。**
## 2. 对齐项
| 对齐项 | 结果 | 证据 |
|---|---|---|
| WG-D 从“执行失败”修订为“阶段暂缓” | PASS | `reports/stage_d_blocker_report_2026-03-27.md` |
| WG-E 从“执行失败”修订为“阶段暂缓” | PASS | `reports/stage_e_blocker_report_2026-03-27.md` |
| 在无 staging 参数前继续推进实现前置TOK-001 | PASS | `docs/token_runtime_minimal_spec_v1.md` |
| “仅平台分享 token”边界保持不变 | PASS | `docs/token_runtime_minimal_spec_v1.md``docs/supply_button_level_prd_v1_2026-03-25.md` |
## 3. 下一步(开发阶段)
1. 继续按 TOK-002~TOK-004 推进实现设计与测试前置。
2. 待项目进入联调阶段后再激活 D/E 阶段。

View File

@@ -0,0 +1,40 @@
# 规划设计对齐验证报告Checkpoint-08 / TOK-002~TOK-004
- 日期2026-03-29
- 触发条件:完成 TOK-002 设计与契约细化、TOK-003/TOK-004 测试断言清单
## 1. 结论
结论:**开发阶段对齐通过,可进入 TOK-002~TOK-004 实现编码阶段。**
## 2. 对齐范围
1. `docs/token_runtime_minimal_spec_v1.md`TOK-001
2. `docs/token_auth_middleware_design_v1_2026-03-29.md`TOK-002
3. `docs/platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml`TOK-002 契约)
4. `docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md`TOK-003/TOK-004
5. `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`(任务链路)
6. `docs/acceptance_gate_single_source_v1_2026-03-18.md`M-021 门禁)
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| TOK-002 设计保持“仅平台凭证入站”边界 | PASS | `docs/token_auth_middleware_design_v1_2026-03-29.md` |
| query key 外拒策略在中间件设计中可执行 | PASS | 同上(`QueryKeyRejectMiddleware` |
| TOK-002 接口契约已覆盖 issue/refresh/revoke/introspect | PASS | `docs/platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml` |
| OpenAPI 草案语法可解析 | PASS | `platform_token_openapi_yaml: PASS` |
| TOK-003 生命周期断言可执行 | PASS | `docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md` |
| TOK-004 审计事件断言可执行 | PASS | 同上(`TOK-AUD-*` |
| 任务单证据口径已区分开发阶段与联调阶段 | PASS | `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md` |
| M-021 门禁口径未被破坏 | PASS | `docs/acceptance_gate_single_source_v1_2026-03-18.md` |
## 4. 风险与限制
1. 本轮为设计/契约/测试前置对齐,不等于运行态实现已完成。
2. D/E 阶段仍处于开发阶段暂缓(待联调窗口激活)。
## 5. 下一步建议
1. 进入 TOK-002 实现编码与单测阶段。
2. 按本断言清单执行 TOK-003/TOK-004 集成测试准备。

View File

@@ -0,0 +1,45 @@
# 规划设计对齐验证报告Checkpoint-09 / TOK-002 代码骨架 + TOK-003/004 测试模板)
- 日期2026-03-29
- 触发条件:完成 TOK-002 中间件代码骨架与单测骨架、TOK-003/004 测试模板文件
## 1. 结论
结论:**开发阶段对齐通过,代码骨架与测试模板与 TOK 基线文档一致。**
## 2. 对齐范围
1. `docs/token_runtime_minimal_spec_v1.md`
2. `docs/token_auth_middleware_design_v1_2026-03-29.md`
3. `docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md`
4. `docs/platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml`
5. `docs/acceptance_gate_single_source_v1_2026-03-18.md`M-021
6. `platform-token-runtime/internal/auth/middleware/token_auth_middleware.go`
7. `platform-token-runtime/internal/auth/middleware/query_key_reject_middleware.go`
8. `platform-token-runtime/internal/auth/middleware/token_auth_middleware_test.go`
9. `platform-token-runtime/internal/token/lifecycle_test_template_test.go`
10. `platform-token-runtime/internal/token/audit_test_template_test.go`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| 中间件链路包含 request_id -> query key 外拒 -> bearer 校验 -> 状态校验 -> scope 鉴权 -> 审计 | PASS | `platform-token-runtime/internal/auth/middleware/token_auth_middleware.go` |
| query key 外拒命中 `key/api_key/token` 且返回 `401 QUERY_KEY_NOT_ALLOWED` | PASS | `platform-token-runtime/internal/auth/middleware/query_key_reject_middleware.go` |
| 错误码语义与 TOK-002 设计一致 | PASS | `platform-token-runtime/internal/auth/service/token_verifier.go` |
| TOK-002 单测骨架覆盖成功/失败/越权/边界拒绝路径 | PASS | `platform-token-runtime/internal/auth/middleware/token_auth_middleware_test.go` |
| TOK-LIFE-001~008 模板已落地 | PASS | `platform-token-runtime/internal/token/lifecycle_test_template_test.go` |
| TOK-AUD-001~007 模板已落地 | PASS | `platform-token-runtime/internal/token/audit_test_template_test.go` |
| SSOT 边界“仅平台凭证入站,不直发上游 token”未被破坏 | PASS | 上述代码与模板均未暴露上游凭证 |
## 4. 限制与说明
1. 当前环境缺少 `go` 工具链,未执行编译/单测命令,仅完成代码骨架与模板落地。
2. TOK-003/004 为模板态(`t.Skip`),待生命周期实现后替换为真实断言执行。
3. staging 联调TOK-005~TOK-007仍需真实环境参数后激活。
## 5. 下一步
1. 实现 `TokenVerifier/TokenStatusResolver/RouteAuthorizer` 的真实逻辑与缓存策略。
2.`TOK-LIFE-*` / `TOK-AUD-*` 模板由 `t.Skip` 切换为真实执行断言。
3. 在具备 `go` 环境后补充单测和覆盖率报告,作为 TOK-002 联调阶段证据。

View File

@@ -0,0 +1,41 @@
# 规划设计对齐验证报告Checkpoint-10 / TOK 最小实现 + 部分可执行测试)
- 日期2026-03-29
- 触发条件:完成内存版 token 运行时实现,并将指定模板用例转为可执行测试
## 1. 结论
结论:**开发阶段对齐通过TOK-002/003/004 已从“纯骨架”推进至“最小可运行实现 + 部分可执行断言”。**
## 2. 对齐范围
1. `docs/token_runtime_minimal_spec_v1.md`
2. `docs/token_auth_middleware_design_v1_2026-03-29.md`
3. `docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md`
4. `docs/platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml`
5. `platform-token-runtime/internal/auth/service/inmemory_runtime.go`
6. `platform-token-runtime/internal/token/lifecycle_executable_test.go`
7. `platform-token-runtime/internal/token/audit_executable_test.go`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| 实现最小 token 运行时(签发/续期/吊销/introspect | PASS | `platform-token-runtime/internal/auth/service/inmemory_runtime.go` |
| TokenVerifier/StatusResolver 已可被中间件直接调用 | PASS | 同上(`Verify` / `Resolve` |
| RouteAuthorizer 已落实 owner/viewer/admin + scope 语义 | PASS | 同上(`ScopeRoleAuthorizer` |
| TOK-LIFE-001/004/005/008 已转为可执行测试 | PASS | `platform-token-runtime/internal/token/lifecycle_executable_test.go` |
| TOK-AUD-003/004/006 已转为可执行测试 | PASS | `platform-token-runtime/internal/token/audit_executable_test.go` |
| SSOT 边界“仅平台凭证入站,不直发上游 token”保持一致 | PASS | 中间件链路 + 测试断言均未暴露上游凭证 |
## 4. 限制与说明
1. 当前环境无 `go` 工具链,未执行 `go test`;本轮为代码级实现与对齐回填。
2. 其余生命周期/审计用例仍保持模板态(`t.Skip`),待后续阶段继续落地。
3. 当前实现为内存版,用于开发阶段前置验证;非生产部署实现。
## 5. 下一步
1. 继续将 `TOK-LIFE-002/003/006/007``TOK-AUD-001/002/005/007` 转可执行断言。
2. 增加幂等键语义(`Idempotency-Key`)与审计不可篡改校验实现。
3. 在具备 Go 环境后执行 `go test ./...`,补齐测试报告证据。

View File

@@ -0,0 +1,44 @@
# 规划设计对齐验证报告Checkpoint-11 / Go 工具链 + TOK 全量用例可执行化)
- 日期2026-03-29
- 触发条件:安装 Go 工具链,完成 TOK 生命周期与审计断言全量可执行化,并通过本地测试
## 1. 结论
结论:**开发阶段对齐通过TOK-003/TOK-004 已由“部分可执行”推进为“全量可执行”,并已完成本地 `go test` 验证。**
## 2. 对齐范围
1. `docs/token_runtime_minimal_spec_v1.md`
2. `docs/token_auth_middleware_design_v1_2026-03-29.md`
3. `docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md`
4. `docs/platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml`
5. `platform-token-runtime/internal/auth/service/inmemory_runtime.go`
6. `platform-token-runtime/internal/token/lifecycle_executable_test.go`
7. `platform-token-runtime/internal/token/audit_executable_test.go`
8. `platform-token-runtime/internal/token/lifecycle_test_template_test.go`
9. `platform-token-runtime/internal/token/audit_test_template_test.go`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| Go 工具链已安装且可执行 | PASS | `/.tools/go-current/bin/go version => go1.26.1` |
| TOK-LIFE-001~008 已具备可执行实现 | PASS | `platform-token-runtime/internal/token/lifecycle_executable_test.go` |
| TOK-AUD-001~007 已具备可执行实现 | PASS | `platform-token-runtime/internal/token/audit_executable_test.go` |
| 幂等重放语义已实现(同键同载荷返回同 token_id冲突载荷拒绝 | PASS | `inmemory_runtime.go` + `TestTOKLife003IssueIdempotencyReplay` |
| 吊销/过期后访问受保护路由返回 `AUTH_TOKEN_INACTIVE` | PASS | `TestTOKLife006RevokedTokenAccessDenied` / `TestTOKLife007ExpiredTokenInactive` |
| 审计必填字段与不可泄露约束断言可执行 | PASS | `assertAuditRequiredFields` + `TestTOKAud006QueryKeyRejectedEvent` |
| 本地测试执行通过 | PASS | `go test ./...`(全部通过) |
## 4. 限制与说明
1. 当前实现为内存版运行时,用于开发阶段验证;未替代生产级持久化/缓存/总线方案。
2. 模板文件保留用于需求追踪基线,执行入口已迁移到 `*_executable_test.go`
3. staging 联调TOK-005~TOK-007仍需真实环境参数后激活。
## 5. 下一步
1. 将内存版运行时替换为数据库 + 缓存实现,接入真实 `platform_token_registry/token_status_cache`
2. 接入真实审计落库表并补充查询验证脚本,替换当前内存审计存储。
3.`.env` 真值就绪后执行 staging 全链路回归并回填 TOK-005~TOK-007 证据。

View File

@@ -0,0 +1,43 @@
# 规划设计对齐验证报告Checkpoint-12 / TOK-005 Dry-Run 门禁并入)
- 日期2026-03-30
- 触发条件:完成 TOK-005 开发阶段 dry-run 脚本、执行证据与门禁文档并入
## 1. 结论
结论:**开发阶段对齐通过。TOK-005 已形成“可执行脚本 + 可落地证据 + 任务单口径”闭环,可等待真实 staging 参数后切换联调。**
## 2. 对齐范围
1. `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
2. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
3. `scripts/supply-gate/tok005_boundary_dryrun.sh`
4. `scripts/supply-gate/staging_precheck_and_run.sh`
5. `reports/gates/tok005_dryrun_2026-03-30_090146.md`
6. `tests/supply/artifacts/tok005_dryrun_2026-03-30_090146/go_test_output.txt`
7. `docs/token_lifecycle_audit_test_assertions_v1_2026-03-29.md`
8. `docs/acceptance_gate_single_source_v1_2026-03-18.md`M-013~M-016, M-021
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| TOK-005 dry-run 命令已落地且可执行 | PASS | `scripts/supply-gate/tok005_boundary_dryrun.sh` |
| staging 预检脚本已接入 TOK-005 dry-run 开关 | PASS | `scripts/supply-gate/staging_precheck_and_run.sh``ENABLE_TOK005_DRYRUN` |
| dry-run 输出报告与原始日志可追溯 | PASS | `reports/gates/tok005_dryrun_2026-03-30_090146.md` + `.log` |
| TOK 运行态 `go test ./...` 在 dry-run 中通过 | PASS | `tests/supply/artifacts/tok005_dryrun_2026-03-30_090146/go_test_output.txt` |
| M-016query key 外拒)具备脚本化检查 | PASS | dry-run 检查项 `Query Key 外拒检查` |
| M-013审计脱敏具备脚本化检查 | PASS | dry-run 检查项 `审计脱敏检查` |
| staging 准备度口径清晰,不伪造联调结论 | PASS | dry-run 报告 `staging 实测就绪性 = NOplaceholder token` |
| 任务单证据口径已区分开发阶段/联调阶段 | PASS | TOK-005 行已更新为双阶段证据 |
## 4. 限制与说明
1. 当前仅完成开发阶段 dry-run不等价于 staging 联调达标。
2. `M-015`(需求方绕过平台直连供应方)仍需真实网络与策略环境实测。
3. 生产放行仍受 `TOK-006/TOK-007` 与最终决议约束。
## 5. 下一步
1.`.env` 真值就绪后,执行:`bash scripts/supply-gate/staging_precheck_and_run.sh scripts/supply-gate/.env`
2. 联调完成后回填:`tests/supply/sec_sup_boundary_report_2026-03-30.md``review/final_decision_2026-03-31.md`

View File

@@ -0,0 +1,43 @@
# 规划设计对齐验证报告Checkpoint-13 / TOK-006 统一 Gate 汇总链路)
- 日期2026-03-30
- 触发条件:完成 TOK-006 汇总脚本、单页判定模板、实跑证据与文档并入
## 1. 结论
结论:**开发阶段对齐通过。TOK-006 已形成“统一汇总脚本 + 单页判定模板 + 实跑证据 + 任务口径”闭环。**
## 2. 对齐范围
1. `scripts/supply-gate/tok006_gate_bundle.sh`
2. `reports/gates/tok006_gate_bundle_2026-03-30_091849.md`
3. `reports/gates/tok006_gate_bundle_2026-03-30_091849.log`
4. `reports/gates/tok006_release_decision_onepager_template_v1_2026-03-30.md`
5. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
6. `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
7. `reports/gates/tok005_dryrun_2026-03-30_091849.md`
8. `tests/supply/ui_sup_acc_report_2026-03-28.md`
9. `tests/supply/ui_sup_pkg_report_2026-03-29.md`
10. `tests/supply/ui_sup_set_report_2026-03-29.md`
11. `tests/supply/sec_sup_boundary_report_2026-03-30.md`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| TOK-006 汇总脚本可执行且可生成单页结论 | PASS | `tok006_gate_bundle.sh` |
| 汇总范围覆盖 TOK-005 + SUP-004~007 | PASS | `tok006_gate_bundle_2026-03-30_091849.md` Gate 矩阵 |
| 发布判定规则满足“有 mock 或 readiness!=YES 不得 GO” | PASS | 同上(输出 `CONDITIONAL_GO` |
| 单页判定模板可复用且字段齐全 | PASS | `tok006_release_decision_onepager_template_v1_2026-03-30.md` |
| 命令手册已纳入 TOK-006 执行入口 | PASS | `docs/supply_gate_command_playbook_v1_2026-03-25.md` |
| 任务单 TOK-006 证据口径已区分开发/联调阶段 | PASS | `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md` |
## 4. 限制与说明
1. 当前汇总判定为 `CONDITIONAL_GO`,原因是现有 SUP 证据为 mock且 TOK-005 readiness 为 NO占位 token
2. 本轮不伪造 staging 结果;真实放行仍依赖 `staging_precheck_and_run.sh` 实测证据。
## 5. 下一步
1. `.env` 真值就绪后,执行:`ENABLE_SUP_RUN=1 bash scripts/supply-gate/tok006_gate_bundle.sh scripts/supply-gate/.env`
2. 实测通过后将单页判定切换为 staging 证据版本,并回填 `review/final_decision_2026-03-31.md`

View File

@@ -0,0 +1,46 @@
# 规划设计对齐验证报告Checkpoint-14 / Superpowers 严格分阶段验证)
- 日期2026-03-30
- 触发条件:新增并执行 `scripts/ci/superpowers_stage_validate.sh`,完成阶段化验证与证据回填
## 1. 结论
结论:**开发阶段对齐通过。已按 superpowers 方式完成“代码测试 + SUP 脚本 + TOK 门禁 + 质量门禁 + staging 预检”的严格阶段验证。**
## 2. 对齐范围
1. `scripts/ci/superpowers_stage_validate.sh`
2. `reports/gates/superpowers_stage_validation_2026-03-30_120619.md`
3. `reports/gates/superpowers_stage_validation_2026-03-30_120619.log`
4. `tests/supply/artifacts/superpowers_stage_validation_2026-03-30_120619/phase01_go_test.log`
5. `tests/supply/artifacts/superpowers_stage_validation_2026-03-30_120619/phase02_sup_run_all_mock.log`
6. `tests/supply/artifacts/superpowers_stage_validation_2026-03-30_120619/phase03_tok005_dryrun_mock.log`
7. `tests/supply/artifacts/superpowers_stage_validation_2026-03-30_120619/phase04_tok006_bundle.log`
8. `tests/supply/artifacts/superpowers_stage_validation_2026-03-30_120619/phase05_dependency_audit.log`
9. `tests/supply/artifacts/superpowers_stage_validation_2026-03-30_120619/phase06_stage_gate_drill.log`
10. `tests/supply/artifacts/superpowers_stage_validation_2026-03-30_120619/phase07_staging_precheck.log`
11. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
12. `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| 阶段验证脚本可执行且可复跑 | PASS | `scripts/ci/superpowers_stage_validate.sh` |
| 代码测试阶段PHASE-01通过 | PASS | `phase01_go_test.log` |
| SUP 本地联调阶段PHASE-02通过 | PASS | `phase02_sup_run_all_mock.log` |
| TOK-005/TOK-006 阶段PHASE-03/04通过 | PASS | `phase03_tok005_dryrun_mock.log` + `phase04_tok006_bundle.log` |
| 依赖/阶段门禁阶段PHASE-05/06通过 | PASS | `phase05_dependency_audit.log` + `phase06_stage_gate_drill.log` |
| 真实 staging 预检阶段PHASE-07按规则 DEFERRED | PASS | `phase07_staging_precheck.log`placeholder token |
| 总判定逻辑符合门禁规则 | PASS | `superpowers_stage_validation_2026-03-30_120619.md`CONDITIONAL_GO |
## 4. 限制与说明
1. 本轮 `PHASE-07` 为 DEFERRED不等价于 staging 联调通过。
2. 因缺少真实 token 与真实 API_BASE_URL当前不能产生生产 GO 结论。
3. 其余可执行阶段均已按返回码与证据路径验证通过。
## 5. 下一步
1. `.env` 真值就绪后重跑同一脚本,目标将 PHASE-07 从 DEFERRED 收敛为 PASS。
2. 重跑后更新 `reports/gates/superpowers_stage_validation_*.md` 并触发 TOK-007 决议复审。

View File

@@ -0,0 +1,39 @@
# 规划设计对齐验证报告Checkpoint-15 / TOK-007 复审自动化)
- 日期2026-03-30
- 触发条件:新增 TOK-007 复审脚本并实跑,完成任务链路与命令手册回填
## 1. 结论
结论:**开发阶段对齐通过。TOK-007 已具备可执行复审入口,可自动汇总 TOK-006/Superpowers/SUP Gate 结果并生成复审报告。**
## 2. 对齐范围
1. `scripts/ci/tok007_release_recheck.sh`
2. `review/outputs/tok007_release_recheck_2026-03-30_121727.md`
3. `reports/gates/tok007_release_recheck_2026-03-30_121727.log`
4. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
5. `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
6. `reports/gates/tok006_gate_bundle_2026-03-30_120620.md`
7. `reports/gates/superpowers_stage_validation_2026-03-30_120619.md`
8. `reports/supply_gate_review_2026-03-31.md`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| TOK-007 脚本可执行并可复跑 | PASS | `scripts/ci/tok007_release_recheck.sh` |
| 复审输入源覆盖 TOK-006/Superpowers/SUP Gate | PASS | `tok007_release_recheck_2026-03-30_121727.md` |
| 输出结论与当前状态一致CONDITIONAL GO | PASS | 同上(机判结论) |
| 命令手册已纳入 TOK-007 执行入口 | PASS | `docs/supply_gate_command_playbook_v1_2026-03-25.md` |
| 任务单 TOK-007 已区分开发阶段/联调阶段证据 | PASS | `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md` |
## 4. 限制与说明
1. 当前复审结论仍为 `CONDITIONAL GO`,因为 staging 真值未就绪,真实联调阶段尚未收敛。
2. 自动化复审不替代专家签署,仅用于复审前的结构化证据汇总。
## 5. 下一步
1. staging 参数就绪后,重跑 `superpowers_stage_validate.sh``tok006_gate_bundle.sh`
2. 复跑 `tok007_release_recheck.sh` 后,将输出回填到 `review/final_decision_2026-03-31.md`

View File

@@ -0,0 +1,40 @@
# 规划设计对齐验证报告Checkpoint-16 / 决议一致性校验并入 TOK-007
- 日期2026-03-30
- 触发条件:新增并执行 `final_decision_consistency_check.sh`,并将其并入 TOK-007 证据链
## 1. 结论
结论:**开发阶段对齐通过。TOK-007 已具备“自动复审 + 最终决议一致性校验”双重门禁能力。**
## 2. 对齐范围
1. `scripts/ci/final_decision_consistency_check.sh`
2. `reports/gates/final_decision_consistency_2026-03-30_*.md`
3. `reports/gates/final_decision_consistency_2026-03-30_*.log`
4. `scripts/ci/tok007_release_recheck.sh`
5. `review/outputs/tok007_release_recheck_2026-03-30_122908.md`
6. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
7. `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
8. `review/final_decision_2026-03-31.md`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| 一致性校验脚本可执行 | PASS | `scripts/ci/final_decision_consistency_check.sh` |
| 三源结论可解析final/tok007/superpowers | PASS | `final_decision_consistency_2026-03-30_*.md` |
| final 与 tok007 不一致时输出 WARN不自动改签署结论 | PASS | 同上(`RESULT=WARN` |
| 命令手册已纳入一致性校验步骤 | PASS | `docs/supply_gate_command_playbook_v1_2026-03-25.md` |
| TOK-007 任务证据口径已扩展为双脚本 | PASS | `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md` |
## 4. 限制与说明
1. 当前一致性状态为 `WARN``final_decision=NO_GO``TOK-007=CONDITIONAL_GO`
2. 该状态说明“决议文档尚未按最新复审自动结论更新”,不代表可直接生产 GO。
3. 真实 staging 阶段未收敛前,不建议变更最终签署结论。
## 5. 下一步
1. staging 真值就绪后,按顺序重跑:`superpowers_stage_validate` -> `tok007_release_recheck` -> `final_decision_consistency_check`
2.`PHASE-07=PASS` 且一致性为 PASS 时,再提交最终决议签署更新。

View File

@@ -0,0 +1,38 @@
# 规划设计对齐验证报告Checkpoint-17 / TOK-007 候选决议稿生成)
- 日期2026-03-30
- 触发条件:新增并执行 `tok007_generate_final_decision_candidate.sh`
## 1. 结论
结论:**开发阶段对齐通过。TOK-007 已补齐“候选决议稿自动生成”能力,实现不改原件前提下的可审阅回填。**
## 2. 对齐范围
1. `scripts/ci/tok007_generate_final_decision_candidate.sh`
2. `review/outputs/final_decision_candidate_from_tok007_2026-03-30_123719.md`
3. `reports/gates/tok007_generate_candidate_2026-03-30_123719.log`
4. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
5. `docs/subapi_integration_risk_controls_execution_tasks_v1_2026-03-17.md`
6. `review/final_decision_2026-03-31.md`
7. `review/outputs/tok007_release_recheck_2026-03-30_122908.md`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| 候选稿生成脚本可执行 | PASS | `scripts/ci/tok007_generate_final_decision_candidate.sh` |
| 输入来源正确final_decision + tok007_recheck | PASS | `tok007_generate_candidate_2026-03-30_123719.log` |
| 输出候选稿不覆盖原签署文件 | PASS | `review/outputs/final_decision_candidate_from_tok007_2026-03-30_123719.md` |
| 候选稿结论与 TOK-007 自动复审一致 | PASS | 同上(`CONDITIONAL GO` |
| 命令手册与任务单证据口径已同步 | PASS | 对应文档更新 |
## 4. 限制与说明
1. 候选稿仅用于人工审阅,不代表签署生效结论。
2. 真实 staging 阶段仍未收敛,最终签署建议保持谨慎。
## 5. 下一步
1. staging 真值就绪后重跑所有 TOK-007 链路脚本。
2. 人工审阅候选稿后再更新正式签署版 `final_decision_2026-03-31.md`

View File

@@ -0,0 +1,44 @@
# 规划设计对齐验证报告Checkpoint-18 / M-017~M-019 指标修复与复跑)
- 日期2026-03-30
- 触发条件:修复 `M-018` 统计异常并完成阶段链路复跑
## 1. 结论
结论:**开发阶段对齐通过。指标链路已修复并纳入自动化复跑阶段验证与TOK-007证据链保持一致。**
## 2. 对齐范围
1. `scripts/ci/metrics_daily_snapshot.sh`
2. `scripts/ci/metrics_trend_report.sh`
3. `reports/gates/metrics_daily_snapshot_2026-03-30.md`
4. `reports/gates/metrics_trend_7d_2026-03-30.md`
5. `reports/gates/superpowers_stage_validation_2026-03-30_154103.md`
6. `review/outputs/tok007_release_recheck_2026-03-30_154104.md`
7. `reports/gates/final_decision_consistency_2026-03-30_154104.md`
8. `review/outputs/final_decision_candidate_from_tok007_2026-03-30_154104.md`
9. `reports/gates/superpowers_release_pipeline_2026-03-30_154103.md`
10. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
11. `reports/superpowers_execution_progress_2026-03-27.md`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| M-018 计算逻辑恢复正确(不再出现 236.36% | PASS | `metrics_daily_snapshot_2026-03-30.md``pass_steps=8/9` |
| 日快照写入会自动清理 debug 行 | PASS | `scripts/ci/metrics_daily_snapshot.sh` |
| 趋势统计仅使用标准日期记录 | PASS | `scripts/ci/metrics_trend_report.sh` + `metrics_trend_7d_2026-03-30.md` |
| Superpowers PHASE-08/09 可执行并通过 | PASS | `superpowers_stage_validation_2026-03-30_154103.md` |
| TOK-007 复审链复跑后证据一致 | PASS | `tok007_release_recheck_2026-03-30_154104.md` + `final_decision_consistency_2026-03-30_154104.md` |
| 总控流水可复跑且步骤全 PASS | PASS | `superpowers_release_pipeline_2026-03-30_154103.md` |
## 4. 限制与说明
1. 真实 staging 凭证仍未就绪PHASE-07 继续按规则保持 DEFERRED。
2. 结论维持 `CONDITIONAL_GO/NO_GO` 防线,不得提前判定生产 `GO`
3. 历史 debug 文件可保留用于审计回溯,但不会进入趋势统计口径。
## 5. 下一步
1. 进入真实 staging 联调窗口后,复跑 `superpowers_release_pipeline.sh` 获取可签署证据。
2. 联调完成后更新 `review/final_decision_2026-03-31.md` 与对应签署记录。

View File

@@ -0,0 +1,46 @@
# 规划设计对齐验证报告Checkpoint-19 / TOK-REAL 与 M-021 接入)
- 日期2026-03-30
- 触发条件:新增 token API 服务实现并将 M-021 接入阶段门禁
## 1. 结论
结论:**开发阶段对齐通过。TOK-REAL-001/003 的“无实现/无构建工件”缺口已明显收敛M-021 已具备自动化计算与门禁接入能力。**
## 2. 对齐范围
1. `platform-token-runtime/cmd/platform-token-runtime/main.go`
2. `platform-token-runtime/internal/httpapi/token_api.go`
3. `platform-token-runtime/internal/httpapi/token_api_test.go`
4. `platform-token-runtime/internal/auth/service/inmemory_runtime.go`
5. `platform-token-runtime/Dockerfile`
6. `scripts/ci/token_runtime_readiness_check.sh`
7. `scripts/ci/superpowers_stage_validate.sh`
8. `scripts/ci/superpowers_release_pipeline.sh`
9. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
10. `reports/gates/token_runtime_readiness_2026-03-30_160246.md`
11. `reports/gates/superpowers_stage_validation_2026-03-30_160244.md`
12. `reports/gates/superpowers_release_pipeline_2026-03-30_160244.md`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| Token API 服务具备可执行入口 | PASS | `cmd/platform-token-runtime/main.go` |
| `issue/refresh/revoke/introspect` 主接口实现存在 | PASS | `internal/httpapi/token_api.go` |
| API 级行为具备可执行测试覆盖 | PASS | `internal/httpapi/token_api_test.go` |
| runtime 可构建并通过测试 | PASS | `token_runtime_go_build_*.log` + `token_runtime_go_test_*.log` |
| M-021 自动化脚本可计算并输出结论 | PASS | `scripts/ci/token_runtime_readiness_check.sh` + readiness 报告 |
| Superpowers 阶段门禁已纳入 M-021 | PASS | `superpowers_stage_validation_2026-03-30_160244.md`PHASE-10 PASS |
## 4. 限制与说明
1. M-021=100% 仅表示“开发阶段实现就绪”,不代表真实 staging 已验收通过。
2. PHASE-07 仍为 DEFERRED真实 URL 与短期 token 未就绪),因此总门禁结论仍为 `CONDITIONAL_GO`
3. 最终签署结论仍需以真实联调证据替换 mock 证据后更新。
## 5. 下一步
1. 进入联调窗口后,使用真实 `.env` 执行 `staging_precheck_and_run.sh`
2. 在真实 staging 复跑 `superpowers_release_pipeline.sh`,并更新最终签署稿。
3. 若要进一步关闭 TOK-REAL-002补齐审计事件入库与查询证明链含租户维度查询样例

View File

@@ -0,0 +1,48 @@
# 规划设计对齐验证报告Checkpoint-20 / TOK-REAL-002 审计查询与差距复审)
- 日期2026-03-30
- 触发条件:补齐 token 审计查询能力并更新 TOK-REAL 差距结论
## 1. 结论
结论:**开发阶段对齐通过。token 审计查询能力已并入实现与契约M-021 指标覆盖从 9 项扩展到 12 项且全部通过。**
## 2. 对齐范围
1. `platform-token-runtime/internal/auth/service/token_verifier.go`
2. `platform-token-runtime/internal/auth/service/inmemory_runtime.go`
3. `platform-token-runtime/internal/httpapi/token_api.go`
4. `platform-token-runtime/internal/httpapi/token_api_test.go`
5. `docs/platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml`
6. `sql/postgresql/token_runtime_schema_v1.sql`
7. `scripts/ci/token_runtime_readiness_check.sh`
8. `scripts/ci/superpowers_stage_validate.sh`
9. `scripts/ci/superpowers_release_pipeline.sh`
10. `reports/gates/token_runtime_readiness_2026-03-30_173728.md`
11. `reports/gates/superpowers_stage_validation_2026-03-30_173726.md`
12. `reports/gates/superpowers_release_pipeline_2026-03-30_173726.md`
13. `reports/token_runtime_implementation_gap_review_2026-03-30.md`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| 审计查询接口已落地(代码) | PASS | `token_api.go``/api/v1/platform/tokens/audit-events` |
| 审计查询接口已落地(契约) | PASS | `platform_token_api_contract_openapi_draft_v1_2026-03-29.yaml` |
| 审计查询能力具备可执行测试 | PASS | `token_api_test.go` |
| token 运行态持久化表结构工件存在 | PASS | `sql/postgresql/token_runtime_schema_v1.sql` |
| M-021 检查项扩展后仍 100% | PASS | `token_runtime_readiness_2026-03-30_173728.md`13/13 |
| 阶段门禁与总控流水复跑通过 | PASS | `superpowers_stage_validation_2026-03-30_173726.md` + `superpowers_release_pipeline_2026-03-30_173726.md` |
| TOK-REAL 差距结论已更新为“开发收敛+联调待闭环” | PASS | `token_runtime_implementation_gap_review_2026-03-30.md` |
## 4. 限制与说明
1. 真实 staging 凭证仍未就绪PHASE-07 继续 DEFERRED。
2. 因存在真实联调缺口,发布结论仍不得上调为生产 `GO`
3. 本轮只关闭开发阶段能力缺口,不替代真实环境验收。
## 5. 下一步
1. 进入真实联调窗口后执行 staging 全链路复跑并回填。
2. 更新最终签署稿中 M-021 与 TOK-REAL 风险状态。
3. 将 token 审计查询结果并入安全看板与取证流程(租户/主体维度)。

View File

@@ -0,0 +1,46 @@
# 规划设计对齐验证报告Checkpoint-21 / 联调前收口与决议口径同步)
- 日期2026-03-30
- 触发条件:完成 staging 预检增强、决议文档口径同步、TOK-007 证据链复跑
## 1. 结论
结论:**本阶段对齐通过。已将“开发阶段能力收敛”与“真实 staging 待验”明确分离,避免对 M-021 与 token 风险做错误外推。**
## 2. 对齐范围
1. `scripts/supply-gate/staging_precheck_and_run.sh`
2. `reports/gates/staging_token_go_evidence_template_v1_2026-03-30.md`
3. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
4. `review/final_decision_2026-03-31.md`
5. `reports/supply_gate_review_2026-03-31.md`
6. `reports/token_runtime_implementation_gap_review_2026-03-30.md`
7. `reports/gates/token_runtime_readiness_2026-03-30_181926.md`
8. `reports/gates/superpowers_stage_validation_2026-03-30_181925.md`
9. `reports/gates/superpowers_release_pipeline_2026-03-30_181925.md`
10. `review/outputs/tok007_release_recheck_2026-03-30_182149.md`
11. `reports/gates/final_decision_consistency_2026-03-30_182149.md`
12. `review/outputs/final_decision_candidate_from_tok007_2026-03-30_182149.md`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| staging 预检已纳入 M-021 前置检查 | PASS | `staging_precheck_and_run.sh` |
| 联调证据回填模板可直接执行 | PASS | `staging_token_go_evidence_template_v1_2026-03-30.md` |
| Final Decision 中 M-021 口径与当前实现一致 | PASS | `review/final_decision_2026-03-31.md` |
| SUP 汇总风险描述与 TOK 差距复审一致 | PASS | `reports/supply_gate_review_2026-03-31.md` + `reports/token_runtime_implementation_gap_review_2026-03-30.md` |
| TOK-007 复审已显式纳入 M-021 输入 | PASS | `tok007_release_recheck_2026-03-30_181927.md` |
| 阶段验证与总控流水可复跑且通过 | PASS | `superpowers_stage_validation_2026-03-30_181925.md` + `superpowers_release_pipeline_2026-03-30_181925.md` |
## 4. 限制与说明
1. PHASE-07 仍为 DEFERRED说明真实 staging 参数尚未完成闭环。
2. 当前结论仍应保持 `CONDITIONAL_GO/NO_GO`,不得提前判定生产 `GO`
3. 本次更新重点是“口径对齐与防误判”,不替代真实联调结果。
## 5. 下一步
1. 使用模板执行真实 staging 回填,补齐 M-013~M-016 与 M-021 的生产口径证据。
2. 回填完成后重跑 `superpowers_release_pipeline.sh` 并更新签署版 `final_decision`
3. 若 PHASE-07 转为 PASS再触发下一轮专家复审。

View File

@@ -0,0 +1,46 @@
# 规划设计对齐验证报告Checkpoint-22 / 联调自动化补齐与双口径决议)
- 日期2026-03-30
- 触发条件:新增 staging 自动化脚本与 final_decision 双口径指标表
## 1. 结论
结论:**本阶段对齐通过。已把“联调前准备”从人工流程提升为可执行脚本,并将决议文档升级为开发口径与 staging 口径并行展示,降低误判风险。**
## 2. 对齐范围
1. `scripts/ci/staging_evidence_autofill.sh`
2. `scripts/ci/staging_release_pipeline.sh`
3. `scripts/ci/superpowers_stage_validate.sh`
4. `scripts/supply-gate/staging_precheck_and_run.sh`
5. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
6. `review/final_decision_2026-03-31.md`
7. `review/outputs/final_decision_candidate_from_tok007_2026-03-30_182830.md`
8. `reports/gates/staging_token_go_evidence_autofill_2026-03-30_182910.md`
9. `reports/gates/superpowers_release_pipeline_2026-03-30_182827.md`
10. `reports/gates/superpowers_stage_validation_2026-03-30_182827.md`
11. `reports/gates/token_runtime_readiness_2026-03-30_182829.md`
12. `review/outputs/tok007_release_recheck_2026-03-30_182830.md`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| staging 证据自动回填脚本可执行 | PASS | `staging_evidence_autofill_2026-03-30_182910.md` |
| staging 一键流水脚本已落地可串联3步 | PASS | `scripts/ci/staging_release_pipeline.sh` |
| PHASE-07 已支持自定义 env 文件 | PASS | `superpowers_stage_validate.sh``STAGING_ENV_FILE` |
| final_decision 指标表已改为双口径 | PASS | `review/final_decision_2026-03-31.md` |
| TOK-007 候选稿与双口径保持一致 | PASS | `final_decision_candidate_from_tok007_2026-03-30_182830.md` |
| 总控流水可复跑并通过 | PASS | `superpowers_release_pipeline_2026-03-30_182827.md` |
## 4. 限制与说明
1. `PHASE-07` 当前仍 `DEFERRED`,说明真实 staging 参数尚未闭环。
2. `staging_evidence_autofill.sh` 仅做草稿抽取,不替代人工签署。
3. 双口径表的 staging 列仍待真实联调回填,当前不能上调为生产 `GO`
## 5. 下一步
1. 使用真实 `.env` 执行 `scripts/ci/staging_release_pipeline.sh`
2. 以真实证据覆盖模板并更新 `final_decision` 签署页。
3. 若 PHASE-07 转 PASS发起下一轮专家复审会。

View File

@@ -0,0 +1,49 @@
# 规划设计对齐验证报告Checkpoint-23 / staging防误跑与证据绑定增强
- 日期2026-03-30
- 触发条件:补齐 staging 流水防误跑机制与证据输入绑定能力
## 1. 结论
结论:**本阶段对齐通过。已补齐“local/mock 防误跑确认 + 自动拉起 mock 演练 + 证据文件显式绑定”三项缺口,且验证链路可复跑。**
## 2. 对齐范围
1. `scripts/ci/staging_evidence_autofill.sh`
2. `scripts/ci/staging_release_pipeline.sh`
3. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
4. `reports/gates/staging_release_pipeline_2026-03-30_185530.md`
5. `reports/gates/staging_token_go_evidence_autofill_2026-03-30_185535.md`
6. `reports/gates/staging_token_go_evidence_autofill_manual_bind_2026-03-30_1853.md`
7. `reports/gates/superpowers_stage_validation_2026-03-30_185531.md`
8. `reports/gates/superpowers_release_pipeline_2026-03-30_185531.md`
9. `review/outputs/tok007_release_recheck_2026-03-30_185535.md`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| `staging_evidence_autofill.sh` 支持显式输入证据文件(非 latest 模式) | PASS | `staging_token_go_evidence_autofill_manual_bind_2026-03-30_1853.md` |
| `staging_release_pipeline.sh` 检测 local/mock env 并要求显式确认 | PASS | 无 `ALLOW_LOCAL_MOCK_STAGING` 时命令返回失败(日志已验证) |
| local/mock 显式确认后可自动拉起 mock server 并串行完成 3 步流水 | PASS | `staging_release_pipeline_2026-03-30_185530.md` |
| STEP-03 回填脚本已绑定本次流水证据路径 | PASS | `staging_token_go_evidence_autofill_2026-03-30_185535.md` |
| Superpowers 主链结果保持 `CONDITIONAL_GO` 防线(不误判为 GO | PASS | `superpowers_stage_validation_2026-03-30_185531.md` |
## 4. 差异与改进点
1. 新增 `staging_evidence_autofill.sh` 参数:`--staging-run-log``--stage-report``--token-readiness``--tok007-report``--pipeline-report``--sec-report``--out-file`
2. 新增 `staging_release_pipeline.sh` 防误跑逻辑:检测 local/mock 环境且未确认时立即失败。
3. 新增 local/mock 演练可执行保障:`ALLOW_LOCAL_MOCK_STAGING=1` 时,若本地 API 不可达则自动尝试拉起 mock server。
4. 文档同步:命令手册补充了防误跑开关与显式证据绑定示例。
## 5. 限制与说明
1. 本次通过基于 local/mock 演练,不能替代真实 staging 证据。
2. `TOK-007` 最新机判仍为 `CONDITIONAL_GO`,与“真实参数未就绪”状态一致。
3. 真实放行仍需:真实 `scripts/supply-gate/.env` + PHASE-07 真机复跑 + Final Decision 签署更新。
## 6. 下一步
1. 将真实 API_BASE_URL 与短期 token 写入 `scripts/supply-gate/.env`
2. 执行:`bash scripts/ci/staging_release_pipeline.sh scripts/supply-gate/.env`
3. 使用 `staging_token_go_evidence_autofill_*.md` 草稿回填真实证据并更新 `review/final_decision_2026-03-31.md`

View File

@@ -0,0 +1,52 @@
# 规划设计对齐验证报告Checkpoint-24 / 真实参数验证回归)
- 日期2026-03-30
- 触发条件:使用真实 `API_BASE_URL + token` 执行 staging 发布流水验证
## 1. 结论
结论:**本阶段对齐未通过NO_GO。根因不是脚本执行框架而是接口契约不匹配当前 URL 指向上游提供方接口,不是 SUP-004~SUP-007 预期的平台 API。**
## 2. 对齐范围
1. `scripts/supply-gate/.env`(真实值注入)
2. `scripts/ci/staging_release_pipeline.sh`
3. `scripts/supply-gate/staging_precheck_and_run.sh`
4. `scripts/supply-gate/run_all.sh`
5. `scripts/supply-gate/sup004_accounts.sh`
6. `reports/gates/staging_release_pipeline_2026-03-30_205035.md`
7. `reports/gates/step-01_2026-03-30_205035.out.log`
8. `tests/supply/artifacts/sup004/01_verify.json`
9. `tests/supply/artifacts/sup004/02_create.json`
10. `reports/gates/superpowers_release_pipeline_2026-03-30_205037.md`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| API_BASE_URL 连通性HEAD | PASS | `step-01_2026-03-30_205035.out.log``reachable` |
| TOK-005 dry-run + M-021 readiness | PASS | `step-01_2026-03-30_205035.out.log`readiness 100% |
| SUP-004 首个业务接口返回契约可解析 JSON | FAIL | `tests/supply/artifacts/sup004/01_verify.json` 为 nginx `404 Not Found` HTML |
| staging_release_pipeline 总结果 | FAIL | `staging_release_pipeline_2026-03-30_205035.md` |
| superpowers_release_pipeline 总结果 | FAIL | `superpowers_release_pipeline_2026-03-30_205037.md` |
## 4. 根因分析
1. `sup004_accounts.sh` 固定访问:`{API_BASE_URL}/api/v1/supply/accounts/verify`
2. 当前提供的 `API_BASE_URL=https://api.minimaxi.com/anthropic`,拼接后为:
`https://api.minimaxi.com/anthropic/api/v1/supply/accounts/verify`
3. 该地址返回 HTML 404非平台契约 JSON导致 `jq` 解析失败并中断 `run_all`
4. 因此当前失败判定为:**环境地址与 SUP 契约不匹配**,并非单纯 token 占位或脚本逻辑缺陷。
## 5. 影响评估
1. 不能据此判定 token 本身有效/无效(未命中正确业务契约)。
2. 当前发布门禁链路维持 FAIL/NO_GO 是正确行为,防止误放行。
3. 若继续沿用该 URLSUP-004~007 全链路都会因契约错位失败。
## 6. 修复建议(下一步)
1. 提供“平台 SUP API 网关”基地址(应与 `/api/v1/supply/*` 契约匹配)。
2. 若目标仅验证 Minimax token请走独立“上游直连 smoke”脚本不应复用 SUP 门禁脚本。
3. 拿到正确平台地址后,重跑:
`bash scripts/ci/staging_release_pipeline.sh scripts/supply-gate/.env`

View File

@@ -0,0 +1,41 @@
# 规划设计对齐验证报告Checkpoint-25 / 开发阶段切回本地地址)
- 日期2026-03-30
- 触发条件按指示“开发阶段先用本地地址跑通Minimax URL/token 仅作开发测试参考”
## 1. 结论
结论:**本阶段对齐通过。默认执行环境已切回本地 mock开发门禁流水恢复可执行且仍保持 `CONDITIONAL_GO` 防误判。**
## 2. 对齐范围
1. `scripts/supply-gate/.env`(已切回 local-mock 值)
2. `scripts/supply-gate/.env.minimax-dev`(保留此前 Minimax 测试值)
3. `scripts/ci/staging_release_pipeline.sh`
4. `reports/gates/staging_release_pipeline_2026-03-30_212424.md`
5. `reports/gates/superpowers_stage_validation_2026-03-30_212426.md`
6. `review/outputs/tok007_release_recheck_2026-03-30_212430.md`
7. `reports/gates/staging_token_go_evidence_autofill_2026-03-30_212430.md`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| 默认 env 已可用于本地演练local mock | PASS | `staging_release_pipeline_2026-03-30_212424.md` |
| staging 三步流水STEP-01~03 | PASS | 同上报告3/3 PASS |
| superpowers 分阶段验证 | PASS决策为 `CONDITIONAL_GO` | `superpowers_stage_validation_2026-03-30_212426.md` |
| TOK-007 复审机判 | PASS机判 `CONDITIONAL_GO` | `tok007_release_recheck_2026-03-30_212430.md` |
| staging 自动回填草稿产出 | PASS | `staging_token_go_evidence_autofill_2026-03-30_212430.md` |
## 4. 说明
1. `CONDITIONAL_GO` 是预期:当前为 local/mock 演练证据,不可上调为真实 staging `GO`
2. Minimax URL/token 不能直接替代 SUP 平台契约地址(`/api/v1/supply/*`),此前已在 Checkpoint-24 记录。
3. 当前做法是:
- 开发门禁与流程联调用 local mock
- 上游 Minimax 能力验证应走独立 smoke不混入 SUP 发布门禁判定)。
## 5. 下一步
1. 需要时可新增 `scripts/supply-gate/minimax_upstream_smoke.sh`,单独校验 Minimax token 可用性。
2. 当平台 staging API 网关地址可用后,恢复真实 env 并重跑完整门禁链路。

View File

@@ -0,0 +1,37 @@
# 规划设计对齐验证报告Checkpoint-26 / Minimax 上游独立 Smoke 落地)
- 日期2026-03-30
- 触发条件:新增“上游 Minimax 独立验证”能力,避免与 SUP 发布门禁链路耦合
## 1. 结论
结论:**本阶段对齐通过。已新增独立上游 smoke 脚本并完成实测Minimax active 探测返回 200SUP 发布门禁仍保持独立判定边界。**
## 2. 对齐范围
1. `scripts/supply-gate/minimax_upstream_smoke.sh`
2. `docs/supply_gate_command_playbook_v1_2026-03-25.md`(新增第 20 节)
3. `reports/gates/minimax_upstream_smoke_2026-03-30_231930.md`
4. `tests/supply/artifacts/minimax_smoke_2026-03-30_231930/02_active_probe_body.json`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| 独立 smoke 脚本可执行(语法 + 运行) | PASS | `minimax_upstream_smoke_2026-03-30_231930.md` |
| Base 连通探测可达 | PASS | http_code=404base 探测) |
| Active 鉴权探测到达业务层并成功返回 | PASS | http_code=200见 active probe body |
| 结果分类与失败边界清晰 | PASS | 报告中 `PASS/PASS_AUTH_REACHED/FAIL_*` 规则 |
| 与 SUP-004~SUP-007 门禁链路解耦 | PASS | 命令手册第20节说明“不可替代 SUP 门禁” |
## 4. 关键说明
1. `API_BASE_URL=https://api.minimaxi.com/anthropic` 在 base 地址上返回 404 属于可预期,不影响 active 路径探测。
2. active 路径 `.../v1/messages` 返回 200说明该 token 在当前 smoke 路径下可用。
3. 该结果仅证明“上游可达 + 鉴权可用”,不等价于 SUP 平台业务契约通过。
## 5. 下一步
1. 继续默认使用 local/mock 跑 SUP 开发门禁。
2. 如需持续监控 Minimax 上游可用性,可将 `minimax_upstream_smoke.sh` 挂入定时健康检查。
3. 等平台 staging 网关地址就绪后,再执行真实 SUP 门禁闭环。

View File

@@ -0,0 +1,47 @@
# 规划设计对齐验证报告Checkpoint-27 / Minimax 监控化增强)
- 日期2026-03-30
- 触发条件:将 Minimax 上游独立 smoke 进一步纳入“可持续执行”的日常快照链路
## 1. 结论
结论:**本阶段对齐通过。已完成 Minimax smoke 判定口径修正、dry-run 能力补齐、每日快照脚本落地,满足“开发期可持续执行 + 不误入 SUP 发布门禁”的要求。**
## 2. 对齐范围
1. `scripts/supply-gate/minimax_upstream_smoke.sh`
2. `scripts/ci/minimax_upstream_daily_snapshot.sh`
3. `docs/supply_gate_command_playbook_v1_2026-03-25.md`
4. `reports/gates/minimax_upstream_smoke_2026-03-30_232510.md`
5. `reports/gates/minimax_upstream_daily_snapshot_2026-03-30.md`
6. `reports/gates/minimax_upstream_daily_snapshots.csv`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| smoke 脚本支持 `MINIMAX_SMOKE_DRY_RUN=1`(不发网络请求) | PASS | `minimax_upstream_smoke_2026-03-30_232510.md` |
| smoke 判定口径修正base=连通、active=业务状态) | PASS | `scripts/supply-gate/minimax_upstream_smoke.sh` 判定规则段 |
| 每日快照脚本可执行并产生日报 | PASS | `minimax_upstream_daily_snapshot_2026-03-30.md` |
| 每日快照 CSV 可更新覆盖当日数据 | PASS | `minimax_upstream_daily_snapshots.csv` |
| 快照默认优先引用非 dry-run 报告 | PASS | 2026-03-30 快照证据指向 `...231930.md`active=200 |
| 文档已补齐第 21 节命令与断言 | PASS | `docs/supply_gate_command_playbook_v1_2026-03-25.md` |
## 4. 关键变更
1. `minimax_upstream_smoke.sh` 新增 dry-run 模式,适配“先联调再开真实请求”的执行策略。
2. `minimax_upstream_smoke.sh` 的规则描述与结果口径保持一致,避免 404 base 探测与规则冲突。
3. 新增 `scripts/ci/minimax_upstream_daily_snapshot.sh`,支持:
- `RUN_ACTIVE_SMOKE=0`:仅汇总(默认);
- `RUN_ACTIVE_SMOKE=1`:实时探测后汇总。
4. 快照脚本默认优先选取“非 PASS_DRY_RUN”最新报告降低误判风险。
## 5. 限制与说明
1. Minimax 快照仅用于上游可达性趋势,不可替代 SUP-004~SUP-007 门禁结论。
2. 当前开发主链仍应使用 local/mock 维持持续迭代;真实 staging 仍待平台网关地址就绪。
## 6. 下一步
1. 如你同意,我可继续把 `minimax_upstream_daily_snapshot.sh` 接入 `superpowers_release_pipeline.sh` 的“可选监控步”(默认关闭)。
2. 也可新增 7 日趋势脚本(类似 M-017~019用于上游稳定性周报。

View File

@@ -0,0 +1,39 @@
# 规划设计对齐验证报告Checkpoint-28 / Minimax 趋势与流水可选监控接入)
- 日期2026-03-30
- 触发条件:在不改变 SUP 主门禁判定边界的前提下,补齐 Minimax 上游 7 日趋势能力,并将其接入总控流水(默认关闭、非阻断)。
## 1. 结论
结论:**本阶段对齐通过。Minimax 日快照 + 7 日趋势链路已可执行,且已通过 `superpowers_release_pipeline` 的可选监控步验证。**
## 2. 对齐范围
1. `scripts/ci/minimax_upstream_trend_report.sh`(新增)
2. `scripts/ci/superpowers_release_pipeline.sh`(新增 STEP-05 可选监控步)
3. `docs/supply_gate_command_playbook_v1_2026-03-25.md`(新增第 22 节与可选开关说明)
4. `reports/gates/minimax_upstream_trend_7d_2026-03-30.md`
5. `reports/gates/superpowers_release_pipeline_2026-03-30_235224.md`
6. `reports/gates/step-05_2026-03-30_235224.out.log`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| Minimax 7 日趋势脚本可执行并产出报告 | PASS | `reports/gates/minimax_upstream_trend_7d_2026-03-30.md` |
| 趋势脚本在样本不足时给出 `INSUFFICIENT_DATA` 而非误报 PASS_7D | PASS | 同上(采样 1 天) |
| 总控流水支持 `ENABLE_MINIMAX_MONITORING=1` 时执行 STEP-05 | PASS | `reports/gates/superpowers_release_pipeline_2026-03-30_235224.md` |
| STEP-05 失败不阻断主门禁(非阻断监控定位) | PASS逻辑校验 | `scripts/ci/superpowers_release_pipeline.sh` |
| 新增命令文档与断言说明齐全 | PASS | `docs/supply_gate_command_playbook_v1_2026-03-25.md` |
## 4. 执行记录说明
1. 首次在受限沙箱直接执行总控流水时,`STEP-01` 因无法绑定本地 `127.0.0.1:18080`(环境权限限制)失败。
2. 在允许非沙箱执行后复跑,同一代码版本下 `STEP-01~STEP-05` 全部 PASS。
3. 由此可判定失败原因为执行环境权限,不是本次代码改动引入的功能回归。
## 5. 下一步
1. 继续按 `docs/plans/2026-03-30-superpowers-execution-tasklist-v2.md` 推进 `Batch-STG-01`(真实 staging 解锁)。
2. 按日执行第 21 节快照,累计满 7 天后复跑第 22 节趋势,支撑 `F-03` 连续观测闭环。

View File

@@ -0,0 +1,51 @@
# 规划设计对齐验证报告Checkpoint-29 / STG 本地演练恢复)
- 日期2026-03-31
- 触发条件:继续执行 STG 批次(本机开发测试口径),修复 PHASE-10 阻塞后复跑整条流水。
## 1. 结论
结论:**本阶段对齐通过。STG 本地演练流水已恢复 `PASS`,并明确保持 `local/mock` 与真实 staging 放行证据边界。**
## 2. 对齐范围
1. `scripts/ci/token_runtime_readiness_check.sh`
2. `reports/gates/staging_release_pipeline_2026-03-31_100116.md`
3. `reports/gates/superpowers_release_pipeline_2026-03-31_100120.md`
4. `reports/gates/superpowers_stage_validation_2026-03-31_100120.md`
5. `review/outputs/tok007_release_recheck_2026-03-31_100127.md`
## 3. 问题与修复
### 3.1 发现的问题
1. `PHASE-10`M-021`ENABLE_TOKEN_RUNTIME_SMOKE=1` 场景下失败。
2. 根因一:默认 smoke 端口 `18082``supply-api` 占用,冒烟请求命中错误服务(`issue` 返回 404
3. 根因二:脚本 smoke 分支使用 `exit 1` 直接退出,失败时无法稳定产出完整汇总输出。
### 3.2 修复动作
1. 为 M-021 冒烟新增端口自动避让:从基准端口起寻找可用端口(最多 50 次)。
2. 将 smoke 执行块改为子 Shell 返回码模型,保留失败但不中断总报告生成流程。
## 4. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| M-021 脚本修复后可执行 | PASS | `reports/gates/token_runtime_readiness_2026-03-31_100017.md` |
| Superpowers 阶段验证恢复通过PHASE-10 PASS | PASS | `reports/gates/superpowers_stage_validation_2026-03-31_100120.md` |
| Superpowers 发布流水恢复通过 | PASS | `reports/gates/superpowers_release_pipeline_2026-03-31_100120.md` |
| STG 本地演练流水 STEP-01~03 全 PASS | PASS | `reports/gates/staging_release_pipeline_2026-03-31_100116.md` |
| TOK-007 复审结论保持 `CONDITIONAL_GO`(未误升 GO | PASS | `review/outputs/tok007_release_recheck_2026-03-31_100127.md` |
## 5. 结论边界说明
1. 本次通过仅代表 `local/mock` 演练链路恢复,不等价真实 staging 放行。
2. `F-01/F-02/F-04` 的真实 staging 证据要求仍保持不变。
## 6. 下一步
1. 进入 STG-001替换真实 `API_BASE_URL` 并完成可达性验证。
2. 进入 STG-002注入真实短期 token 并复跑 `staging_release_pipeline.sh`(真实环境)。
3. 完成 STG-004将真实证据回填至 `review/final_decision_2026-03-31.md``reports/supply_gate_review_2026-03-31.md`

View File

@@ -0,0 +1,36 @@
# 规划设计对齐验证报告Checkpoint-30 / STG 端口基线固化)
- 日期2026-03-31
- 触发条件:按“先清理本机冲突进程并固化端口基线”继续执行 STG 批次。
## 1. 结论
结论:**本阶段对齐通过。蚊子残留与关键冲突进程已清理STG 本地演练在清理后可稳定复现 PASS。**
## 2. 对齐范围
1. `reports/gates/local_dev_port_baseline_2026-03-31.md`
2. `reports/gates/staging_release_pipeline_2026-03-31_100942.md`
3. `reports/gates/superpowers_release_pipeline_2026-03-31_100943.md`
4. `scripts/ci/token_runtime_readiness_check.sh`(沿用 Checkpoint-29 修复)
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| 蚊子残留端口 `5176/5177/8080` 已释放 | PASS | `reports/gates/local_dev_port_baseline_2026-03-31.md` |
| M-021 历史冲突端口 `18082` 已释放 | PASS | 同上 |
| 清理后 STG 本地流水可通过 | PASS | `reports/gates/staging_release_pipeline_2026-03-31_100942.md` |
| 清理后 Superpowers 总控可通过 | PASS | `reports/gates/superpowers_release_pipeline_2026-03-31_100943.md` |
| 结论边界保持(未误升为真实 staging GO | PASS | `LOCAL_MOCK` 标记 + `CONDITIONAL_GO` 链路 |
## 4. 说明
1. 端口 `3000` 仍被占用,但不在 STG 本地演练关键端口集内,当前不构成阻塞。
2. 本次结果仅覆盖“本机开发测试口径”;真实 staging 放行仍依赖 `STG-001/STG-002`
## 5. 下一步
1. 你确认真实 staging 地址后,我直接执行 `STG-001`
2. 你提供短期 token 后,我直接执行真实 `STG-002/003/004` 并回填最终决议证据。

View File

@@ -0,0 +1,37 @@
# 规划设计对齐验证报告Checkpoint-31 / 真实 STG 就绪度自动化)
- 日期2026-03-31
- 触发条件:继续执行实施计划,在“外网 STG 暂未申请”条件下补齐真实放行前置检查自动化。
## 1. 结论
结论:**本阶段对齐通过。已新增“真实 STG 就绪度检查”能力,并已在当前本地配置下正确判定为 `BLOCKED`。**
## 2. 对齐范围
1. `scripts/ci/generate_local_staging_env.sh`(一键生成本地 `.env.staging-real`
2. `scripts/ci/staging_real_readiness_check.sh`(真实 STG 前置检查)
3. `docs/supply_gate_command_playbook_v1_2026-03-25.md`(新增第 23/24 节)
4. `reports/gates/local_staging_env_generation_2026-03-31_105620.md`
5. `reports/gates/staging_real_readiness_2026-03-31_110213.md`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| 一键脚本可生成 owner/viewer/admin token 并写入 env | PASS | `local_staging_env_generation_2026-03-31_105620.md` |
| 生成 env 可直接用于本地 STG 流水 | PASS | `staging_release_pipeline_2026-03-31_105633.md` |
| 真实 STG 就绪度脚本可执行并生成报告 | PASS | `staging_real_readiness_2026-03-31_110213.md` |
| 当前配置下(本地 URL被判定 `BLOCKED` | PASS预期 | `STG-RDY-004/008` 失败项 |
| 命令手册完成同步 | PASS | `docs/supply_gate_command_playbook_v1_2026-03-25.md` |
## 4. 当前阻塞结论
1. `API_BASE_URL` 仍是本地地址(`127.0.0.1`),不满足真实 STG 放行前提。
2. 未申请外网地址前,实施计划只能继续按 local/mock 开发测试口径推进。
## 5. 下一步
1. 外网 STG 地址可用后,更新 `.env.staging-real` 并重跑 `staging_real_readiness_check.sh`,目标从 `BLOCKED` 转为 `READY`
2. 通过就绪检查后执行真实 `staging_release_pipeline.sh`,并回填 `F-01/F-02/F-04` 证据闭环。

View File

@@ -0,0 +1,39 @@
# 规划设计对齐验证报告Checkpoint-32 / 完整开发测试续跑)
- 日期2026-03-31
- 触发条件:用户确认继续完成项目完整开发测试,执行本地 STG 全链路续跑并复核真实 STG 前置状态。
## 1. 结论
结论:**本阶段对齐通过。本地完整开发测试链路稳定 PASS真实 STG 放行前置仍为 `BLOCKED`,结论边界保持一致。**
## 2. 对齐范围
1. `scripts/ci/generate_local_staging_env.sh`
2. `scripts/ci/staging_release_pipeline.sh`
3. `scripts/ci/staging_real_readiness_check.sh`
4. `scripts/supply-gate/minimax_upstream_smoke.sh`
5. `docs/plans/2026-03-30-superpowers-execution-tasklist-v2.md`
6. `reports/superpowers_execution_progress_2026-03-27.md`
## 3. 核查结果
| 核查项 | 结果 | 证据 |
|---|---|---|
| `.env.staging-real` 已重新生成并包含 owner/viewer/admin 三类 token | PASS | `reports/gates/local_staging_env_generation_2026-03-31_123102.md` |
| 本地 STG 发布流水可重复通过 | PASS | `reports/gates/staging_release_pipeline_2026-03-31_123148.md` |
| Superpowers 全链路与 TOK-007 复审可重复通过 | PASS | `reports/gates/superpowers_release_pipeline_2026-03-31_123150.md` + `review/outputs/tok007_release_recheck_2026-03-31_123153.md` |
| 真实 STG 就绪检查准确识别当前阻塞 | PASS预期 | `reports/gates/staging_real_readiness_2026-03-31_123159.md` |
| Minimax 上游可达与鉴权调用保持通过 | PASS | `reports/gates/minimax_upstream_smoke_2026-03-31_123210.md` |
## 4. 阻塞与边界
1. `STG-RDY-004` 未关闭:`API_BASE_URL` 当前是本地地址 `http://127.0.0.1:18080`
2. `STG-RDY-008` 未关闭:真实 STG 可达性探测仍失败(`http_code=000`)。
3.`F-01/F-02/F-04` 仍未关闭,本轮不得上调到真实 `GO`,当前仅可维持 `CONDITIONAL_GO`(开发口径)。
## 5. 下一步
1.`.env.staging-real``API_BASE_URL` 切换到可达的真实 STG 地址(内网或公网均可)。
2. 注入真实环境可用的 owner/viewer/admin 平台 token复跑 `staging_real_readiness_check.sh`,目标 `READY`
3. 就绪后执行真实口径 `staging_release_pipeline.sh`(不带 `ALLOW_LOCAL_MOCK_STAGING=1`),回填 `F-01/F-02/F-04` 证据。

View File

@@ -0,0 +1,82 @@
audit_events_pkey
auth_platform_api_keys_pkey
auth_platform_api_keys_tenant_id_key_prefix_key
billing_accounts_pkey
billing_accounts_tenant_id_project_id_account_type_key
billing_ledger_entries_pkey
billing_ledger_entries_tenant_id_request_id_entry_type_key
core_projects_pkey
core_projects_tenant_id_project_code_key
core_tenants_pkey
core_tenants_tenant_code_key
iam_users_pkey
iam_users_tenant_id_email_key
idx_audit_events_request_id
idx_audit_events_result_code
idx_audit_events_tenant_domain_time
idx_audit_events_trace_id
idx_auth_platform_api_keys_last_used_at
idx_auth_platform_api_keys_project_status
idx_auth_platform_api_keys_tenant_status
idx_billing_accounts_tenant_status
idx_billing_ledger_entries_account_time
idx_billing_ledger_entries_tenant_time
idx_billing_ledger_entries_trace_id
idx_core_projects_tenant_status
idx_core_tenants_plan_code
idx_core_tenants_status
idx_iam_users_tenant_role
idx_iam_users_tenant_status
idx_routing_policies_model_pattern
idx_routing_policies_tenant_project_status
idx_security_kms_key_registry_status
idx_supply_accounts_platform
idx_supply_accounts_platform_status_updated
idx_supply_accounts_risk_level
idx_supply_accounts_status
idx_supply_accounts_user_id
idx_supply_accounts_user_status_updated
idx_supply_earnings_earned_at
idx_supply_earnings_source_request_id
idx_supply_earnings_status
idx_supply_earnings_user_id
idx_supply_earnings_user_status_available
idx_supply_orders_buyer_status_created
idx_supply_orders_buyer_user_id
idx_supply_orders_request_id
idx_supply_orders_status
idx_supply_orders_supplier_status_created
idx_supply_orders_supplier_user_id
idx_supply_orders_supply_package_id
idx_supply_packages_active_lookup
idx_supply_packages_platform_model
idx_supply_packages_platform_model_status
idx_supply_packages_status
idx_supply_packages_supply_account_id
idx_supply_packages_user_id
idx_supply_packages_user_status_updated
idx_supply_settlements_period
idx_supply_settlements_request_id
idx_supply_settlements_status
idx_supply_settlements_user_id
idx_supply_settlements_user_status_updated
idx_supply_usage_records_order_id
idx_supply_usage_records_order_started
idx_supply_usage_records_platform_model
idx_supply_usage_records_request_id
idx_supply_usage_records_started_at
idx_supply_usage_records_supplier_started
idx_supply_usage_records_supply_account_id
idx_supply_usage_records_trace_id
routing_policies_pkey
security_kms_key_registry_key_alias_key
security_kms_key_registry_pkey
supply_accounts_pkey
supply_earnings_pkey
supply_orders_order_no_key
supply_orders_pkey
supply_packages_pkey
supply_settlements_pkey
supply_settlements_settlement_no_key
supply_usage_records_pkey
uq_supply_settlements_user_processing

View File

@@ -0,0 +1,37 @@
audit_events.idempotency_key
audit_events.request_id
billing_accounts.currency_code
billing_ledger_entries.amount_unit
billing_ledger_entries.currency_code
billing_ledger_entries.idempotency_key
billing_ledger_entries.request_id
routing_policies.version
supply_accounts.audit_trace_id
supply_accounts.credential_cipher_algo
supply_accounts.credential_fingerprint
supply_accounts.credential_key_version
supply_accounts.credential_kms_key_alias
supply_accounts.currency_code
supply_accounts.quota_unit
supply_accounts.version
supply_earnings.amount_unit
supply_earnings.audit_trace_id
supply_earnings.version
supply_orders.audit_trace_id
supply_orders.currency_code
supply_orders.idempotency_key
supply_orders.quota_unit
supply_orders.request_id
supply_orders.version
supply_packages.audit_trace_id
supply_packages.currency_code
supply_packages.price_unit
supply_packages.quota_unit
supply_packages.version
supply_settlements.amount_unit
supply_settlements.audit_trace_id
supply_settlements.currency_code
supply_settlements.idempotency_key
supply_settlements.request_id
supply_settlements.version
supply_usage_records.request_id

View File

@@ -0,0 +1,15 @@
audit_events
auth_platform_api_keys
billing_accounts
billing_ledger_entries
core_projects
core_tenants
iam_users
routing_policies
security_kms_key_registry
supply_accounts
supply_earnings
supply_orders
supply_packages
supply_settlements
supply_usage_records

View File

@@ -0,0 +1,71 @@
# 数据库基线执行验证报告2026-03-27
- 执行环境PostgreSQL 15 (`127.0.0.1:34603`)
- 执行账号:`mosquito`
- 验证库:`lijiaoqiao_design_review_20260327`
- 执行人Codex
---
## 1. 执行 SQL 清单
1. `sql/postgresql/platform_core_schema_v1.sql`
2. `sql/postgresql/supply_schema_v1.sql`
3. `sql/postgresql/supply_schema_v1_patch_2026-03-27.sql`
原始日志:
1. `reports/db/sql_apply_2026-03-27.log`
---
## 2. 执行结果
1. 三份 SQL 均执行成功(全部到 `COMMIT`)。
2. 表总数:`15`
3. 索引总数:`82`
4. 关键字段命中数:`37`
结构快照:
1. `reports/db/tables_2026-03-27.txt`
2. `reports/db/indexes_2026-03-27.txt`
3. `reports/db/key_columns_2026-03-27.txt`
---
## 3. 关键验收点核对
1. 跨域核心表Core/IAM/Auth/Billing/Routing/Security/Audit已创建。
2. 供应域 patch 中加密字段已生效:
- `credential_cipher_algo`
- `credential_kms_key_alias`
- `credential_key_version`
- `credential_fingerprint`
3. 单位字段已生效:
- `quota_unit`
- `price_unit`
- `amount_unit`
- `currency_code`
4. 审计与幂等字段已生效:
- `request_id`
- `idempotency_key`
- `audit_trace_id`
- `version`
5. 关键组合索引与部分索引已创建(含 `uq_supply_settlements_user_processing`)。
---
## 4. 问题与修复记录
1. 首次执行失败原因:新增 SQL 文件字符串默认值引号丢失。
2. 修复动作:重写 `platform_core_schema_v1.sql``supply_schema_v1_patch_2026-03-27.sql`,统一字符串字面量语法。
3. 修复后复跑结果:全部通过。
---
## 5. 结论
结论:**通过(设计层 SQL 可执行)**。
后续建议:
1. 在目标测试环境执行同样脚本并对比 `EXPLAIN` 计划。
2. 将执行日志纳入 `SUP-008``GO` 决策证据包。

View File

@@ -0,0 +1,16 @@
# Dependency Compatibility Matrix2026-03-27
- Audit-Status: PASS
| Component | Baseline | Current | Result | Note |
|---|---|---|---|---|
| Go | 1.21.x | 1.21.x文档基线 | PASS | 与架构基线一致 |
| PostgreSQL | 15.x | 15.xSQL 语法) | PASS | DDL 在 PG15 实测通过 |
| Redis | 7.x | 7.x文档基线 | PASS | 与架构基线一致 |
| subapi | X.Y.Z fixed | 未变更 | PASS | 无依赖升级 |
| Frontend Node | 20.x LTS | 未变更 | PASS | 无依赖升级 |
## Conclusion
1. 本次无 runtime 依赖变更。
2. 兼容性审计结果可放行。

View File

@@ -0,0 +1,10 @@
# Dependency Audit Check Result (2026-03-27)
- Result: PASS
- M-017 (`dependency_compat_audit_pass_pct`): 100%
- Checked files:
1. reports/dependency/sbom_2026-03-27.spdx.json
2. reports/dependency/lockfile_diff_2026-03-27.md
3. reports/dependency/compat_matrix_2026-03-27.md
4. reports/dependency/risk_register_2026-03-27.md

View File

@@ -0,0 +1,15 @@
# Lockfile Diff2026-03-27
- Audit-Status: PASS
- Scope: Baseline document-only sync
## Summary
1. `go.mod/go.sum`:无本次变更。
2. `package-lock.json` / `pnpm-lock.yaml`:无本次变更。
3. `pom.xml`:无本次变更。
## Risk
1. 本次提交仅含文档与 SQL不涉及应用依赖升级。
2. 依赖风险等级Low。

View File

@@ -0,0 +1,14 @@
# Dependency Risk Register2026-03-27
- Audit-Status: PASS
| Risk ID | Risk | Severity | Mitigation | Owner | Status |
|---|---|---|---|---|---|
| DEP-R-001 | 未锁定 subapi 精确版本导致回归 | High | 固定 `X.Y.Z` + 三重Gate | ARCH | Open |
| DEP-R-002 | 锁文件漂移未触发审计 | Medium | CI 强制执行 dependency-audit-check | PLAT | Open |
| DEP-R-003 | 漏洞库更新导致新 Critical CVE | High | 夜间扫描 + 发布阻断 | SEC | Open |
## Conclusion
1. 当前无新增依赖变更触发的阻断项。
2. 风险条目已登记并进入持续治理。

View File

@@ -0,0 +1,32 @@
{
"spdxVersion": "SPDX-2.3",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "lijiaoqiao-dependency-sbom-2026-03-27",
"documentNamespace": "https://lijiaoqiao.local/sbom/2026-03-27",
"creationInfo": {
"created": "2026-03-27T12:00:00Z",
"creators": [
"Tool: codex-manual-baseline"
]
},
"packages": [
{
"SPDXID": "SPDXRef-Package-Go",
"name": "go-runtime",
"versionInfo": "1.21.x",
"downloadLocation": "NOASSERTION"
},
{
"SPDXID": "SPDXRef-Package-PostgreSQL",
"name": "postgresql",
"versionInfo": "15.x",
"downloadLocation": "NOASSERTION"
},
{
"SPDXID": "SPDXRef-Package-Redis",
"name": "redis",
"versionInfo": "7.x",
"downloadLocation": "NOASSERTION"
}
]
}

View File

@@ -0,0 +1,14 @@
# 需求-设计-测试漂移日检2026-03-30-debug
- 状态:**PASS**
- 依据M-019=100.00%(目标=100%
## 检查结论
1. 若 M-019 < 100%,判定存在追踪漂移风险。
2. 当前说明tracked_rows=15/15
## 处理动作
1. 若 FAIL24h 内补齐缺失追踪项并复跑本脚本。
2. 若 PASS纳入 7 日趋势统计。

View File

@@ -0,0 +1,14 @@
# 需求-设计-测试漂移日检2026-03-30
- 状态:**PASS**
- 依据M-019=100.00%(目标=100%
## 检查结论
1. 若 M-019 < 100%,判定存在追踪漂移风险。
2. 当前说明tracked_rows=15/15
## 处理动作
1. 若 FAIL24h 内补齐缺失追踪项并复跑本脚本。
2. 若 PASS纳入 7 日趋势统计。

View File

@@ -0,0 +1,14 @@
# 需求-设计-测试漂移日检2026-03-31
- 状态:**PASS**
- 依据M-019=100.00%(目标=100%
## 检查结论
1. 若 M-019 < 100%,判定存在追踪漂移风险。
2. 当前说明tracked_rows=15/15
## 处理动作
1. 若 FAIL24h 内补齐缺失追踪项并复跑本脚本。
2. 若 PASS纳入 7 日趋势统计。

View File

@@ -0,0 +1,25 @@
# 门禁指标与报告一致性检查2026-03-27
- 检查范围:
- `docs/acceptance_gate_single_source_v1_2026-03-18.md`
- `reports/supply_gate_review_2026-03-31.md`
- `review/final_decision_2026-03-31.md`
- `review/prd_tech_planning_recheck_v3_2026-03-27.md`
## 1. 总体结论
结论:**主要一致,存在 1 项历史引用缺口待清理。**
## 2. 检查结果
| 项目 | 结果 | 说明 |
|---|---|---|
| M-013~M-016 在 SUP 报告与最终决议均有体现 | PASS | 口径一致,均标记为 mock 有条件通过 |
| `NO-GO` 决策与 staging 阻塞状态一致 | PASS | 与 D/E 阶段阻塞报告一致 |
| M-017~M-019 在复检与最终决议均有体现 | PASS | 口径一致连续7天证据未齐 |
| M-021token 运行态门禁)是否在决议表中显式核对 | PASS | 已补入最终决议与 SUP 风险项 |
| 链接完整性检查是否全绿 | FAIL | 存在历史任务文档引用未落地条目,详见 `reports/link_integrity_check_2026-03-27.md` |
## 3. 修复建议
1. 将链接检查中的“未落地引用”拆分为 backlog 并标注 owner。

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_123320
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_122908.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_122907.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_123622
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_122908.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_122907.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_145306
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_145306.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_145305.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_145749
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_145306.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_145305.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_151609
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_145749.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_151555.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_151621
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_151621.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_151555.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_151838
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_151838.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_151821.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_154104
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_154104.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_154103.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_155729
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_155729.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_155727.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_160041
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_160041.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_160039.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_160246
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_160246.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_160244.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_161011
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_161011.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_161009.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_173342
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_173342.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_173339.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_173713
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_173713.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_173339.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_173728
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_173728.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_173726.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_181925
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_181925.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_173726.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

View File

@@ -0,0 +1,26 @@
# Final Decision Consistency Check
- 时间戳2026-03-30_181927
- 执行脚本:`scripts/ci/final_decision_consistency_check.sh`
## 1. 输入源
| 来源 | 路径 | 解析结论 |
|---|---|---|
| final_decision | /home/long/project/立交桥/review/final_decision_2026-03-31.md | NO_GO |
| tok007_recheck | /home/long/project/立交桥/review/outputs/tok007_release_recheck_2026-03-30_181927.md | CONDITIONAL_GO |
| superpowers_stage_validation | /home/long/project/立交桥/reports/gates/superpowers_stage_validation_2026-03-30_181925.md | CONDITIONAL_GO |
## 2. 一致性结果
- 状态:**WARN**
- 说明final signed decision lags latest machine recheck; requires manual review update
## 3. 建议动作
1. 若状态为 WARN人工确认是否需要更新 `review/final_decision_2026-03-31.md` 的勾选与签署记录。
2. 若状态为 FAIL先修复报告来源或解析格式再重新执行本检查。
3. staging 真值就绪后,按顺序重跑:
1. `scripts/ci/superpowers_stage_validate.sh`
2. `scripts/ci/tok007_release_recheck.sh`
3. `scripts/ci/final_decision_consistency_check.sh`

Some files were not shown because too many files have changed in this diff Show More