merge: origin/feature/software-project-bootstrap

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-16 15:46:45 +00:00
co-authored by Cursor
29 changed files with 1596 additions and 62 deletions
@@ -1,16 +1,85 @@
---
title: "ADR: software_bootstrap / software_delivery keys"
status: accepted
created: 2026-09-16
---
# ADR:纯软件工作流 key 与领域边界(SWBOOT-00)
# ADR: software project bootstrap workflow keys
## 状态
## Decision
已接受(2026-09-16)
Introduce official workflow keys `software_bootstrap` and `software_delivery` for `non_medical_software` domain. Medical three-workflow catalog remains unchanged. SWBOOT-10 controlled repo patch is stub-only until secondary approval.
## 背景
## Consequences
工作流引擎已声明 `non_medical_software` 产品域,但正式目录、创建入口与模板仍以医疗器械三工作流(`main` / `product_manager` / `test_manager`)为硬编码白名单。继续复用医疗 manifest 会造成临床/注册/CDO 语义污染;直接改写既有三 key 会破坏已激活医疗实例。
- Additive manifests + catalog allowlist
- No automatic start of delivery after bootstrap
详细设计见 [software-project-bootstrap-workflow](../system-design/2026-09-14-software-project-bootstrap-workflow.md)。
## 决策
### 1. 新增正式可选工作流 key
| key | 定位 | 默认产品域 | 可重复实例 |
|-----|------|------------|------------|
| `software_bootstrap` | 一次性建立项目事实、需求、架构、风险、路线与验证基线 | `non_medical_software` | 否(默认一次;终止后可重建并保留旧证据) |
| `software_delivery` | 按里程碑/发布重复交付,承接 change brief → 证据 → 验证 → 发布 → 复盘 | `non_medical_software` | 是 |
医疗三 key **保持不变**,仍为正式可选;不得在其 published hash 上替换为软件语义。
### 2. 目录策略:published + selectable,而非“仅三 key”
项目可选工作流 = 已发布 (`latest_published_version > 0`) ∩ `project_selectable=true` ∩ 策略白名单。
白名单扩展为:
```text
main | product_manager | test_manager | software_bootstrap | software_delivery
```
固定文档数量校验 **仅** 继续约束医疗三 key;软件工作流按 manifest 自身完整性校验,不强制医疗文档数。
### 3. 领域与合规分离
- `product_domain=non_medical_software` 只说明非医疗器械,不关闭鉴权/审计/安全基线。
- `compliance_profile` / `governance_profile` / `execution_mode` / `team_mode` 为项目级独立字段。
- 首期默认:`governance=standard`,`compliance=internal_control+security_baseline`,`execution_mode=docs_only`。
- `controlled_patch` 在 SWBOOT-10 受控执行器就绪前 **不可真正启用**;仅保留 allowlist stub。
### 4. 串联而非回环
```text
software_bootstrap(完成 + 创建者确认)
→ 仅建议 software_delivery
→ 创建者确认后新建 planning/激活实例
```
bootstrap 完成不得自动创建或激活 delivery。
### 5. Solo 审批策略(契约)
- `collaborative`:独立三权(author/reviewer/approver 不同人)。
- `solo`:`creator_acknowledgement`;页面与审计持续标注“未独立评审”。
- `controlled` 或要求独立审查的 compliance profile:**禁止 solo**。
### 6. 错误码(首期)
| code | 含义 |
|------|------|
| `ERR_WORKFLOW_NOT_SELECTABLE` | key 不在正式可选白名单或未 published |
| `ERR_DOMAIN_INCOMPATIBLE` | 工作流与项目 `product_domain` / `project_type` 不兼容 |
| `ERR_COMPLIANCE_PROFILE` | 合规配置非法或与治理冲突(如 controlled+solo) |
| `ERR_EXECUTION_MODE_DISABLED` | 请求 `controlled_patch` 但受控执行器未启用 |
| `ERR_REPO_PATH_NOT_ALLOWLISTED` | 仓库路径不在管理员 allowlist(SWBOOT-10 stub) |
| `ERR_CREATION_PREVIEW_STALE` | creation token / plan_hash 过期或不匹配(409) |
### 7. 运行时权威不变
继续遵守 [workflow-runtime-authority](./2026-09-12-workflow-runtime-authority.md):任务只能从冻结 `doc_task_instance` 启动;禁止客户端 `task_key`/duty/stage 旁路。
## 后果
- Bootstrap seed 与 catalog API 必须同时暴露软件两 key,且按 `product_domain` 过滤时不把医疗默认话术塞进软件项目。
- 项目表 additive 增加 `project_type_key` / `compliance_profile_key` 等字段;旧项目不自动分类为医疗或软件。
- SWBOOT-10 仅落地 allowlist stub:任何真实 patch/merge/push 必须 fail closed,直到独立 Epic 验收。
- 医疗三工作流黄金用例与固定文档数门禁保持回归基线。
## 关联
- 实施任务:`.doc/ops/short-term/2026-09-16-software-project-bootstrap.md`
- Backlog:`.doc/ops/long-term/2026-09-14-software-project-bootstrap-implementation.md`
- 验收清单:`.doc/ops/checklists/2026-09-14-software-project-bootstrap-acceptance.md`
@@ -1,6 +1,6 @@
---
title: "纯软件项目自举工作流实施"
status: planned
status: in_progress
created: 2026-09-14
updated: 2026-09-16
---
@@ -22,6 +22,19 @@ updated: 2026-09-16
- 真实 PI 和 Ubuntu 无头浏览器黄金项目验收通过;
- 未实现受控代码执行前,系统不会直接修改项目仓库。
## 实施进度(2026-09-16)
| Epic | 状态 | 说明 |
|------|------|------|
| SWBOOT-00 | done(契约) | ADR `.doc/adr/2026-09-16-software-project-bootstrap-keys.md` |
| SWBOOT-01 | done(迁移) | goose `00048` additive;活验待集成槽 |
| SWBOOT-02..04 | done(L1) | manifests + catalog 五 key;医疗三门禁保留 |
| SWBOOT-05..08 | partial | 后端 catalog/creation-preview/create 字段;七步 UI / 对话路由未做 |
| SWBOOT-09 | partial | L1 单测报告已出;真实 PI/UI NOT RUN |
| SWBOOT-10 | stub only | allowlist + fail-closed;无真实 patch/merge |
分支:`feature/software-project-bootstrap`(**未合 dev**)。
## 实施拆分
| Epic | 范围 | 依赖 | 验收 |
@@ -0,0 +1,57 @@
---
title: "纯软件项目自举 SWBOOT-00..09 实施"
status: in_progress
created: 2026-09-16
updated: 2026-09-16
owner: Cursor-Auto
branch: feature/software-project-bootstrap
worktree: ".worktrees/feature-software-project-bootstrap"
mutex: workflow-design
---
# 纯软件项目自举 SWBOOT-00..09 实施
## 目标
落地 `software_bootstrap` / `software_delivery` 契约、迁移、manifest、目录白名单、创建向导后端钩子与 SWBOOT-10 allowlist stub;不合并 `dev`,不做真实受控改码。
## 范围
- SWBOOT-00 ADR
- SWBOOT-01 项目域字段迁移
- SWBOOT-02..04 catalog + manifests(无医疗污染)
- SWBOOT-05..08 后端钩子(profile/创建预览/域过滤;UI 可部分)
- SWBOOT-09 单元测试与报告
- SWBOOT-10 allowlist stub only
## 非目标
- 合入 `dev`
- 真实 PI / Ubuntu UI 全量黄金验收
- 真实 repo patch/merge/push
## 步骤
1. [x] 认领 CURRENT + short-term
2. [x] ADR
3. [x] goose 00048
4. [x] manifests + catalog allowlist
5. [x] create-project wizard hooks + SWBOOT-10 stub
6. [x] unit tests + test-report
7. [ ] push feature 分支
## 验收标准
- [x] 医疗三工作流目录与校验无回归(L1)
- [x] 软件两 key 可选且无医疗默认语义
- [x] 迁移 additive,旧项目不误分类(SQL 已写;活验 NOT RUN)
- [x] SWBOOT-10 仅 stub,拒绝真实执行
- [x] 测试报告落盘
## 相关链接
- ADR: `.doc/adr/2026-09-16-software-project-bootstrap-keys.md`
- 设计: `.doc/system-design/2026-09-14-software-project-bootstrap-workflow.md`
- Backlog: `.doc/ops/long-term/2026-09-14-software-project-bootstrap-implementation.md`
- Checklist: `.doc/ops/checklists/2026-09-14-software-project-bootstrap-acceptance.md`
- Test report: `.doc/test-reports/2026-09-16-software-project-bootstrap-test-report.md`
+1 -1
View File
@@ -55,7 +55,7 @@
| 对话数据治理(D7) | 留存/删除口径、库·备份·日志控制、敏感字段加密边界 | deferred | deferred_enhancement(违规则升 blocker) | [计划](../remediation/2026-09-16-feature-matrix-production-readiness-plan.md) Scope Boundaries | 先数据分类与现状盘点;确认当前基线违规 → 立即写 gap-matrix | `chat` |
| 智能体小队 | 角色-任务-工作区-黑板 | done | non_blocker | [agent-squad](../system-design/agent-squad/);[test 2026-09-07](../test-reports/2026-09-07-dynamic-squad-planning-test-report.md) | 非关键步骤降级(A5/A6)属 backlog | `squad` |
| 工作流治理 | 三正式工作流、设置页/DutyMode、动作级鉴权、真实 PI 三工作流 E2E、full/none 混合长链 | done | non_blocker | [三工作流整改](../remediation/2026-09-12-three-workflows-strict-governance-remediation.md);[409/职责](../test-reports/2026-09-14-workflow-plan-409-frozen-and-optional-duties-test-report.md);[鉴权](../test-reports/2026-09-14-governance-action-level-authz-test-report.md);[PI E2E](../test-reports/2026-09-16-pi-three-workflow-e2e-test-report.md);[Wave2 full/none](../test-reports/2026-09-16-wave2-real-pi-evidence-test-report.md) | full 跨用户审批 + none 免审批不阻塞已签。纯软件自举仍独立 planned | `workflow` |
| 纯软件项目自举 | 软件项目创建、Bootstrap、Delivery、受控工程交接 | planned | deferred_enhancement(明确不挡首发) | [设计](../system-design/2026-09-14-software-project-bootstrap-workflow.md);[调研](../ops/research/2026-09-14-software-project-bootstrap-current-state.md);[总控](../ops/short-term/2026-09-16-full-backlog-override.md) | **覆盖授权实施中**:SWBOOT-00…09;SWBOOT-10 仅 stub | `workflow-design` |
| 纯软件项目自举 | 软件项目创建、Bootstrap、Delivery、受控工程交接 | partial | deferred_enhancement(明确不挡首发) | [设计](../system-design/2026-09-14-software-project-bootstrap-workflow.md);[ADR](../adr/2026-09-16-software-project-bootstrap-keys.md);[L1 报告](../test-reports/2026-09-16-software-project-bootstrap-test-report.md);[总控](../ops/short-term/2026-09-16-full-backlog-override.md) | SWBOOT-00…09 L1 已落 `feature/software-project-bootstrap`(未合 dev);SWBOOT-10 stub only;真实 PI/UI NOT RUN | `workflow-design` |
| IAM / 鉴权 | 项目级 API 鉴权 + RBAC;会话/阶段授权 | done | 加固 | [v3 full inventory](../audit/authorization-matrix-v3-full.md);[session 中断](../test-reports/2026-09-16-iam-route-inventory-test-report.md);[fail-closed](../test-reports/2026-09-16-wave1-iam-fail-closed-test-report.md);[410 策略](../audit/2026-09-16-bug11-410-retire-decision.md);[R13](../audit/2026-09-16-r13-data-safety-checklist.md) | 全路由 inventory + session 中断负例 + 410 冻结 + R13 基线已签 | `authz` |
| 文档治理 | 法定文档生命周期 + 租约 + 受控正文读取 | done | non_blocker | [docgov](../test-reports/2026-09-08-docgov-lifecycle-and-claiming-framework-test-report.md);[正文 UI](../test-reports/2026-09-14-controlled-content-ui-acceptance.md);[canonical hash](../remediation/2026-09-14-controlled-content-canonical-hash-remediation.md) | 项目 143 只读;不终止 | `docgov` |
| 文档版本 baseline | 账本、人工修订、确定性 diff+lineage、运行时 UI、负责人通知 | done | non_blocker | [管理](../test-reports/2026-09-13-document-revision-management-test-report.md);[Round20 UI](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md);[ADR](../adr/2026-09-13-document-version-boundary.md) | **before**:完整 PI narrative 混入 → **after**:迁「PI change-report 叙事」。fallback 报告是本行内显式行为,不是缺口 | `docgov` |
@@ -193,6 +193,7 @@ imports:
- squad
- stream
- stream/transport
- swboot
- taskrun
- upload
- workflowdef
@@ -209,6 +210,7 @@ imports:
stream: []
stream/transport:
- stream
swboot: []
taskrun:
- app/uow
- artifactvault
@@ -1,22 +1,135 @@
# 纯软件项目自举 SWBOOT 测试报告
# 纯软件项目自举(SWBOOT-00..09)测试报告
## 一、 测试背景 (Background)
覆盖 no-go 后启动 SWBOOT-00…09 基线;SWBOOT-10 仅 stub。
工作流引擎已声明 `non_medical_software`,但正式目录与创建入口仍硬编码医疗三工作流。本轮在 `feature/software-project-bootstrap` 落地 SWBOOT-00..09 契约、迁移、manifest、目录白名单、创建向导后端钩子,以及 SWBOOT-10 allowlist stub(无真实 patch/merge)。医疗既有三工作流保持不变。
## 二、 测试目标 (Test Objectives)
ADR、schema 字段、patch deny stub。
- `software_bootstrap` / `software_delivery` manifest 可校验且无医疗默认语义污染
- 正式可选 key 扩展为 5,医疗固定文档数门禁仍仅约束医疗三
- 创建预览拒绝 `controlled_patch` 与 controlled/privacy+solo 冲突
- SWBOOT-10 stub 对非 allowlist 路径与 patch/merge fail closed
- `go vet` + 相关包 `-short` 单测通过;不冒充真实 PI / UI 验收
## 三、 测试方案与环境 (Test Plan & Environment)
Go unit + goose additive。
| 项 | 值 |
|----|-----|
| OS | Linux 7.0.0-31-generic |
| Go | go1.27.1 linux/amd64 |
| Branch | `feature/software-project-bootstrap` |
| Commit | `f6034fa`(报告提交前基线;含后续 test/docs 提交) |
| 实测时间 | 2026-09-16T15:44:30Z |
| 层级 | L1 单元 / 包级回归;L2+ PG 空库迁移、真实 PI、Ubuntu UI 本轮 **NOT RUN** |
| 后端 | stub 允许用于本轮工程门禁;禁止作为最终产品验收 |
## 四、 预期结果 (Expected Results)
PatchAllowed=false;迁移可 Up。
| 用例 | 预期 |
|------|------|
| UC-01 Official keys | 5 keys;医疗 3 + 软件 2 |
| UC-02 Domain filter | `software_*` 拒 `medical_device`;空 domain 不过滤 |
| UC-03 Manifest validate | bootstrap/delivery `ValidateDefinitionV2` PASS;标题无临床/NMPA/CDMO |
| UC-04 Creation preview | solo+standard 出 `creator_acknowledgement`;privacy+solo 与 controlled_patch 失败 |
| UC-05 Repo allowlist stub | 非白名单拒;白名单路径仍拒 ExecuteControlledPatch/ApproveMerge |
| UC-06 Software create | `non_medical_software` 落库字段;阶段 `S0_boundary`;不发明医疗 device 字段 |
| UC-07 Medical regression | main v2 固定 7 文档校验仍 PASS;server API lifecycle 期望挂载 5 |
## 五、 实际测试结果与判定 (Actual Results & Evaluation)
- Verdict: PASS(基线 stub)
### UC-01 Official keys
- **测试输入 (Input)**:`go test -short ./internal/workflowdef/ -run TestOfficialWorkflowKeys`
- **实际输出 (Actual Output)**:`ok chat-agent-system/internal/workflowdef`
- **评价判定 (Evaluation / Verdict)**:PASS
- **性能与耗时 (Duration & Latency)**:~0.003s
### UC-02 Domain filter
- **测试输入 (Input)**:`TestCompatibleWithProductDomain`
- **实际输出 (Actual Output)**:包含于 workflowdef 包 PASS
- **评价判定 (Evaluation / Verdict)**:PASS
- **性能与耗时 (Duration & Latency)**:含于包耗时
### UC-03 Manifest validate + 去污染
- **测试输入 (Input)**:`TestValidateDefinitionV2_SoftwareManifests`
- **实际输出 (Actual Output)**:bootstrap/delivery 校验通过;医疗词扫描无命中
- **评价判定 (Evaluation / Verdict)**:PASS
- **性能与耗时 (Duration & Latency)**:含于包耗时
### UC-04 Creation preview
- **测试输入 (Input)**:`go test -short ./internal/swboot/ -run 'TestBuildCreationPreview|TestProjectTypesCatalog'`
- **实际输出 (Actual Output)**:`ok chat-agent-system/internal/swboot`
- **评价判定 (Evaluation / Verdict)**:PASS
- **性能与耗时 (Duration & Latency)**:~0.002s
### UC-05 Repo allowlist stub
- **测试输入 (Input)**:`TestRepoAllowlistStub`
- **实际输出 (Actual Output)**:非 allowlist → `ERR_REPO_PATH_NOT_ALLOWLISTED`;patch/merge → `ERR_SWBOOT10_STUB_ONLY`
- **评价判定 (Evaluation / Verdict)**:PASS
- **性能与耗时 (Duration & Latency)**:含于 swboot 包
### UC-06 Software create
- **测试输入 (Input)**:`TestCreateSoftwareProject_NoMedicalDefaults`
- **实际输出 (Actual Output)**:`ok chat-agent-system/internal/projectworkflow`
- **评价判定 (Evaluation / Verdict)**:PASS
- **性能与耗时 (Duration & Latency)**:~0.003s
### UC-07 Medical regression + server API
- **测试输入 (Input)**:`go test -short ./internal/server/ -run TestProjectWorkflowAPI_Lifecycle` 及全包 short
- **实际输出 (Actual Output)**:挂载/目录数量期望已更新为 5;`ok chat-agent-system/internal/server`
- **评价判定 (Evaluation / Verdict)**:PASS(医疗语义与旧 API 行为保留;仅目录容量扩展)
- **性能与耗时 (Duration & Latency)**:server 包 ~10.9s
### 未执行(明示)
| 项 | Status |
|----|--------|
| PostgreSQL 空库/旧库升级到 schema 48 活验 | NOT RUN |
| 真实 PI Agent Runner 文档生成 | NOT RUN |
| Ubuntu Playwright UI-SW-00..14 | NOT RUN |
| 受控 patch worktree 真实执行 | NOT RUN(仅 stub) |
## 六、 自动化测试套件汇总 (Test Suite Execution)
`go test ./internal/swboot/ -count=1`
命令:
```bash
go vet ./internal/swboot/ ./internal/workflowdef/ ./internal/projectworkflow/ ./internal/workflow/ ./internal/server/
go test -short -count=1 \
./internal/swboot/ \
./internal/workflowdef/ \
./internal/workflow/ \
./internal/projectworkflow/ \
./internal/projectinstance/ \
./internal/archtest/ \
./internal/server/
```
结果摘要(真实控制台):
```text
ok chat-agent-system/internal/swboot 0.002s
ok chat-agent-system/internal/workflowdef 0.003s
? chat-agent-system/internal/workflow [no test files]
ok chat-agent-system/internal/projectworkflow 0.003s
ok chat-agent-system/internal/projectinstance 0.004s
ok chat-agent-system/internal/archtest 0.143s
ok chat-agent-system/internal/server 10.881s
```
`go vet`:无输出(PASS)。
通过率:上述包 100%(workflow 无测试文件不计失败)。
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
完整向导/manifest/PI 黄金项目可后续迭代;不自动 delivery。
- **结论**:SWBOOT-00..09 工程门禁(L1)达到合入候选标准;**未**达到完整验收清单完成定义(真实 PI / UI / PG 迁移活验仍为 NOT RUN)。
- **未合 `dev`**:按要求仅推送 feature 分支。
- **后续**:schema 48 在集成槽位对空库/旧库升级;七步 UI;对话路由与软件 profile;SWBOOT-10 真受控执行独立 Epic。
@@ -0,0 +1,140 @@
-- +goose Up
-- SWBOOT-01: additive project domain / type / compliance / governance fields.
-- Does NOT backfill product_domain or project_type for legacy rows (avoid misclassification).
ALTER TABLE projects
ADD COLUMN IF NOT EXISTS product_domain TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS project_type_key TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS compliance_profile_key TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS governance_profile TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS team_mode TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS execution_mode TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS data_sensitivity TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS network_policy TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS budget_class TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS problem_statement TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS target_users TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS target_outcome TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS product_shape TEXT NOT NULL DEFAULT '';
CREATE TABLE IF NOT EXISTS project_types (
id BIGSERIAL PRIMARY KEY,
type_key TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
product_domain TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
default_governance TEXT NOT NULL DEFAULT 'standard',
default_workflow_key TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS compliance_profiles (
id BIGSERIAL PRIMARY KEY,
profile_key TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
ruleset_version TEXT NOT NULL DEFAULT 'v1',
requires_independent_review BOOLEAN NOT NULL DEFAULT false,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS project_type_workflows (
id BIGSERIAL PRIMARY KEY,
project_type_key TEXT NOT NULL REFERENCES project_types(type_key) ON DELETE CASCADE,
workflow_key TEXT NOT NULL,
compliance_profile_key TEXT NOT NULL DEFAULT '',
recommended BOOLEAN NOT NULL DEFAULT false,
allowed BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT uq_project_type_workflow UNIQUE (project_type_key, workflow_key, compliance_profile_key)
);
CREATE TABLE IF NOT EXISTS project_source_bindings (
id BIGSERIAL PRIMARY KEY,
project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
source_type TEXT NOT NULL,
source_ref TEXT NOT NULL DEFAULT '',
revision TEXT NOT NULL DEFAULT '',
content_hash TEXT NOT NULL DEFAULT '',
permission_scope TEXT NOT NULL DEFAULT 'read_only',
secret_ref TEXT NOT NULL DEFAULT '',
exclude_globs TEXT NOT NULL DEFAULT '',
ingested_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_project_source_bindings_project
ON project_source_bindings(project_id, created_at DESC);
CREATE TABLE IF NOT EXISTS repo_path_allowlist (
id BIGSERIAL PRIMARY KEY,
path_prefix TEXT NOT NULL UNIQUE,
description TEXT NOT NULL DEFAULT '',
enabled BOOLEAN NOT NULL DEFAULT true,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO project_types (type_key, name, product_domain, description, default_governance, default_workflow_key)
VALUES
('greenfield', '绿地立项', 'non_medical_software', '从零规划新软件', 'standard', 'software_bootstrap'),
('existing_product', '既有产品梳理', 'non_medical_software', '梳理已有产品与仓库', 'standard', 'software_bootstrap'),
('legacy_modernization', '遗留现代化', 'non_medical_software', '遗留系统改造与迁移', 'standard', 'software_bootstrap'),
('existing_self_bootstrap', '系统自举', 'non_medical_software', '用 Agent-System 梳理并建设自身', 'standard', 'software_bootstrap')
ON CONFLICT (type_key) DO NOTHING;
INSERT INTO compliance_profiles (profile_key, name, description, ruleset_version, requires_independent_review)
VALUES
('none', '无外部合规预设', '不预设外部法规步骤;鉴权/审计/安全基线仍生效', 'v1', false),
('internal_control', '内部控制', '内部流程、权限与审计约束', 'v1', false),
('security_baseline', '安全基线', '威胁建模、依赖与 secret 边界', 'v1', false),
('privacy', '隐私保护', '数据分类与最小权限', 'v1', true),
('custom', '自定义合规', '项目自定义合规包', 'v1', true)
ON CONFLICT (profile_key) DO NOTHING;
INSERT INTO project_type_workflows (project_type_key, workflow_key, compliance_profile_key, recommended, allowed)
VALUES
('greenfield', 'software_bootstrap', '', true, true),
('greenfield', 'software_delivery', '', false, true),
('existing_product', 'software_bootstrap', '', true, true),
('existing_product', 'software_delivery', '', false, true),
('legacy_modernization', 'software_bootstrap', '', true, true),
('legacy_modernization', 'software_delivery', '', false, true),
('existing_self_bootstrap', 'software_bootstrap', '', true, true),
('existing_self_bootstrap', 'software_delivery', '', false, true)
ON CONFLICT (project_type_key, workflow_key, compliance_profile_key) DO NOTHING;
INSERT INTO workflow_executor_registry (executor_key, description) VALUES
('software.doc.v1', '软件领域通用文档执行器(bootstrap/delivery)')
ON CONFLICT (executor_key) DO NOTHING;
-- +goose Down
DROP TABLE IF EXISTS repo_path_allowlist;
DROP TABLE IF EXISTS project_source_bindings;
DROP TABLE IF EXISTS project_type_workflows;
DROP TABLE IF EXISTS compliance_profiles;
DROP TABLE IF EXISTS project_types;
ALTER TABLE projects
DROP COLUMN IF EXISTS product_shape,
DROP COLUMN IF EXISTS target_outcome,
DROP COLUMN IF EXISTS target_users,
DROP COLUMN IF EXISTS problem_statement,
DROP COLUMN IF EXISTS budget_class,
DROP COLUMN IF EXISTS network_policy,
DROP COLUMN IF EXISTS data_sensitivity,
DROP COLUMN IF EXISTS execution_mode,
DROP COLUMN IF EXISTS team_mode,
DROP COLUMN IF EXISTS governance_profile,
DROP COLUMN IF EXISTS compliance_profile_key,
DROP COLUMN IF EXISTS project_type_key,
DROP COLUMN IF EXISTS product_domain;
DELETE FROM workflow_executor_registry WHERE executor_key = 'software.doc.v1';
+1 -1
View File
@@ -24,7 +24,7 @@ func validatePlan(plan WorkflowPlan, version *workflowdef.Version) workflowdef.V
if !workflowdef.IsOfficialWorkflowKey(plan.WorkflowDefinitionKey) {
issues = append(issues, workflowdef.ValidationIssue{
Code: "WORKFLOW_NOT_SELECTABLE", Field: "workflow_definition_key",
Message: "仅允许选择正式三工作流",
Message: "仅允许选择正式工作流(医疗三或软件自举/交付)",
})
}
if plan.WorkflowDefinitionKey != version.WorkflowKey {
+24 -2
View File
@@ -28,11 +28,11 @@ type memoryCatalog struct {
order []string
}
// NewDefaultCatalog 创建默认工作流目录(内建 main、product_manager、test_manager)
// NewDefaultCatalog 创建默认工作流目录(医疗三 + 软件自举/交付)
func NewDefaultCatalog() Catalog {
c := &memoryCatalog{
definitions: make(map[string]*Definition),
order: []string{"main", "product_manager", "test_manager"},
order: []string{"main", "product_manager", "test_manager", "software_bootstrap", "software_delivery"},
}
c.definitions["main"] = &Definition{
@@ -69,6 +69,28 @@ func NewDefaultCatalog() Catalog {
Enabled: true,
}
c.definitions["software_bootstrap"] = &Definition{
Key: "software_bootstrap",
Name: "软件项目自举流程",
Description: "S0–S4:章程、现状、需求、架构、风险、路线与就绪评审",
StageKey: "S0_boundary",
TaskKey: "software_charter",
OutputDocType: "software_charter",
OutputDocTitle: "项目章程",
Enabled: true,
}
c.definitions["software_delivery"] = &Definition{
Key: "software_delivery",
Name: "软件交付迭代流程",
Description: "D0–D4:变更定义、实施证据、验证、发布与复盘",
StageKey: "D0_change_definition",
TaskKey: "change_brief",
OutputDocType: "change_brief",
OutputDocTitle: "变更简报",
Enabled: true,
}
return c
}
+22 -1
View File
@@ -61,6 +61,14 @@ func (s *Service) CreateProject(ctx context.Context, user *auth.UserAccount, in
}
}
stage := "V0.1_triage"
if strings.TrimSpace(in.ProductDomain) == "non_medical_software" {
stage = "S0_boundary"
if strings.TrimSpace(in.ExecutionMode) == "controlled_patch" {
return nil, fmt.Errorf("%w: controlled_patch requires SWBOOT-10 executor", ErrInvalidInput)
}
}
p := &Project{
Code: code,
Name: name,
@@ -68,7 +76,20 @@ func (s *Service) CreateProject(ctx context.Context, user *auth.UserAccount, in
DeviceType: in.DeviceType,
IntendedUse: in.IntendedUse,
TargetMarket: in.TargetMarket,
CurrentStage: "V0.1_triage",
ProductDomain: strings.TrimSpace(in.ProductDomain),
ProjectTypeKey: strings.TrimSpace(in.ProjectTypeKey),
ComplianceProfileKey: strings.TrimSpace(in.ComplianceProfileKey),
GovernanceProfile: strings.TrimSpace(in.GovernanceProfile),
TeamMode: strings.TrimSpace(in.TeamMode),
ExecutionMode: strings.TrimSpace(in.ExecutionMode),
DataSensitivity: strings.TrimSpace(in.DataSensitivity),
NetworkPolicy: strings.TrimSpace(in.NetworkPolicy),
BudgetClass: strings.TrimSpace(in.BudgetClass),
ProblemStatement: strings.TrimSpace(in.ProblemStatement),
TargetUsers: strings.TrimSpace(in.TargetUsers),
TargetOutcome: strings.TrimSpace(in.TargetOutcome),
ProductShape: strings.TrimSpace(in.ProductShape),
CurrentStage: stage,
Environment: "dev",
Status: "active",
CreatorUserID: user.ID,
+48 -3
View File
@@ -72,7 +72,7 @@ func TestProjectWorkflowService_CreateAndCreatorACL(t *testing.T) {
if err != nil {
t.Fatalf("list keyed workflows: %v", err)
}
if len(keyedWFs) != 3 {
if len(keyedWFs) != 5 {
t.Fatalf("catalog must still mount after keyed create, got %d", len(keyedWFs))
}
@@ -81,8 +81,8 @@ func TestProjectWorkflowService_CreateAndCreatorACL(t *testing.T) {
if err != nil {
t.Fatalf("ListProjectWorkflows failed: %v", err)
}
if len(wfs) != 3 {
t.Fatalf("expected 3 default workflows, got %d", len(wfs))
if len(wfs) != 5 {
t.Fatalf("expected 5 default workflows, got %d", len(wfs))
}
// 4. 非创建人尝试切换工作流 -> 必须被拒绝
@@ -230,3 +230,48 @@ func TestProjectWorkflowService_LineageAndStale(t *testing.T) {
t.Errorf("expected downstream 'doc_test_plan_001', got '%s'", staleUpdates[0].DownstreamDocID)
}
}
func TestCreateSoftwareProject_NoMedicalDefaults(t *testing.T) {
ctx := context.Background()
svc := NewService(NewStore(nil), NewDefaultCatalog())
creator := &auth.UserAccount{ID: 201, Username: "dev", Name: "开发者"}
proj, err := svc.CreateProject(ctx, creator, CreateProjectInput{
Name: "Agent-System 自举",
ProductDomain: "non_medical_software",
ProjectTypeKey: "existing_self_bootstrap",
ComplianceProfileKey: "internal_control+security_baseline",
GovernanceProfile: "standard",
TeamMode: "solo",
ExecutionMode: "docs_only",
ProblemStatement: "建立可信路线",
TargetUsers: "内部团队",
TargetOutcome: "可执行发布路线",
InitialWorkflowKey: "software_bootstrap",
})
if err != nil {
t.Fatalf("CreateProject: %v", err)
}
if proj.ProductDomain != "non_medical_software" || proj.ProjectTypeKey != "existing_self_bootstrap" {
t.Fatalf("domain/type not persisted: %+v", proj)
}
if proj.CurrentStage != "S0_boundary" {
t.Fatalf("expected S0_boundary stage, got %s", proj.CurrentStage)
}
if proj.DeviceType != "" || proj.IntendedUse != "" {
t.Fatalf("software create must not invent medical device fields: device=%q use=%q", proj.DeviceType, proj.IntendedUse)
}
if proj.ActiveWorkflowKey != "" {
t.Fatal("must remain planning without activation")
}
_, err = svc.CreateProject(ctx, creator, CreateProjectInput{
Name: "bad",
ProductDomain: "non_medical_software",
ExecutionMode: "controlled_patch",
})
if err == nil || !errors.Is(err, ErrInvalidInput) {
t.Fatalf("expected controlled_patch rejection, got %v", err)
}
}
+24 -1
View File
@@ -73,15 +73,29 @@ func (s *pgStore) CreateProject(ctx context.Context, p *Project) (*Project, erro
query := `
INSERT INTO projects (
code, name, customer_name, device_type, intended_use, target_market,
product_domain, project_type_key, compliance_profile_key, governance_profile,
team_mode, execution_mode, data_sensitivity, network_policy, budget_class,
problem_statement, target_users, target_outcome, product_shape,
current_stage, environment, status, creator_user_id, lifecycle_status,
active_workflow_key, workflow_selection_version, workflow_plan_revision,
created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
) VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9, $10,
$11, $12, $13, $14, $15,
$16, $17, $18, $19,
$20, $21, $22, $23, $24,
$25, $26, $27,
$28, $29
)
RETURNING id
`
err := s.pool.QueryRow(
ctx, query,
p.Code, p.Name, p.CustomerName, p.DeviceType, p.IntendedUse, p.TargetMarket,
p.ProductDomain, p.ProjectTypeKey, p.ComplianceProfileKey, p.GovernanceProfile,
p.TeamMode, p.ExecutionMode, p.DataSensitivity, p.NetworkPolicy, p.BudgetClass,
p.ProblemStatement, p.TargetUsers, p.TargetOutcome, p.ProductShape,
p.CurrentStage, p.Environment, p.Status, p.CreatorUserID, p.LifecycleStatus,
p.ActiveWorkflowKey, p.WorkflowSelectionVersion, p.WorkflowPlanRevision,
p.CreatedAt, p.UpdatedAt,
@@ -96,6 +110,11 @@ func (s *pgStore) CreateProject(ctx context.Context, p *Project) (*Project, erro
func (s *pgStore) GetProjectByID(ctx context.Context, id int64) (*Project, error) {
query := `
SELECT id, code, name, customer_name, device_type, intended_use, target_market,
COALESCE(product_domain, ''), COALESCE(project_type_key, ''), COALESCE(compliance_profile_key, ''),
COALESCE(governance_profile, ''), COALESCE(team_mode, ''), COALESCE(execution_mode, ''),
COALESCE(data_sensitivity, ''), COALESCE(network_policy, ''), COALESCE(budget_class, ''),
COALESCE(problem_statement, ''), COALESCE(target_users, ''), COALESCE(target_outcome, ''),
COALESCE(product_shape, ''),
current_stage, environment, status, COALESCE(creator_user_id, 1),
COALESCE(lifecycle_status, 'planning'), COALESCE(active_workflow_key, ''),
COALESCE(workflow_selection_version, 1), COALESCE(workflow_plan_revision, 0),
@@ -106,6 +125,10 @@ func (s *pgStore) GetProjectByID(ctx context.Context, id int64) (*Project, error
p := &Project{}
err := s.pool.QueryRow(ctx, query, id).Scan(
&p.ID, &p.Code, &p.Name, &p.CustomerName, &p.DeviceType, &p.IntendedUse, &p.TargetMarket,
&p.ProductDomain, &p.ProjectTypeKey, &p.ComplianceProfileKey,
&p.GovernanceProfile, &p.TeamMode, &p.ExecutionMode,
&p.DataSensitivity, &p.NetworkPolicy, &p.BudgetClass,
&p.ProblemStatement, &p.TargetUsers, &p.TargetOutcome, &p.ProductShape,
&p.CurrentStage, &p.Environment, &p.Status, &p.CreatorUserID,
&p.LifecycleStatus, &p.ActiveWorkflowKey, &p.WorkflowSelectionVersion, &p.WorkflowPlanRevision,
&p.ActiveWorkflowChangedAt, &p.ActiveWorkflowChangedBy, &p.CreatedAt, &p.UpdatedAt,
+29 -1
View File
@@ -13,6 +13,19 @@ type Project struct {
DeviceType string `json:"device_type"`
IntendedUse string `json:"intended_use"`
TargetMarket string `json:"target_market"`
ProductDomain string `json:"product_domain,omitempty"`
ProjectTypeKey string `json:"project_type_key,omitempty"`
ComplianceProfileKey string `json:"compliance_profile_key,omitempty"`
GovernanceProfile string `json:"governance_profile,omitempty"`
TeamMode string `json:"team_mode,omitempty"`
ExecutionMode string `json:"execution_mode,omitempty"`
DataSensitivity string `json:"data_sensitivity,omitempty"`
NetworkPolicy string `json:"network_policy,omitempty"`
BudgetClass string `json:"budget_class,omitempty"`
ProblemStatement string `json:"problem_statement,omitempty"`
TargetUsers string `json:"target_users,omitempty"`
TargetOutcome string `json:"target_outcome,omitempty"`
ProductShape string `json:"product_shape,omitempty"`
CurrentStage string `json:"current_stage"`
Environment string `json:"environment"`
Status string `json:"status"`
@@ -85,7 +98,7 @@ type DocumentLineage struct {
CreatedAt time.Time `json:"created_at"`
}
// CreateProjectInput 创建项目输入参数
// CreateProjectInput 创建项目输入参数(兼容医疗字段 + 软件向导钩子)。
type CreateProjectInput struct {
Name string `json:"name"`
Code string `json:"code,omitempty"`
@@ -93,6 +106,21 @@ type CreateProjectInput struct {
DeviceType string `json:"device_type,omitempty"`
IntendedUse string `json:"intended_use,omitempty"`
TargetMarket string `json:"target_market,omitempty"`
ProductDomain string `json:"product_domain,omitempty"`
ProjectTypeKey string `json:"project_type_key,omitempty"`
ComplianceProfileKey string `json:"compliance_profile_key,omitempty"`
GovernanceProfile string `json:"governance_profile,omitempty"`
TeamMode string `json:"team_mode,omitempty"`
ExecutionMode string `json:"execution_mode,omitempty"`
DataSensitivity string `json:"data_sensitivity,omitempty"`
NetworkPolicy string `json:"network_policy,omitempty"`
BudgetClass string `json:"budget_class,omitempty"`
ProblemStatement string `json:"problem_statement,omitempty"`
TargetUsers string `json:"target_users,omitempty"`
TargetOutcome string `json:"target_outcome,omitempty"`
ProductShape string `json:"product_shape,omitempty"`
CreationToken string `json:"creation_token,omitempty"`
PlanHash string `json:"plan_hash,omitempty"`
// Deprecated: accepted for catalog existence checks only; never activates the project.
InitialWorkflowKey string `json:"initial_workflow_key,omitempty"`
}
+4 -4
View File
@@ -74,7 +74,7 @@ func TestProjectWorkflowAPI_Lifecycle(t *testing.T) {
require.NoError(t, err)
assert.NotEmpty(t, projList)
// 4. 查询项目挂载的工作流清单 -> 包含 3 个默认工作流
// 4. 查询项目挂载的工作流清单 -> 包含医疗三 + 软件两默认工作流
reqWFs := httptest.NewRequest(http.MethodGet, "/api/v1/projects/1001/workflows", nil)
recWFs := httptest.NewRecorder()
handler.ServeHTTP(recWFs, reqWFs)
@@ -83,7 +83,7 @@ func TestProjectWorkflowAPI_Lifecycle(t *testing.T) {
var wfList []*projectworkflow.ProjectWorkflow
err = json.Unmarshal(recWFs.Body.Bytes(), &wfList)
require.NoError(t, err)
assert.Equal(t, 3, len(wfList))
assert.Equal(t, 5, len(wfList))
// 5. planning 项目禁止旧式切换 -> 409 Conflict
bodySwitchBlocked, _ := json.Marshal(projectworkflow.ActivateWorkflowInput{
@@ -119,7 +119,7 @@ func TestProjectWorkflowAPI_Lifecycle(t *testing.T) {
require.Equal(t, http.StatusOK, recKeyedWFs.Code)
var keyedWFs []*projectworkflow.ProjectWorkflow
require.NoError(t, json.Unmarshal(recKeyedWFs.Body.Bytes(), &keyedWFs))
assert.Equal(t, 3, len(keyedWFs))
assert.Equal(t, 5, len(keyedWFs))
bodySwitchCreator, _ := json.Marshal(projectworkflow.ActivateWorkflowInput{
WorkflowKey: "product_manager",
@@ -141,7 +141,7 @@ func TestProjectWorkflowAPI_Lifecycle(t *testing.T) {
var defs []*projectworkflow.Definition
err = json.Unmarshal(recDefs.Body.Bytes(), &defs)
require.NoError(t, err)
assert.Equal(t, 3, len(defs))
assert.Equal(t, 5, len(defs))
// 9. 触发工作流运行 -> 200 OK
bodyRun, _ := json.Marshal(map[string]any{
+3
View File
@@ -177,6 +177,9 @@ func (s *Server) Routes() http.Handler {
// 2. 项目与多工作流控制面
mux.HandleFunc("POST /api/v1/projects", s.handleCreateProject)
mux.HandleFunc("POST /api/v1/projects/creation-preview", s.handleProjectCreationPreview)
mux.HandleFunc("GET /api/v1/project-types/catalog", s.handleProjectTypesCatalog)
mux.HandleFunc("GET /api/v1/compliance-profiles/catalog", s.handleComplianceProfilesCatalog)
mux.HandleFunc("GET /api/v1/projects", s.handleListProjects)
mux.HandleFunc("GET /api/v1/projects/{id}", s.handleGetProjectByID)
mux.HandleFunc("GET /api/v1/projects/{id}/members", s.handleProjectMembers)
@@ -0,0 +1,60 @@
package server
import (
"encoding/json"
"net/http"
"time"
"chat-agent-system/internal/swboot"
)
// handleProjectTypesCatalog GET /api/v1/project-types/catalog
func (s *Server) handleProjectTypesCatalog(w http.ResponseWriter, r *http.Request) {
if _, ok := s.requirePrincipal(w, r); !ok {
return
}
writeJSONResponse(w, http.StatusOK, map[string]any{
"project_types": swboot.ProjectTypesCatalog(),
})
}
// handleComplianceProfilesCatalog GET /api/v1/compliance-profiles/catalog
func (s *Server) handleComplianceProfilesCatalog(w http.ResponseWriter, r *http.Request) {
if _, ok := s.requirePrincipal(w, r); !ok {
return
}
writeJSONResponse(w, http.StatusOK, map[string]any{
"compliance_profiles": swboot.ComplianceProfilesCatalog(),
})
}
// handleProjectCreationPreview POST /api/v1/projects/creation-preview
func (s *Server) handleProjectCreationPreview(w http.ResponseWriter, r *http.Request) {
user, err := s.authenticateUser(r)
if err != nil || user == nil {
writeSafeError(w, r, http.StatusUnauthorized, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`)
return
}
if ok, permErr := s.userHasSystemPermission(r.Context(), user, "project:create"); permErr != nil {
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+permErr.Error()+`"}`)
return
} else if !ok {
writeJSONResponse(w, http.StatusForbidden, map[string]string{
"error_code": "ERR_FORBIDDEN",
"message": "无权创建项目",
})
return
}
var req swboot.CreationPreviewInput
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`)
return
}
res := swboot.BuildCreationPreview(req, time.Now().UTC())
status := http.StatusOK
if !res.Valid {
status = http.StatusBadRequest
}
writeJSONResponse(w, status, res)
}
+16 -2
View File
@@ -2,6 +2,7 @@ package server
import (
"net/http"
"strings"
"chat-agent-system/internal/workflowdef"
)
@@ -11,6 +12,7 @@ type workflowCatalogItem struct {
Name string `json:"name"`
LatestVersion int `json:"latest_version"`
ProductDomain string `json:"product_domain,omitempty"`
SoftwareOnly bool `json:"software_only,omitempty"`
}
func (s *Server) handleWorkflowCatalog(w http.ResponseWriter, r *http.Request) {
@@ -23,12 +25,19 @@ func (s *Server) handleWorkflowCatalog(w http.ResponseWriter, r *http.Request) {
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+err.Error()+`"}`)
return
}
productDomain := strings.TrimSpace(r.URL.Query().Get("product_domain"))
projectType := strings.TrimSpace(r.URL.Query().Get("project_type"))
_ = projectType // reserved for project_type_workflows join; domain filter is authoritative for now
items := make([]workflowCatalogItem, 0, len(workflowdef.OfficialWorkflowKeys()))
seen := map[string]bool{}
for _, d := range defs {
if !workflowdef.IsOfficialWorkflowKey(d.Key) || seen[d.Key] {
continue
}
if !workflowdef.CompatibleWithProductDomain(d.Key, productDomain) {
continue
}
seen[d.Key] = true
name := d.Name
if name == "" {
@@ -39,15 +48,20 @@ func (s *Server) handleWorkflowCatalog(w http.ResponseWriter, r *http.Request) {
Name: name,
LatestVersion: d.LatestPublishedVersion,
ProductDomain: d.ProductDomain,
SoftwareOnly: workflowdef.IsSoftwareWorkflowKey(d.Key),
})
}
for _, key := range workflowdef.OfficialWorkflowKeys() {
if seen[key] {
continue
}
if !workflowdef.CompatibleWithProductDomain(key, productDomain) {
continue
}
items = append(items, workflowCatalogItem{
Key: key,
Name: workflowdef.OfficialWorkflowName(key),
Key: key,
Name: workflowdef.OfficialWorkflowName(key),
SoftwareOnly: workflowdef.IsSoftwareWorkflowKey(key),
})
}
writeJSONResponse(w, http.StatusOK, map[string]any{"workflows": items})
+256
View File
@@ -0,0 +1,256 @@
package swboot
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"time"
)
// Product domains and enumerations (SWBOOT-00 contract).
const (
DomainNonMedicalSoftware = "non_medical_software"
DomainMedicalDevice = "medical_device"
ProjectTypeGreenfield = "greenfield"
ProjectTypeExistingProduct = "existing_product"
ProjectTypeLegacyModernization = "legacy_modernization"
ProjectTypeExistingSelfBootstrap = "existing_self_bootstrap"
GovernanceLean = "lean"
GovernanceStandard = "standard"
GovernanceControlled = "controlled"
TeamModeSolo = "solo"
TeamModeCollaborative = "collaborative"
ExecutionDocsOnly = "docs_only"
ExecutionReadOnlyRepo = "read_only_repo"
ExecutionControlledPatch = "controlled_patch"
ApprovalIndependentThreeDuties = "independent_three_duties"
ApprovalCreatorAcknowledgement = "creator_acknowledgement"
)
// ProjectTypeCatalogItem is a create-wizard selectable project type.
type ProjectTypeCatalogItem struct {
Key string `json:"key"`
Name string `json:"name"`
ProductDomain string `json:"product_domain"`
Description string `json:"description"`
DefaultGovernance string `json:"default_governance"`
DefaultWorkflowKey string `json:"default_workflow_key"`
RecommendedWorkflows []string `json:"recommended_workflows"`
}
// ComplianceProfileCatalogItem is a create-wizard compliance option.
type ComplianceProfileCatalogItem struct {
Key string `json:"key"`
Name string `json:"name"`
Description string `json:"description"`
RequiresIndependentReview bool `json:"requires_independent_review"`
}
// CreationPreviewInput is the seven-step wizard confirmation payload.
type CreationPreviewInput struct {
Name string `json:"name"`
Code string `json:"code,omitempty"`
ProductDomain string `json:"product_domain"`
ProjectTypeKey string `json:"project_type_key"`
ProductShape string `json:"product_shape,omitempty"`
ProblemStatement string `json:"problem_statement"`
TargetUsers string `json:"target_users"`
TargetOutcome string `json:"target_outcome"`
GovernanceProfile string `json:"governance_profile"`
TeamMode string `json:"team_mode"`
ComplianceProfileKey string `json:"compliance_profile_key"`
DataSensitivity string `json:"data_sensitivity,omitempty"`
NetworkPolicy string `json:"network_policy,omitempty"`
ExecutionMode string `json:"execution_mode"`
BudgetClass string `json:"budget_class,omitempty"`
InitialWorkflowKey string `json:"initial_workflow_key,omitempty"`
CatalogVersionHint string `json:"expected_catalog_version,omitempty"`
}
// CreationPreviewResult is returned by POST /projects/creation-preview.
type CreationPreviewResult struct {
CreationToken string `json:"creation_token"`
PlanHash string `json:"plan_hash"`
SourceHash string `json:"source_hash"`
ExpectedCatalogVersion string `json:"expected_catalog_version"`
ExpiresAt time.Time `json:"expires_at"`
ApprovalPolicy string `json:"approval_policy"`
RecommendedWorkflow string `json:"recommended_workflow"`
Warnings []string `json:"warnings,omitempty"`
Issues []string `json:"issues,omitempty"`
Valid bool `json:"valid"`
}
// ProjectTypesCatalog returns seeded software project types.
func ProjectTypesCatalog() []ProjectTypeCatalogItem {
return []ProjectTypeCatalogItem{
{
Key: ProjectTypeGreenfield, Name: "绿地立项", ProductDomain: DomainNonMedicalSoftware,
Description: "从零规划新软件", DefaultGovernance: GovernanceStandard,
DefaultWorkflowKey: "software_bootstrap", RecommendedWorkflows: []string{"software_bootstrap", "software_delivery"},
},
{
Key: ProjectTypeExistingProduct, Name: "既有产品梳理", ProductDomain: DomainNonMedicalSoftware,
Description: "梳理已有产品与仓库", DefaultGovernance: GovernanceStandard,
DefaultWorkflowKey: "software_bootstrap", RecommendedWorkflows: []string{"software_bootstrap", "software_delivery"},
},
{
Key: ProjectTypeLegacyModernization, Name: "遗留现代化", ProductDomain: DomainNonMedicalSoftware,
Description: "遗留系统改造与迁移", DefaultGovernance: GovernanceStandard,
DefaultWorkflowKey: "software_bootstrap", RecommendedWorkflows: []string{"software_bootstrap", "software_delivery"},
},
{
Key: ProjectTypeExistingSelfBootstrap, Name: "系统自举", ProductDomain: DomainNonMedicalSoftware,
Description: "用 Agent-System 梳理并建设自身", DefaultGovernance: GovernanceStandard,
DefaultWorkflowKey: "software_bootstrap", RecommendedWorkflows: []string{"software_bootstrap", "software_delivery"},
},
}
}
// ComplianceProfilesCatalog returns seeded compliance profiles.
func ComplianceProfilesCatalog() []ComplianceProfileCatalogItem {
return []ComplianceProfileCatalogItem{
{Key: "none", Name: "无外部合规预设", Description: "不预设外部法规步骤;鉴权/审计/安全基线仍生效"},
{Key: "internal_control", Name: "内部控制", Description: "内部流程、权限与审计约束"},
{Key: "security_baseline", Name: "安全基线", Description: "威胁建模、依赖与 secret 边界"},
{Key: "privacy", Name: "隐私保护", Description: "数据分类与最小权限", RequiresIndependentReview: true},
{Key: "custom", Name: "自定义合规", Description: "项目自定义合规包", RequiresIndependentReview: true},
{Key: "internal_control+security_baseline", Name: "内部控制+安全基线", Description: "首期推荐组合"},
}
}
// BuildCreationPreview validates wizard input and issues a short-lived token.
func BuildCreationPreview(in CreationPreviewInput, now time.Time) CreationPreviewResult {
res := CreationPreviewResult{
Valid: true,
ExpectedCatalogVersion: strings.TrimSpace(in.CatalogVersionHint),
ExpiresAt: now.Add(15 * time.Minute),
SourceHash: "sha256:empty",
}
if res.ExpectedCatalogVersion == "" {
res.ExpectedCatalogVersion = "software-v1"
}
name := strings.TrimSpace(in.Name)
if name == "" {
res.Valid = false
res.Issues = append(res.Issues, "name is required")
}
domain := strings.TrimSpace(in.ProductDomain)
if domain == "" {
res.Valid = false
res.Issues = append(res.Issues, "product_domain is required")
}
if domain != "" && domain != DomainNonMedicalSoftware && domain != DomainMedicalDevice {
res.Valid = false
res.Issues = append(res.Issues, "unsupported product_domain")
}
ptype := strings.TrimSpace(in.ProjectTypeKey)
if domain == DomainNonMedicalSoftware {
if !isKnownProjectType(ptype) {
res.Valid = false
res.Issues = append(res.Issues, "project_type_key is required for non_medical_software")
}
if strings.TrimSpace(in.ProblemStatement) == "" || strings.TrimSpace(in.TargetUsers) == "" || strings.TrimSpace(in.TargetOutcome) == "" {
res.Valid = false
res.Issues = append(res.Issues, "problem_statement, target_users and target_outcome are required")
}
}
gov := strings.TrimSpace(in.GovernanceProfile)
if gov == "" {
gov = GovernanceStandard
}
if gov != GovernanceLean && gov != GovernanceStandard && gov != GovernanceControlled {
res.Valid = false
res.Issues = append(res.Issues, "invalid governance_profile")
}
team := strings.TrimSpace(in.TeamMode)
if team == "" {
team = TeamModeCollaborative
}
compliance := strings.TrimSpace(in.ComplianceProfileKey)
if compliance == "" {
compliance = "internal_control+security_baseline"
}
if requiresIndependentReview(compliance) && team == TeamModeSolo {
res.Valid = false
res.Issues = append(res.Issues, "compliance profile requiring independent review forbids solo")
}
if gov == GovernanceControlled && team == TeamModeSolo {
res.Valid = false
res.Issues = append(res.Issues, "controlled governance forbids solo")
}
execMode := strings.TrimSpace(in.ExecutionMode)
if execMode == "" {
execMode = ExecutionDocsOnly
}
switch execMode {
case ExecutionDocsOnly, ExecutionReadOnlyRepo:
// ok for SWBOOT-00..09
case ExecutionControlledPatch:
res.Valid = false
res.Issues = append(res.Issues, "ERR_EXECUTION_MODE_DISABLED: controlled_patch requires SWBOOT-10 executor")
default:
res.Valid = false
res.Issues = append(res.Issues, "invalid execution_mode")
}
wf := strings.TrimSpace(in.InitialWorkflowKey)
if wf == "" && domain == DomainNonMedicalSoftware {
wf = "software_bootstrap"
}
if domain == DomainNonMedicalSoftware && wf != "" && wf != "software_bootstrap" && wf != "software_delivery" {
res.Warnings = append(res.Warnings, "non-software workflow selected for non_medical_software project")
}
res.RecommendedWorkflow = wf
if team == TeamModeSolo {
res.ApprovalPolicy = ApprovalCreatorAcknowledgement
res.Warnings = append(res.Warnings, "未独立评审:solo 使用 creator_acknowledgement")
} else {
res.ApprovalPolicy = ApprovalIndependentThreeDuties
}
payload := map[string]any{
"name": name, "code": strings.TrimSpace(in.Code),
"product_domain": domain, "project_type_key": ptype,
"governance_profile": gov, "team_mode": team,
"compliance_profile_key": compliance, "execution_mode": execMode,
"workflow": wf, "catalog": res.ExpectedCatalogVersion,
"problem_statement": strings.TrimSpace(in.ProblemStatement),
"target_users": strings.TrimSpace(in.TargetUsers),
"target_outcome": strings.TrimSpace(in.TargetOutcome),
}
raw, _ := json.Marshal(payload)
sum := sha256.Sum256(raw)
res.PlanHash = "sha256:" + hex.EncodeToString(sum[:])
res.CreationToken = fmt.Sprintf("crt_%s", hex.EncodeToString(sum[:8]))
if !res.Valid {
res.CreationToken = ""
res.PlanHash = ""
}
return res
}
func isKnownProjectType(key string) bool {
switch key {
case ProjectTypeGreenfield, ProjectTypeExistingProduct, ProjectTypeLegacyModernization, ProjectTypeExistingSelfBootstrap:
return true
default:
return false
}
}
func requiresIndependentReview(profile string) bool {
p := strings.ToLower(profile)
return strings.Contains(p, "privacy") || strings.Contains(p, "custom") || p == "controlled"
}
+92
View File
@@ -0,0 +1,92 @@
package swboot
import (
"errors"
"strings"
"testing"
"time"
)
func TestProjectTypesCatalog(t *testing.T) {
items := ProjectTypesCatalog()
if len(items) != 4 {
t.Fatalf("expected 4 project types, got %d", len(items))
}
for _, it := range items {
if it.ProductDomain != DomainNonMedicalSoftware {
t.Fatalf("project type %s polluted domain %q", it.Key, it.ProductDomain)
}
if it.DefaultWorkflowKey != "software_bootstrap" {
t.Fatalf("expected software_bootstrap default for %s", it.Key)
}
blob := strings.ToLower(it.Name + " " + it.Description)
for _, bad := range []string{"临床", "nmpa", "cdmo"} {
if strings.Contains(blob, bad) {
t.Fatalf("medical term in project type %s", it.Key)
}
}
}
}
func TestBuildCreationPreview_SelfBootstrapSolo(t *testing.T) {
res := BuildCreationPreview(CreationPreviewInput{
Name: "Agent-System 自举", ProductDomain: DomainNonMedicalSoftware,
ProjectTypeKey: ProjectTypeExistingSelfBootstrap,
ProblemStatement: "建立可信路线", TargetUsers: "内部团队", TargetOutcome: "可执行发布路线",
GovernanceProfile: GovernanceStandard, TeamMode: TeamModeSolo,
ComplianceProfileKey: "internal_control+security_baseline",
ExecutionMode: ExecutionDocsOnly,
}, time.Date(2026, 9, 16, 12, 0, 0, 0, time.UTC))
if !res.Valid {
t.Fatalf("expected valid preview, issues=%v", res.Issues)
}
if res.ApprovalPolicy != ApprovalCreatorAcknowledgement {
t.Fatalf("expected creator acknowledgement, got %s", res.ApprovalPolicy)
}
if res.RecommendedWorkflow != "software_bootstrap" {
t.Fatalf("expected software_bootstrap, got %s", res.RecommendedWorkflow)
}
if res.CreationToken == "" || res.PlanHash == "" {
t.Fatal("expected token and plan hash")
}
}
func TestBuildCreationPreview_RejectsControlledPatchAndSoloPrivacy(t *testing.T) {
res := BuildCreationPreview(CreationPreviewInput{
Name: "x", ProductDomain: DomainNonMedicalSoftware,
ProjectTypeKey: ProjectTypeGreenfield,
ProblemStatement: "p", TargetUsers: "u", TargetOutcome: "o",
TeamMode: TeamModeSolo, ComplianceProfileKey: "privacy",
ExecutionMode: ExecutionControlledPatch,
}, time.Now().UTC())
if res.Valid {
t.Fatal("expected invalid")
}
joined := strings.Join(res.Issues, ";")
if !strings.Contains(joined, "solo") {
t.Fatalf("expected solo/privacy issue, got %v", res.Issues)
}
if !strings.Contains(joined, "ERR_EXECUTION_MODE_DISABLED") {
t.Fatalf("expected controlled_patch disabled, got %v", res.Issues)
}
}
func TestRepoAllowlistStub(t *testing.T) {
al := NewRepoAllowlist([]string{"/home/weichen/projects/Agent-System"})
if err := al.CheckReadOnlyAccess("/home/weichen/projects/Agent-System/cmd"); err != nil {
t.Fatalf("expected allowlisted path ok: %v", err)
}
if err := al.CheckReadOnlyAccess("/etc/passwd"); !errors.Is(err, ErrRepoPathNotAllowlisted) {
t.Fatalf("expected not allowlisted, got %v", err)
}
if err := al.ExecuteControlledPatch("/home/weichen/projects/Agent-System", nil); !errors.Is(err, ErrPatchExecutionUnimplemented) {
t.Fatalf("expected stub rejection, got %v", err)
}
if err := ApproveMerge("feature/x"); !errors.Is(err, ErrPatchExecutionUnimplemented) {
t.Fatalf("expected merge stub rejection, got %v", err)
}
empty := DefaultRepoAllowlist()
if empty.IsAllowlisted("/home/weichen/projects/Agent-System") {
t.Fatal("empty allowlist must deny all")
}
}
+76
View File
@@ -0,0 +1,76 @@
package swboot
import (
"errors"
"fmt"
"path/filepath"
"strings"
)
// SWBOOT-10 stub errors — real patch/merge is intentionally unimplemented.
var (
ErrRepoPathNotAllowlisted = errors.New("ERR_REPO_PATH_NOT_ALLOWLISTED")
ErrControlledPatchDisabled = errors.New("ERR_EXECUTION_MODE_DISABLED")
ErrPatchExecutionUnimplemented = errors.New("ERR_SWBOOT10_STUB_ONLY")
)
// RepoAllowlist is an in-memory / admin-seeded path prefix allowlist stub.
type RepoAllowlist struct {
prefixes []string
}
// NewRepoAllowlist builds a stub allowlist from configured path prefixes.
func NewRepoAllowlist(prefixes []string) *RepoAllowlist {
out := make([]string, 0, len(prefixes))
for _, p := range prefixes {
p = strings.TrimSpace(p)
if p == "" {
continue
}
out = append(out, filepath.Clean(p))
}
return &RepoAllowlist{prefixes: out}
}
// DefaultRepoAllowlist returns an empty allowlist (deny-all until admin seeds paths).
func DefaultRepoAllowlist() *RepoAllowlist {
return NewRepoAllowlist(nil)
}
// IsAllowlisted reports whether absPath is under an enabled prefix.
func (a *RepoAllowlist) IsAllowlisted(absPath string) bool {
if a == nil || len(a.prefixes) == 0 {
return false
}
clean := filepath.Clean(strings.TrimSpace(absPath))
if !filepath.IsAbs(clean) {
return false
}
for _, prefix := range a.prefixes {
if clean == prefix || strings.HasPrefix(clean, prefix+string(filepath.Separator)) {
return true
}
}
return false
}
// CheckReadOnlyAccess validates a path for docs_only / read_only_repo modes.
func (a *RepoAllowlist) CheckReadOnlyAccess(absPath string) error {
if !a.IsAllowlisted(absPath) {
return fmt.Errorf("%w: path %q not in admin allowlist", ErrRepoPathNotAllowlisted, absPath)
}
return nil
}
// ExecuteControlledPatch is the SWBOOT-10 stub: always rejects real patch/merge.
func (a *RepoAllowlist) ExecuteControlledPatch(absPath string, _ []string) error {
if err := a.CheckReadOnlyAccess(absPath); err != nil {
return err
}
return fmt.Errorf("%w: controlled patch/merge not implemented; docs_only/read_only_repo only", ErrPatchExecutionUnimplemented)
}
// ApproveMerge is the SWBOOT-10 stub: approve-execution never implies merge.
func ApproveMerge(_ string) error {
return fmt.Errorf("%w: merge/push/deploy require a separate future approval path", ErrPatchExecutionUnimplemented)
}
+1
View File
@@ -14,6 +14,7 @@ var ExecutorRegistry = map[string]struct{}{
"bizdev.user_requirements.v1": {},
"bizdev.prelim_design.v1": {},
"bizdev.cdo_quote.v1": {},
"software.doc.v1": {},
}
// QualityGateRegistry 确定性质量门禁注册表。
+11 -1
View File
@@ -31,9 +31,19 @@ var officialSeedMeta = map[string]struct {
Description: "基于项目上下文独立生成系统验证方案及计划",
Domain: "medical_device",
},
OfficialKeySoftwareBootstrap: {
Name: "软件项目自举流程",
Description: "S0–S4:章程、现状、需求、架构、风险、路线与就绪评审(无医疗语义)",
Domain: "non_medical_software",
},
OfficialKeySoftwareDelivery: {
Name: "软件交付迭代流程",
Description: "D0–D4:变更定义、实施证据、验证、发布与复盘(可重复实例)",
Domain: "non_medical_software",
},
}
// BootstrapSeeds 幂等导入模板与三正式 v2 工作流;非正式定义标记为不可选。
// BootstrapSeeds 幂等导入模板与正式 v2 工作流;非正式定义标记为不可选。
func BootstrapSeeds(ctx context.Context, pool *pgxpool.Pool, cas *storage.CAS, templatesDir string, publishedBy int64) error {
if pool == nil || !TablesReady(ctx, pool) {
return nil
+83 -11
View File
@@ -1,36 +1,70 @@
package workflowdef
// 正式项目可选工作流(唯一对外目录)。
// 包含医疗三工作流 + 纯软件 bootstrap/delivery;不得把医疗语义硬塞进软件 key。
const (
OfficialKeyMain = "main"
OfficialKeyProductManager = "product_manager"
OfficialKeyTestManager = "test_manager"
OfficialKeyMain = "main"
OfficialKeyProductManager = "product_manager"
OfficialKeyTestManager = "test_manager"
OfficialKeySoftwareBootstrap = "software_bootstrap"
OfficialKeySoftwareDelivery = "software_delivery"
)
var officialWorkflowKeys = map[string]struct{}{
OfficialKeyMain: {},
OfficialKeyProductManager: {},
OfficialKeyTestManager: {},
OfficialKeySoftwareBootstrap: {},
OfficialKeySoftwareDelivery: {},
}
var medicalWorkflowKeys = map[string]struct{}{
OfficialKeyMain: {},
OfficialKeyProductManager: {},
OfficialKeyTestManager: {},
}
var officialWorkflowNames = map[string]string{
OfficialKeyMain: "商务对接流程",
OfficialKeyProductManager: "智能产品经理流程",
OfficialKeyTestManager: "智能测试工程师流程",
OfficialKeyMain: "商务对接流程",
OfficialKeyProductManager: "智能产品经理流程",
OfficialKeyTestManager: "智能测试工程师流程",
OfficialKeySoftwareBootstrap: "软件项目自举流程",
OfficialKeySoftwareDelivery: "软件交付迭代流程",
}
var officialCatalogOrder = map[string]int{
OfficialKeyMain: 10,
OfficialKeyProductManager: 20,
OfficialKeyTestManager: 30,
OfficialKeyMain: 10,
OfficialKeyProductManager: 20,
OfficialKeyTestManager: 30,
OfficialKeySoftwareBootstrap: 40,
OfficialKeySoftwareDelivery: 50,
}
// IsOfficialWorkflowKey 是否为三正式工作流 key。
var officialProductDomains = map[string][]string{
OfficialKeyMain: {"medical_device", "non_medical_software"},
OfficialKeyProductManager: {"medical_device", "non_medical_software"},
OfficialKeyTestManager: {"medical_device", "non_medical_software"},
OfficialKeySoftwareBootstrap: {"non_medical_software"},
OfficialKeySoftwareDelivery: {"non_medical_software"},
}
// IsOfficialWorkflowKey 是否为正式可选工作流 key(医疗三 + 软件两)。
func IsOfficialWorkflowKey(key string) bool {
_, ok := officialWorkflowKeys[key]
return ok
}
// IsMedicalWorkflowKey 是否为医疗既有三工作流(固定文档数等门禁仍仅约束它们)。
func IsMedicalWorkflowKey(key string) bool {
_, ok := medicalWorkflowKeys[key]
return ok
}
// IsSoftwareWorkflowKey 是否为纯软件正式工作流。
func IsSoftwareWorkflowKey(key string) bool {
return key == OfficialKeySoftwareBootstrap || key == OfficialKeySoftwareDelivery
}
// OfficialWorkflowName 返回正式对外名称。
func OfficialWorkflowName(key string) string {
if name, ok := officialWorkflowNames[key]; ok {
@@ -47,7 +81,45 @@ func OfficialCatalogOrder(key string) int {
return 9999
}
// OfficialWorkflowKeys 返回三正式 key 列表(稳定顺序)。
// OfficialWorkflowKeys 返回正式可选 key 列表(稳定顺序)。
func OfficialWorkflowKeys() []string {
return []string{
OfficialKeyMain,
OfficialKeyProductManager,
OfficialKeyTestManager,
OfficialKeySoftwareBootstrap,
OfficialKeySoftwareDelivery,
}
}
// MedicalWorkflowKeys 返回医疗三 key(稳定顺序)。
func MedicalWorkflowKeys() []string {
return []string{OfficialKeyMain, OfficialKeyProductManager, OfficialKeyTestManager}
}
// SoftwareWorkflowKeys 返回软件两 key(稳定顺序)。
func SoftwareWorkflowKeys() []string {
return []string{OfficialKeySoftwareBootstrap, OfficialKeySoftwareDelivery}
}
// CompatibleWithProductDomain 判断工作流是否与项目产品域兼容。
// 空 domain 视为不过滤(兼容旧项目);软件 key 在 medical_device 下不可选。
func CompatibleWithProductDomain(workflowKey, productDomain string) bool {
if !IsOfficialWorkflowKey(workflowKey) {
return false
}
domain := productDomain
if domain == "" {
return true
}
allowed, ok := officialProductDomains[workflowKey]
if !ok {
return false
}
for _, d := range allowed {
if d == domain {
return true
}
}
return false
}
+52 -3
View File
@@ -1,17 +1,41 @@
package workflowdef
import "testing"
import (
"strings"
"testing"
)
func TestOfficialWorkflowKeys(t *testing.T) {
keys := OfficialWorkflowKeys()
if len(keys) != 3 {
t.Fatalf("expected 3 official keys, got %d", len(keys))
if len(keys) != 5 {
t.Fatalf("expected 5 official keys, got %d", len(keys))
}
for _, k := range keys {
if !IsOfficialWorkflowKey(k) {
t.Fatalf("key %q not official", k)
}
}
if len(MedicalWorkflowKeys()) != 3 {
t.Fatalf("expected 3 medical keys")
}
if len(SoftwareWorkflowKeys()) != 2 {
t.Fatalf("expected 2 software keys")
}
}
func TestCompatibleWithProductDomain(t *testing.T) {
if !CompatibleWithProductDomain(OfficialKeyMain, "medical_device") {
t.Fatal("main should accept medical_device")
}
if !CompatibleWithProductDomain(OfficialKeySoftwareBootstrap, "non_medical_software") {
t.Fatal("software_bootstrap should accept non_medical_software")
}
if CompatibleWithProductDomain(OfficialKeySoftwareBootstrap, "medical_device") {
t.Fatal("software_bootstrap must reject medical_device")
}
if !CompatibleWithProductDomain(OfficialKeySoftwareDelivery, "") {
t.Fatal("empty domain should not filter")
}
}
func TestValidateDefinitionV2_OfficialDocCounts(t *testing.T) {
@@ -26,6 +50,31 @@ func TestValidateDefinitionV2_OfficialDocCounts(t *testing.T) {
}
}
func TestValidateDefinitionV2_SoftwareManifests(t *testing.T) {
medicalTerms := []string{"临床", "nmpa", "注册检验", "gb 9706", "cdmo", "疗效"}
for _, file := range []string{"software_bootstrap.v2.json", "software_delivery.v2.json"} {
def, err := LoadManifestDefinition(file)
if err != nil {
t.Fatalf("%s: %v", file, err)
}
normalizeManifestDefinition(&def)
res := ValidateDefinitionV2(def)
if !res.Valid {
t.Fatalf("%s invalid: %+v", file, res.Issues)
}
for _, st := range def.Stages {
for _, dt := range st.DocTasks {
blob := strings.ToLower(dt.Title + " " + dt.OutputDocTitle + " " + dt.Key)
for _, bad := range medicalTerms {
if strings.Contains(blob, strings.ToLower(bad)) {
t.Fatalf("%s task %s contains medical term %q", file, dt.Key, bad)
}
}
}
}
}
}
func TestValidateDefinitionV2_RejectsUpstream(t *testing.T) {
def := VersionDefinition{
SchemaVersion: 2,
+5 -3
View File
@@ -2,7 +2,7 @@ package workflowdef
import "context"
// ListProjectSelectableDefinitions 返回项目可选的已发布正式工作流(严格三项白名单)。
// ListProjectSelectableDefinitions 返回项目可选的已发布正式工作流(医疗三 + 软件两白名单)。
func (s *Store) ListProjectSelectableDefinitions(ctx context.Context) ([]Definition, error) {
if !s.enabled() {
return nil, ErrNotAvailable
@@ -18,7 +18,7 @@ func (s *Store) ListProjectSelectableDefinitions(ctx context.Context) ([]Definit
return nil, err
}
defer rows.Close()
out := make([]Definition, 0, 3)
out := make([]Definition, 0, len(OfficialWorkflowKeys()))
for rows.Next() {
var d Definition
if err := rows.Scan(&d.ID, &d.Key, &d.Name, &d.Description, &d.Status, &d.LatestPublishedVersion, &d.ProductDomain,
@@ -39,7 +39,7 @@ func (s *Store) ListProjectSelectableDefinitions(ctx context.Context) ([]Definit
return out, rows.Err()
}
// RetireNonOfficialDefinitions 将非三正式定义标记为不可选。
// RetireNonOfficialDefinitions 将非白名单正式定义标记为不可选。
func (s *Store) RetireNonOfficialDefinitions(ctx context.Context) error {
if !s.enabled() {
return ErrNotAvailable
@@ -59,6 +59,8 @@ func (s *Store) RetireNonOfficialDefinitions(ctx context.Context) error {
WHEN 'main' THEN 10
WHEN 'product_manager' THEN 20
WHEN 'test_manager' THEN 30
WHEN 'software_bootstrap' THEN 40
WHEN 'software_delivery' THEN 50
ELSE catalog_order
END,
updated_at = now()
+6 -4
View File
@@ -25,9 +25,11 @@ func LoadManifestDefinition(filename string) (VersionDefinition, error) {
return def, nil
}
// V2ManifestFiles 三个正式工作流 v2 manifest 文件名。
// V2ManifestFiles 正式工作流 v2 manifest 文件名(医疗三 + 软件两)。
var V2ManifestFiles = map[string]string{
"main": "main.v2.json",
"product_manager": "product_manager.v2.json",
"test_manager": "test_manager.v2.json",
"main": "main.v2.json",
"product_manager": "product_manager.v2.json",
"test_manager": "test_manager.v2.json",
"software_bootstrap": "software_bootstrap.v2.json",
"software_delivery": "software_delivery.v2.json",
}
@@ -0,0 +1,199 @@
{
"schema_version": 2,
"workflow_key": "software_bootstrap",
"product_domains": ["non_medical_software"],
"stages": [
{
"key": "S0_boundary",
"name": "S0 项目边界",
"position": 10,
"optional": false,
"manual_gate": true,
"doc_tasks": [
{
"key": "software_charter",
"title": "项目章程",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "software_charter",
"output_doc_title": "项目章程"
},
{
"key": "source_inventory",
"title": "资料与仓库清单",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "source_inventory",
"output_doc_title": "资料与仓库清单"
}
]
},
{
"key": "S1_current_state",
"name": "S1 现状基线",
"position": 20,
"optional": false,
"manual_gate": true,
"doc_tasks": [
{
"key": "current_state_audit",
"title": "现状审计",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "current_state_audit",
"output_doc_title": "现状审计"
},
{
"key": "architecture_baseline",
"title": "当前架构与依赖图",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "architecture_baseline",
"output_doc_title": "当前架构与依赖图"
},
{
"key": "software_risk_register",
"title": "风险登记册",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "software_risk_register",
"output_doc_title": "风险登记册"
}
]
},
{
"key": "S2_product_baseline",
"name": "S2 产品基线",
"position": 30,
"optional": false,
"manual_gate": true,
"doc_tasks": [
{
"key": "product_requirements",
"title": "产品与系统需求基线",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "product_requirements",
"output_doc_title": "产品与系统需求基线"
},
{
"key": "scope_backlog",
"title": "范围化 backlog",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "scope_backlog",
"output_doc_title": "范围化 backlog"
}
]
},
{
"key": "S3_target_design",
"name": "S3 目标方案",
"position": 40,
"optional": false,
"manual_gate": true,
"doc_tasks": [
{
"key": "target_architecture",
"title": "目标架构方案",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "target_architecture",
"output_doc_title": "目标架构方案"
},
{
"key": "decision_log",
"title": "架构决策记录",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "decision_log",
"output_doc_title": "架构决策记录"
}
]
},
{
"key": "S4_bootstrap_roadmap",
"name": "S4 自举路线",
"position": 50,
"optional": false,
"manual_gate": true,
"doc_tasks": [
{
"key": "bootstrap_roadmap",
"title": "分阶段建设路线",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "bootstrap_roadmap",
"output_doc_title": "分阶段建设路线"
},
{
"key": "verification_strategy",
"title": "验证策略与测试矩阵",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "verification_strategy",
"output_doc_title": "验证策略与测试矩阵"
},
{
"key": "operations_baseline",
"title": "部署运维与恢复基线",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "operations_baseline",
"output_doc_title": "部署运维与恢复基线"
},
{
"key": "bootstrap_readiness",
"title": "自举就绪评审",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "bootstrap_readiness",
"output_doc_title": "自举就绪评审"
}
]
}
],
"completion_policy": {
"suggest_next_workflow": "software_delivery",
"auto_start_next": false
}
}
@@ -0,0 +1,167 @@
{
"schema_version": 2,
"workflow_key": "software_delivery",
"product_domains": ["non_medical_software"],
"required_upstream_workflow": "",
"stages": [
{
"key": "D0_change_definition",
"name": "D0 变更定义",
"position": 10,
"optional": false,
"manual_gate": true,
"doc_tasks": [
{
"key": "change_brief",
"title": "变更简报",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "change_brief",
"output_doc_title": "变更简报"
},
{
"key": "implementation_plan",
"title": "实施计划",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "implementation_plan",
"output_doc_title": "实施计划"
}
]
},
{
"key": "D1_implementation_evidence",
"name": "D1 实施证据",
"position": 20,
"optional": false,
"manual_gate": true,
"doc_tasks": [
{
"key": "change_manifest",
"title": "变更清单",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "change_manifest",
"output_doc_title": "变更清单"
},
{
"key": "adr_delta",
"title": "架构决策增量",
"default_enabled": false,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "adr_delta",
"output_doc_title": "架构决策增量"
}
]
},
{
"key": "D2_verification",
"name": "D2 验证",
"position": 30,
"optional": false,
"manual_gate": true,
"doc_tasks": [
{
"key": "test_report",
"title": "测试报告",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "test_report",
"output_doc_title": "测试报告"
},
{
"key": "security_review",
"title": "安全审查",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "security_review",
"output_doc_title": "安全审查"
}
]
},
{
"key": "D3_release",
"name": "D3 发布",
"position": 40,
"optional": false,
"manual_gate": true,
"doc_tasks": [
{
"key": "release_plan",
"title": "发布计划",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "release_plan",
"output_doc_title": "发布计划"
},
{
"key": "release_notes",
"title": "发布说明",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "release_notes",
"output_doc_title": "发布说明"
}
]
},
{
"key": "D4_learning",
"name": "D4 学习",
"position": 50,
"optional": false,
"manual_gate": true,
"doc_tasks": [
{
"key": "retrospective",
"title": "复盘",
"default_enabled": true,
"creator_configurable": false,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "retrospective",
"output_doc_title": "复盘"
},
{
"key": "backlog_delta",
"title": "Backlog 增量",
"default_enabled": true,
"creator_configurable": true,
"blocking_when_enabled": true,
"executor_key": "software.doc.v1",
"quality_gate_key": "quality.generic.v1",
"output_doc_type": "backlog_delta",
"output_doc_title": "Backlog 增量"
}
]
}
],
"completion_policy": {
"repeatable": true,
"auto_start_next": false
}
}
+2 -4
View File
@@ -69,16 +69,14 @@ func ValidateDefinitionV2(def VersionDefinition) ValidationResult {
if len(def.Stages) == 0 {
issues = append(issues, ValidationIssue{Code: "NO_STAGES", Message: "at least one stage required"})
}
if def.WorkflowKey != "" && IsOfficialWorkflowKey(def.WorkflowKey) {
// 固定文档数仅约束医疗三工作流;软件工作流按自身 manifest 完整性校验。
if def.WorkflowKey != "" && IsMedicalWorkflowKey(def.WorkflowKey) {
if want, ok := officialDocCounts[def.WorkflowKey]; ok && docCount != want {
issues = append(issues, ValidationIssue{
Code: "OFFICIAL_DOC_COUNT", Field: def.WorkflowKey,
Message: fmt.Sprintf("official workflow %s requires exactly %d doc tasks, got %d", def.WorkflowKey, want, docCount),
})
}
if name := OfficialWorkflowName(def.WorkflowKey); name == def.WorkflowKey {
// name check happens at publish layer via Definition.Name
}
}
return ValidationResult{Valid: len(issues) == 0, Issues: issues}
}