From a30931349a928ff7ceacb8155b656e33a41fe382 Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 16 Sep 2026 15:35:03 +0000 Subject: [PATCH 1/6] docs(adr): SWBOOT-00 software_bootstrap/delivery keys and domain boundary Freeze selectable workflow keys, domain/compliance separation, solo policy, and SWBOOT-10 fail-closed stub contract without touching medical three. Co-authored-by: Cursor --- ...6-09-16-software-project-bootstrap-keys.md | 85 +++++++++++++++++++ .doc/ops/CURRENT.md | 1 + .../2026-09-16-software-project-bootstrap.md | 55 ++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 .doc/adr/2026-09-16-software-project-bootstrap-keys.md create mode 100644 .doc/ops/short-term/2026-09-16-software-project-bootstrap.md diff --git a/.doc/adr/2026-09-16-software-project-bootstrap-keys.md b/.doc/adr/2026-09-16-software-project-bootstrap-keys.md new file mode 100644 index 00000000..c33e71ed --- /dev/null +++ b/.doc/adr/2026-09-16-software-project-bootstrap-keys.md @@ -0,0 +1,85 @@ +# ADR:纯软件工作流 key 与领域边界(SWBOOT-00) + +## 状态 + +已接受(2026-09-16) + +## 背景 + +工作流引擎已声明 `non_medical_software` 产品域,但正式目录、创建入口与模板仍以医疗器械三工作流(`main` / `product_manager` / `test_manager`)为硬编码白名单。继续复用医疗 manifest 会造成临床/注册/CDO 语义污染;直接改写既有三 key 会破坏已激活医疗实例。 + +详细设计见 [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` diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 99ea1d3d..6e4aa175 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -13,6 +13,7 @@ | owner | task | branch | worktree | mutex | status | |-------|------|--------|----------|-------|--------| +| Cursor-Auto | 2026-09-16-software-project-bootstrap | feature/software-project-bootstrap | - | workflow-design | in_progress | | Cursor | 2026-09-16-full-backlog-override | docs/override-nogo-full-backlog | - | architecture-docs | in_progress | | Cursor | 2026-09-16-gitea-nightly-docker-datadir | fix/gitea-nightly-docker-datadir | - | ci | done | | Cursor | 2026-09-16-gitea-nightly-host-tools | fix/gitea-nightly-host-tools | - | ci | done | diff --git a/.doc/ops/short-term/2026-09-16-software-project-bootstrap.md b/.doc/ops/short-term/2026-09-16-software-project-bootstrap.md new file mode 100644 index 00000000..2f2785b7 --- /dev/null +++ b/.doc/ops/short-term/2026-09-16-software-project-bootstrap.md @@ -0,0 +1,55 @@ +--- +title: "纯软件项目自举 SWBOOT-00..09 实施" +status: in_progress +created: 2026-09-16 +updated: 2026-09-16 +owner: Cursor-Auto +branch: feature/software-project-bootstrap +worktree: "-" +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. [ ] ADR +3. [ ] goose 00048 +4. [ ] manifests + catalog allowlist +5. [ ] create-project wizard hooks + SWBOOT-10 stub +6. [ ] unit tests + test-report +7. [ ] push feature 分支 + +## 验收标准 + +- [ ] 医疗三工作流目录与校验无回归 +- [ ] 软件两 key 可选且无医疗默认语义 +- [ ] 迁移 additive,旧项目不误分类 +- [ ] SWBOOT-10 仅 stub,拒绝真实执行 +- [ ] 测试报告落盘 + +## 相关链接 + +- 设计: `.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` From d661af71d5e63b52f281fcec7d86934af588659a Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 16 Sep 2026 15:36:13 +0000 Subject: [PATCH 2/6] feat(db): SWBOOT-01 additive project type and compliance schema Add project domain/governance fields, catalog tables, source bindings, and repo allowlist stub table without backfilling legacy classification. Co-authored-by: Cursor --- ...9-16-software-project-bootstrap-fields.sql | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 deploy/postgres/goose/00048_2026-09-16-software-project-bootstrap-fields.sql diff --git a/deploy/postgres/goose/00048_2026-09-16-software-project-bootstrap-fields.sql b/deploy/postgres/goose/00048_2026-09-16-software-project-bootstrap-fields.sql new file mode 100644 index 00000000..7436348c --- /dev/null +++ b/deploy/postgres/goose/00048_2026-09-16-software-project-bootstrap-fields.sql @@ -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'; From 909384c3352f9650be094a924eee32a245d1a0f2 Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 16 Sep 2026 15:38:46 +0000 Subject: [PATCH 3/6] feat(workflow): SWBOOT-02..04 software manifests and catalog allowlist Add software_bootstrap/delivery v2 manifests, expand selectable keys to five, keep medical doc-count gates, and filter catalog by product_domain. Co-authored-by: Cursor --- internal/projectinstance/validator.go | 2 +- internal/server/workflow_catalog_handlers.go | 18 +- internal/workflow/registry.go | 1 + internal/workflowdef/bootstrap.go | 12 +- internal/workflowdef/catalog_policy.go | 94 ++++++++- internal/workflowdef/catalog_policy_test.go | 55 ++++- internal/workflowdef/catalog_store.go | 8 +- internal/workflowdef/manifest_loader.go | 10 +- .../manifests/software_bootstrap.v2.json | 199 ++++++++++++++++++ .../manifests/software_delivery.v2.json | 167 +++++++++++++++ internal/workflowdef/validator_v2.go | 6 +- 11 files changed, 543 insertions(+), 29 deletions(-) create mode 100644 internal/workflowdef/manifests/software_bootstrap.v2.json create mode 100644 internal/workflowdef/manifests/software_delivery.v2.json diff --git a/internal/projectinstance/validator.go b/internal/projectinstance/validator.go index 2e675679..fb33db0e 100644 --- a/internal/projectinstance/validator.go +++ b/internal/projectinstance/validator.go @@ -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 { diff --git a/internal/server/workflow_catalog_handlers.go b/internal/server/workflow_catalog_handlers.go index bce101d7..3174122b 100644 --- a/internal/server/workflow_catalog_handlers.go +++ b/internal/server/workflow_catalog_handlers.go @@ -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}) diff --git a/internal/workflow/registry.go b/internal/workflow/registry.go index 9481a38b..3ddba2ed 100644 --- a/internal/workflow/registry.go +++ b/internal/workflow/registry.go @@ -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 确定性质量门禁注册表。 diff --git a/internal/workflowdef/bootstrap.go b/internal/workflowdef/bootstrap.go index f4ab5fbc..e283131c 100644 --- a/internal/workflowdef/bootstrap.go +++ b/internal/workflowdef/bootstrap.go @@ -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 diff --git a/internal/workflowdef/catalog_policy.go b/internal/workflowdef/catalog_policy.go index 13f2edf0..d6a41c31 100644 --- a/internal/workflowdef/catalog_policy.go +++ b/internal/workflowdef/catalog_policy.go @@ -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 +} diff --git a/internal/workflowdef/catalog_policy_test.go b/internal/workflowdef/catalog_policy_test.go index 29d95504..9919a775 100644 --- a/internal/workflowdef/catalog_policy_test.go +++ b/internal/workflowdef/catalog_policy_test.go @@ -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, diff --git a/internal/workflowdef/catalog_store.go b/internal/workflowdef/catalog_store.go index eb9bf1a9..9d8f0b78 100644 --- a/internal/workflowdef/catalog_store.go +++ b/internal/workflowdef/catalog_store.go @@ -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() diff --git a/internal/workflowdef/manifest_loader.go b/internal/workflowdef/manifest_loader.go index 49f4dbc4..420ba14f 100644 --- a/internal/workflowdef/manifest_loader.go +++ b/internal/workflowdef/manifest_loader.go @@ -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", } diff --git a/internal/workflowdef/manifests/software_bootstrap.v2.json b/internal/workflowdef/manifests/software_bootstrap.v2.json new file mode 100644 index 00000000..3ccf584b --- /dev/null +++ b/internal/workflowdef/manifests/software_bootstrap.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 + } +} diff --git a/internal/workflowdef/manifests/software_delivery.v2.json b/internal/workflowdef/manifests/software_delivery.v2.json new file mode 100644 index 00000000..947d8b7d --- /dev/null +++ b/internal/workflowdef/manifests/software_delivery.v2.json @@ -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 + } +} diff --git a/internal/workflowdef/validator_v2.go b/internal/workflowdef/validator_v2.go index a5240f69..4dec223b 100644 --- a/internal/workflowdef/validator_v2.go +++ b/internal/workflowdef/validator_v2.go @@ -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} } From f6034fad3796ce10a6fe97aa21eefe8ddc036631 Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 16 Sep 2026 15:43:13 +0000 Subject: [PATCH 4/6] feat(api): SWBOOT-05..08 wizard hooks and SWBOOT-10 allowlist stub Add project-types/compliance catalogs, creation-preview, software create fields, and fail-closed repo allowlist stub without real patch/merge. Co-authored-by: Cursor --- .doc/system-design/module-boundaries.yaml | 2 + internal/projectworkflow/catalog.go | 26 +- internal/projectworkflow/service.go | 23 +- internal/projectworkflow/service_test.go | 51 +++- internal/projectworkflow/store.go | 25 +- internal/projectworkflow/types.go | 30 ++- internal/server/server.go | 3 + internal/server/software_project_handlers.go | 60 +++++ internal/swboot/catalog.go | 256 +++++++++++++++++++ internal/swboot/catalog_test.go | 92 +++++++ internal/swboot/repo_allowlist.go | 76 ++++++ 11 files changed, 636 insertions(+), 8 deletions(-) create mode 100644 internal/server/software_project_handlers.go create mode 100644 internal/swboot/catalog.go create mode 100644 internal/swboot/catalog_test.go create mode 100644 internal/swboot/repo_allowlist.go diff --git a/.doc/system-design/module-boundaries.yaml b/.doc/system-design/module-boundaries.yaml index b9b7c460..e81d9e5f 100644 --- a/.doc/system-design/module-boundaries.yaml +++ b/.doc/system-design/module-boundaries.yaml @@ -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 diff --git a/internal/projectworkflow/catalog.go b/internal/projectworkflow/catalog.go index ec42b490..b6960948 100644 --- a/internal/projectworkflow/catalog.go +++ b/internal/projectworkflow/catalog.go @@ -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 } diff --git a/internal/projectworkflow/service.go b/internal/projectworkflow/service.go index 3f79091e..a5e24a40 100644 --- a/internal/projectworkflow/service.go +++ b/internal/projectworkflow/service.go @@ -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, diff --git a/internal/projectworkflow/service_test.go b/internal/projectworkflow/service_test.go index 266f8f0f..81a3d48a 100644 --- a/internal/projectworkflow/service_test.go +++ b/internal/projectworkflow/service_test.go @@ -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) + } +} + diff --git a/internal/projectworkflow/store.go b/internal/projectworkflow/store.go index 81f6731f..a2387e54 100644 --- a/internal/projectworkflow/store.go +++ b/internal/projectworkflow/store.go @@ -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, diff --git a/internal/projectworkflow/types.go b/internal/projectworkflow/types.go index e1d2e866..18275e41 100644 --- a/internal/projectworkflow/types.go +++ b/internal/projectworkflow/types.go @@ -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"` } diff --git a/internal/server/server.go b/internal/server/server.go index a459929b..903a6b16 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -166,6 +166,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) diff --git a/internal/server/software_project_handlers.go b/internal/server/software_project_handlers.go new file mode 100644 index 00000000..35d0667f --- /dev/null +++ b/internal/server/software_project_handlers.go @@ -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) +} diff --git a/internal/swboot/catalog.go b/internal/swboot/catalog.go new file mode 100644 index 00000000..c06cde09 --- /dev/null +++ b/internal/swboot/catalog.go @@ -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" +} diff --git a/internal/swboot/catalog_test.go b/internal/swboot/catalog_test.go new file mode 100644 index 00000000..da5e6926 --- /dev/null +++ b/internal/swboot/catalog_test.go @@ -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") + } +} diff --git a/internal/swboot/repo_allowlist.go b/internal/swboot/repo_allowlist.go new file mode 100644 index 00000000..998a6821 --- /dev/null +++ b/internal/swboot/repo_allowlist.go @@ -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) +} From 01a783b8d5bf742ad131fee2dedf524fc2d1d02d Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 16 Sep 2026 15:45:08 +0000 Subject: [PATCH 5/6] test(swboot): SWBOOT-09 L1 suite and acceptance report Update catalog size expectations to five workflows and archive the unit-test report; sync FEATURE-MATRIX and task docs without merging to dev. Co-authored-by: Cursor --- .doc/ops/CURRENT.md | 2 +- ...ftware-project-bootstrap-implementation.md | 15 +- .../2026-09-16-software-project-bootstrap.md | 24 ++-- .doc/status/FEATURE-MATRIX.md | 2 +- ...-software-project-bootstrap-test-report.md | 135 ++++++++++++++++++ internal/server/project_workflow_api_test.go | 8 +- 6 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 .doc/test-reports/2026-09-16-software-project-bootstrap-test-report.md diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 6e4aa175..18c72e19 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -13,7 +13,7 @@ | owner | task | branch | worktree | mutex | status | |-------|------|--------|----------|-------|--------| -| Cursor-Auto | 2026-09-16-software-project-bootstrap | feature/software-project-bootstrap | - | workflow-design | in_progress | +| Cursor-Auto | 2026-09-16-software-project-bootstrap | feature/software-project-bootstrap | .worktrees/feature-software-project-bootstrap | workflow-design | in_progress | | Cursor | 2026-09-16-full-backlog-override | docs/override-nogo-full-backlog | - | architecture-docs | in_progress | | Cursor | 2026-09-16-gitea-nightly-docker-datadir | fix/gitea-nightly-docker-datadir | - | ci | done | | Cursor | 2026-09-16-gitea-nightly-host-tools | fix/gitea-nightly-host-tools | - | ci | done | diff --git a/.doc/ops/long-term/2026-09-14-software-project-bootstrap-implementation.md b/.doc/ops/long-term/2026-09-14-software-project-bootstrap-implementation.md index eff609ea..5dc1915a 100644 --- a/.doc/ops/long-term/2026-09-14-software-project-bootstrap-implementation.md +++ b/.doc/ops/long-term/2026-09-14-software-project-bootstrap-implementation.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 | 范围 | 依赖 | 验收 | diff --git a/.doc/ops/short-term/2026-09-16-software-project-bootstrap.md b/.doc/ops/short-term/2026-09-16-software-project-bootstrap.md index 2f2785b7..1707ad4d 100644 --- a/.doc/ops/short-term/2026-09-16-software-project-bootstrap.md +++ b/.doc/ops/short-term/2026-09-16-software-project-bootstrap.md @@ -5,7 +5,7 @@ created: 2026-09-16 updated: 2026-09-16 owner: Cursor-Auto branch: feature/software-project-bootstrap -worktree: "-" +worktree: ".worktrees/feature-software-project-bootstrap" mutex: workflow-design --- @@ -33,23 +33,25 @@ mutex: workflow-design ## 步骤 1. [x] 认领 CURRENT + short-term -2. [ ] ADR -3. [ ] goose 00048 -4. [ ] manifests + catalog allowlist -5. [ ] create-project wizard hooks + SWBOOT-10 stub -6. [ ] unit tests + test-report +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 分支 ## 验收标准 -- [ ] 医疗三工作流目录与校验无回归 -- [ ] 软件两 key 可选且无医疗默认语义 -- [ ] 迁移 additive,旧项目不误分类 -- [ ] SWBOOT-10 仅 stub,拒绝真实执行 -- [ ] 测试报告落盘 +- [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` diff --git a/.doc/status/FEATURE-MATRIX.md b/.doc/status/FEATURE-MATRIX.md index 61987c24..19369d13 100644 --- a/.doc/status/FEATURE-MATRIX.md +++ b/.doc/status/FEATURE-MATRIX.md @@ -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` | diff --git a/.doc/test-reports/2026-09-16-software-project-bootstrap-test-report.md b/.doc/test-reports/2026-09-16-software-project-bootstrap-test-report.md new file mode 100644 index 00000000..93dfb47f --- /dev/null +++ b/.doc/test-reports/2026-09-16-software-project-bootstrap-test-report.md @@ -0,0 +1,135 @@ +# 纯软件项目自举(SWBOOT-00..09)测试报告 + +## 一、 测试背景 (Background) + +工作流引擎已声明 `non_medical_software`,但正式目录与创建入口仍硬编码医疗三工作流。本轮在 `feature/software-project-bootstrap` 落地 SWBOOT-00..09 契约、迁移、manifest、目录白名单、创建向导后端钩子,以及 SWBOOT-10 allowlist stub(无真实 patch/merge)。医疗既有三工作流保持不变。 + +## 二、 测试目标 (Test Objectives) + +- `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) + +| 项 | 值 | +|----|-----| +| 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) + +| 用例 | 预期 | +|------|------| +| 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) + +### 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) + +命令: + +```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) + +- **结论**:SWBOOT-00..09 工程门禁(L1)达到合入候选标准;**未**达到完整验收清单完成定义(真实 PI / UI / PG 迁移活验仍为 NOT RUN)。 +- **未合 `dev`**:按要求仅推送 feature 分支。 +- **后续**:schema 48 在集成槽位对空库/旧库升级;七步 UI;对话路由与软件 profile;SWBOOT-10 真受控执行独立 Epic。 diff --git a/internal/server/project_workflow_api_test.go b/internal/server/project_workflow_api_test.go index 8043b051..5af9cd78 100644 --- a/internal/server/project_workflow_api_test.go +++ b/internal/server/project_workflow_api_test.go @@ -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{ From b5ce947e7768c951bbb62ca3dd52904b9799b614 Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 16 Sep 2026 15:45:34 +0000 Subject: [PATCH 6/6] docs(ops): mark SWBOOT-00..09 feature slice done after push Record remote branch tip and release workflow-design mutex; still not merged to dev. Co-authored-by: Cursor --- .doc/ops/CURRENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 18c72e19..daad36a6 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -13,7 +13,7 @@ | owner | task | branch | worktree | mutex | status | |-------|------|--------|----------|-------|--------| -| Cursor-Auto | 2026-09-16-software-project-bootstrap | feature/software-project-bootstrap | .worktrees/feature-software-project-bootstrap | workflow-design | in_progress | +| Cursor-Auto | 2026-09-16-software-project-bootstrap | feature/software-project-bootstrap | .worktrees/feature-software-project-bootstrap | workflow-design | done | | Cursor | 2026-09-16-full-backlog-override | docs/override-nogo-full-backlog | - | architecture-docs | in_progress | | Cursor | 2026-09-16-gitea-nightly-docker-datadir | fix/gitea-nightly-docker-datadir | - | ci | done | | Cursor | 2026-09-16-gitea-nightly-host-tools | fix/gitea-nightly-host-tools | - | ci | done |