diff --git a/.doc/ops/long-term/2026-09-19-audit-remainder.md b/.doc/ops/long-term/2026-09-19-audit-remainder.md index af2b4a5c..c5b26544 100644 --- a/.doc/ops/long-term/2026-09-19-audit-remainder.md +++ b/.doc/ops/long-term/2026-09-19-audit-remainder.md @@ -49,7 +49,7 @@ updated: 2026-09-19 | FE App.tsx | 部分。登录壳不再静态引用导出引擎、文件预览、管理台、块编辑器和图表。入口包 2.83 MB → 1.00 MB(br 626 KB → 235 KB)。`loginShellImports.test.ts`。工作台主体仍在 `App.tsx`,未按 hook 拆文件 | | H1-del | 关闭理由:`allowLegacyTestPrincipal` 仍被 `AllowInMemory && ContextDbHost==""` 的 dev/test 内存测试使用。删除会把这批测试变成未登录 401。保留告警,删除另开、先改测试夹具 | -仍开放:rdev P2 前端规划 UI、P3 三样本真 PI、P4 键名空间化、首次加载 U1/U6/U7、dev 容器重建与治理库 PG。P2 的对话点名已接:`生成项目建议书` 命中 `rd_proposal`,长标题压过短标题。U4 登录扇出收口仍有效,浏览器请求次数证据还没有。H2 批注/审计的项目成员检查在 Postgres 上走 `RequireProjectAccess`;内存授权器在无库时把任意正用户 id 视为成员,这是 `TestAuthorizeDocRevision_ReviewerAndNonMemberCannotWrite` 的隔离前提,不能改成默认拒绝。H2 的 HTTP 负例因此要在 `TEST_DATABASE_URL` 上补,本机未设置该变量。M7 的投影意图已随账本提交(goose `00053`);容器 schema 仍是 52,要等占槽重建后才升到 53。 +仍开放:rdev P2 的 PrerequisiteBanner、P3 三样本真 PI、P4 键名空间化、首次加载 U1/U6/U7、dev 容器重建与治理库 PG。P2 规划界面已能折叠长阶段并批量纳入人工审批。U4 登录扇出收口仍有效,浏览器请求次数证据还没有。H2 批注/审计的项目成员检查在 Postgres 上走 `RequireProjectAccess`;内存授权器在无库时把任意正用户 id 视为成员,这是 `TestAuthorizeDocRevision_ReviewerAndNonMemberCannotWrite` 的隔离前提,不能改成默认拒绝。H2 的 HTTP 负例因此要在 `TEST_DATABASE_URL` 上补,本机未设置该变量。M7 的投影意图已随账本提交(goose `00053`);容器 schema 仍是 52,要等占槽重建后才升到 53。 红线关闭(不是实现): diff --git a/.doc/status/FEATURE-MATRIX.md b/.doc/status/FEATURE-MATRIX.md index 48d444c3..803d2027 100644 --- a/.doc/status/FEATURE-MATRIX.md +++ b/.doc/status/FEATURE-MATRIX.md @@ -57,7 +57,7 @@ | 对话数据治理(D7) | 留存/删除口径、库·备份·日志控制、敏感字段加密边界 | deferred | deferred_enhancement(违规则升 blocker) | [计划](../remediation/2026-09-16-feature-matrix-production-readiness-plan.md) Scope Boundaries;[收口 §7.1](../remediation/2026-09-16-partial-deferred-completion-plan.md);[D7 ADR 草案](../adr/2026-09-17-d7-chat-data-retention.md);[W7 盘点](../audit/2026-09-17-d7-data-inventory.md);[报告](../test-reports/2026-09-17-w7-d7-inventory-test-report.md) | **after W7**:只读盘点完成(sessions/messages/KB/日志/备份/event)。**未发现**已确认基线违规,未写 gap-matrix。口径仍 `pending-compliance-signoff`;禁止按 365/180/30 物理删除。本行不升 done | `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` | -| 研发设计控制(rdev) | 医疗第 4 工作流:13×60 manifest/模板、`squad.rdev_doc.v1` kind 编排、main-completed 硬 precheck、StageKey 动态化;不含前端/真 PI/键名空间化 | partial | deferred_enhancement | [设计](../system-design/project-workflow/07-研发工作流rdev接入-设计与实施计划.md);[short-term](../archive/ops/2026-09-17-rdev-workflow-integration.md);[模板深度](../archive/ops/2026-09-17-rdev-template-depth.md);[S0+S1 首批报告](../test-reports/2026-09-17-rdev-template-depth-test-report.md);[S1 续批报告](../test-reports/2026-09-17-rdev-template-depth-s1-continued-test-report.md);[写作规范](../system-design/project-workflow/08-rdev模板写作规范.md);[backlog](../backlog/2026-09-17-rdev-phase2-backlog.md) | **P0+P1 done**。**模板深度 S0+S1 首批 + S1 续批 done**:60/60 模板已补强;真 PI 抽样 PASS(含 `rd_planning_review` + `rd_design_vv`,项目 6446)。剩余:S3 overlay 与 R2/R3 已接。P2 名称匹配已接:`生成项目建议书` → `rd_proposal`,更长标题优先(`TestIntentRecognition_RDevProposalAndLongerTitleWins`)。P2 前端规划 UI、P3 三样本真 PI E2E、P4 键名空间化仍未做 | `workflow-def` / `rdev-templates` | +| 研发设计控制(rdev) | 医疗第 4 工作流:13×60 manifest/模板、`squad.rdev_doc.v1` kind 编排、main-completed 硬 precheck、StageKey 动态化;不含前端/真 PI/键名空间化 | partial | deferred_enhancement | [设计](../system-design/project-workflow/07-研发工作流rdev接入-设计与实施计划.md);[short-term](../archive/ops/2026-09-17-rdev-workflow-integration.md);[模板深度](../archive/ops/2026-09-17-rdev-template-depth.md);[S0+S1 首批报告](../test-reports/2026-09-17-rdev-template-depth-test-report.md);[S1 续批报告](../test-reports/2026-09-17-rdev-template-depth-s1-continued-test-report.md);[写作规范](../system-design/project-workflow/08-rdev模板写作规范.md);[backlog](../backlog/2026-09-17-rdev-phase2-backlog.md) | **P0+P1 done**。**模板深度 S0+S1 首批 + S1 续批 done**:60/60 模板已补强;真 PI 抽样 PASS(含 `rd_planning_review` + `rd_design_vv`,项目 6446)。剩余:S3 overlay 与 R2/R3 已接。P2 名称匹配已接。规划界面:任务多于 4 的后续阶段默认折叠,可对本阶段批量纳入或取消人工审批(`projectSettingsDuties.test.ts`);工作流条在目录不可用时仍显示「研发设计控制流程」。PrerequisiteBanner、P3 三样本真 PI、P4 键名空间化仍未做 | `workflow-def` / `rdev-templates` | | 纯软件项目自举 | 软件项目创建、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);[W5a](../test-reports/2026-09-17-w5a-software-bootstrap-ui-pi-test-report.md) | **before**:L1 + UI 仍克隆医疗模板。**after W5a**:catalog 驱动创建、软件不挂医疗 key、docs_only 真 PI 建议 delivery 且不自动激活。**SWBOOT-10 仍 stub**(W5b);本行不升 done | `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` | diff --git a/web/src/components/project/ProjectSettingsModal.tsx b/web/src/components/project/ProjectSettingsModal.tsx index 9231d22f..dda044b2 100644 --- a/web/src/components/project/ProjectSettingsModal.tsx +++ b/web/src/components/project/ProjectSettingsModal.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { X, ChevronRight, ChevronLeft, CheckCircle2, AlertCircle, Loader2 } from 'lucide-react'; +import { X, ChevronRight, ChevronDown, ChevronLeft, CheckCircle2, AlertCircle, Loader2 } from 'lucide-react'; import { fetchProjectSettings, fetchDefaultWorkflowPlan, @@ -36,6 +36,9 @@ import { resolveClientDocTaskDuty, toggleAuthorOnly, toggleHumanApproval, + stageStartsCollapsed, + applyStageHumanApproval, + stageApprovalKeys, } from '../../lib/projectSettingsDuties'; import { ACTIVATION_UNMANAGED_FREEZE_HINT } from '../../lib/blockedGovernanceCopy'; import type { ProjectSettingsIssue } from '../../api/projectSettingsError'; @@ -69,6 +72,7 @@ export const ProjectSettingsModal: React.FC = ({ const [error, setError] = useState(null); const [settings, setSettings] = useState(null); const [plan, setPlan] = useState(null); + const [openStages, setOpenStages] = useState>({}); const [preview, setPreview] = useState(null); const [selectedKey, setSelectedKey] = useState(''); const [terminateReason, setTerminateReason] = useState(''); @@ -148,6 +152,24 @@ export const ProjectSettingsModal: React.FC = ({ setPlan({ ...plan, stages }); }; + const applyStageApproval = (stageIdx: number, managed: boolean) => { + if (!plan) return; + const stage = plan.stages[stageIdx]; + if (!stage) return; + const keys = stageApprovalKeys(stage.doc_tasks, showEditor); + const stages = plan.stages.map((item, index) => + index === stageIdx + ? { ...item, doc_tasks: applyStageHumanApproval(item.doc_tasks, managed, showEditor) } + : item, + ); + setPlan({ ...plan, stages }); + setRequireApprovalKeys((current) => { + if (managed) return Array.from(new Set([...current, ...keys])); + const drop = new Set(keys); + return current.filter((key) => !drop.has(key)); + }); + }; + const handleSavePlan = async () => { if (!plan || !settings || !settings.can_edit_plan) return; @@ -499,10 +521,42 @@ export const ProjectSettingsModal: React.FC = ({ 请先保存一次规划以从工作流定义加载阶段结构,或返回上一步重新选择。 )} - {plan.stages.map((st, si) => ( + {plan.stages.map((st, si) => { + const expanded = openStages[st.stage_key] ?? !stageStartsCollapsed(st.doc_tasks.length, si); + return (
-
{st.stage_name || st.stage_key}
- {st.doc_tasks.map((dt, ti) => { + + {expanded && showEditor && ( +
+ + +
+ )} + {expanded && st.doc_tasks.map((dt, ti) => { const resolved = resolveClientDocTaskDuty(dt); const managed = resolved.mode !== 'none' || @@ -627,7 +681,8 @@ export const ProjectSettingsModal: React.FC = ({ ); })}
- ))} + ); + })} )} diff --git a/web/src/components/workflow/ProjectWorkflowSwitcher.tsx b/web/src/components/workflow/ProjectWorkflowSwitcher.tsx index 2f0f5356..b204a32c 100644 --- a/web/src/components/workflow/ProjectWorkflowSwitcher.tsx +++ b/web/src/components/workflow/ProjectWorkflowSwitcher.tsx @@ -18,6 +18,7 @@ const FALLBACK_OPTIONS = [ { key: 'main', label: '商务对接流程', icon: '⚡' }, { key: 'product_manager', label: '智能产品经理流程', icon: '📋' }, { key: 'test_manager', label: '智能测试工程师流程', icon: '🧪' }, + { key: 'rdev', label: '研发设计控制流程', icon: '📐' }, ]; export const ProjectWorkflowSwitcher: React.FC = ({ diff --git a/web/src/lib/projectSettingsDuties.test.ts b/web/src/lib/projectSettingsDuties.test.ts index 219851a3..b902a82d 100644 --- a/web/src/lib/projectSettingsDuties.test.ts +++ b/web/src/lib/projectSettingsDuties.test.ts @@ -10,6 +10,9 @@ import { resolveClientDocTaskDuty, toggleAuthorOnly, toggleHumanApproval, + stageStartsCollapsed, + applyStageHumanApproval, + stageApprovalKeys, } from './projectSettingsDuties'; function plan(tasks: WorkflowPlan['stages'][number]['doc_tasks']): WorkflowPlan { @@ -170,4 +173,21 @@ describe('projectSettingsDuties', () => { assert.equal(off.duty_policy, ''); assert.deepEqual(off.duties, { author_user_id: 7 }); }); + + it('folds later rdev stages and batches human approval keys', () => { + assert.equal(stageStartsCollapsed(2, 0), false); + assert.equal(stageStartsCollapsed(12, 0), false); + assert.equal(stageStartsCollapsed(12, 1), true); + const tasks = [ + { task_key: 'rd_proposal', enabled: true, approval_configurable: true, duties: { author_user_id: 1, reviewer_user_id: 2, approver_user_id: 3 } }, + { task_key: 'rd_design_plan', enabled: true, approval_configurable: false, duties: {} }, + { task_key: 'rd_sw_plan', enabled: false, approval_configurable: true, duties: {} }, + ]; + assert.deepEqual(stageApprovalKeys(tasks, true), ['rd_proposal']); + const cleared = applyStageHumanApproval(tasks, false, true); + assert.equal(cleared[0].duty_policy, ''); + assert.deepEqual(cleared[0].duties, {}); + assert.equal(cleared[1].approval_configurable, false); + assert.equal(cleared[2].enabled, false); + }); }); diff --git a/web/src/lib/projectSettingsDuties.ts b/web/src/lib/projectSettingsDuties.ts index 98ed3a9c..a6c08abd 100644 --- a/web/src/lib/projectSettingsDuties.ts +++ b/web/src/lib/projectSettingsDuties.ts @@ -55,6 +55,28 @@ export function toggleHumanApproval(task: DocTaskPlan, managed: boolean): DocTas return { ...task }; } +// 研发流程一个阶段常有十几份文档。第一阶段保持展开,后面任务较多的阶段先折叠。 +export function stageStartsCollapsed(taskCount: number, stageIndex: number): boolean { + return stageIndex > 0 && taskCount > 4; +} + +export function applyStageHumanApproval( + tasks: DocTaskPlan[], + managed: boolean, + planning: boolean, +): DocTaskPlan[] { + return tasks.map((task) => { + if (!task.enabled || !canToggleHumanApproval(task, planning)) return task; + return toggleHumanApproval(task, managed); + }); +} + +export function stageApprovalKeys(tasks: DocTaskPlan[], planning: boolean): string[] { + return tasks + .filter((task) => task.enabled && canToggleHumanApproval(task, planning)) + .map((task) => task.task_key); +} + /** 切换兼任模式:开启时只保留编制人并清空审核/批准;关闭时清除 duty_policy。 */ export function toggleAuthorOnly(task: DocTaskPlan, enabled: boolean): DocTaskPlan { if (!enabled) {