From c0e7df5abf69dec5ea93859ac07a5c9f3abf91c6 Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 17 Sep 2026 14:07:52 +0000 Subject: [PATCH 1/3] feat(docgov): expose proposal HTTP and wire frontend decide flow Add POST /api/v1/docgov/proposal for ProposeModification, harden decide status mapping, and replace App pendingProposal local mock with real API calls. Co-authored-by: Cursor --- cmd/server/main.go | 1 + internal/server/handlers.go | 73 ++++++++++++ internal/server/server.go | 1 + internal/server/server_test.go | 61 ++++++++++ web/src/App.tsx | 201 ++++++++++++++++++++++++++------- web/src/api/docgov.test.ts | 77 +++++++++++++ web/src/api/docgov.ts | 41 +++++++ 7 files changed, 412 insertions(+), 43 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 71b260ab..c581b3d0 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -70,6 +70,7 @@ func main() { fmt.Println(" • [GET] /api/v1/context/cases/recommend - 往期受控标杆案例与历史报价语义自推荐") fmt.Println(" • [GET] /api/v1/docgov/document - 查询受控文档治理状态看板") fmt.Println(" • [POST] /api/v1/docgov/claim - 查阅并认领受控文档 (正式稿方可认领)") + fmt.Println(" • [POST] /api/v1/docgov/proposal - 对已认领文档发起变更提案") fmt.Println(" • [POST] /api/v1/docgov/proposal/decide - 编制负责人拍板同意/拒绝变更提案") fmt.Println(" • [POST] /api/v1/docgov/submit-review - 编制完成提交审核 (文档正文锁定)") fmt.Println(" • [POST] /api/v1/docgov/review-decision - 审核通过或驳回 (驳回必填原因且解锁)") diff --git a/internal/server/handlers.go b/internal/server/handlers.go index bd334210..95b1c663 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "net/http" "path/filepath" "strconv" @@ -735,6 +736,66 @@ func (s *Server) handleDocGovApproverDecision(w http.ResponseWriter, r *http.Req writeJSONResponse(w, http.StatusOK, doc) } +// ProposeModificationRequest 发起变更提案请求 +type ProposeModificationRequest struct { + DocID string `json:"doc_id"` + TriggeredBy string `json:"triggered_by"` + ProposedChanges string `json:"proposed_changes"` + ImpactAnalysis string `json:"impact_analysis"` + RiskAssessment string `json:"risk_assessment"` +} + +// handleDocGovProposeModification 对已认领文档发起变更提案(写库 + 审计) +func (s *Server) handleDocGovProposeModification(w http.ResponseWriter, r *http.Request) { + user, ok := s.requireAuthenticatedUser(w, r) + if !ok { + return + } + + var req ProposeModificationRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error()) + return + } + if strings.TrimSpace(req.DocID) == "" { + writeSafeError(w, r, http.StatusBadRequest, "doc_id cannot be empty") + return + } + if strings.TrimSpace(req.ProposedChanges) == "" { + writeSafeError(w, r, http.StatusBadRequest, "proposed_changes cannot be empty") + return + } + + if s.gov == nil { + writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized") + return + } + + triggeredBy := strings.TrimSpace(req.TriggeredBy) + if triggeredBy == "" { + triggeredBy = fmt.Sprintf("user:%d", user.ID) + } + + proposal, err := s.gov.ProposeModification( + r.Context(), + req.DocID, + triggeredBy, + req.ProposedChanges, + req.ImpactAnalysis, + req.RiskAssessment, + ) + if err != nil { + if errors.Is(err, docgov.ErrDocumentNotFound) { + writeSafeError(w, r, http.StatusNotFound, "document not found") + return + } + writeSafeError(w, r, http.StatusBadRequest, "propose modification failed: "+err.Error()) + return + } + + writeJSONResponse(w, http.StatusOK, proposal) +} + // DecideProposalRequest 拍板变更提案请求 type DecideProposalRequest struct { ProposalID string `json:"proposal_id"` @@ -755,6 +816,10 @@ func (s *Server) handleDocGovDecideProposal(w http.ResponseWriter, r *http.Reque writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error()) return } + if strings.TrimSpace(req.ProposalID) == "" { + writeSafeError(w, r, http.StatusBadRequest, "proposal_id cannot be empty") + return + } if s.gov == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized") @@ -763,6 +828,14 @@ func (s *Server) handleDocGovDecideProposal(w http.ResponseWriter, r *http.Reque proposal, err := s.gov.DecideModificationProposal(r.Context(), req.ProposalID, user.ID, req.Approve, req.Reason) if err != nil { + if errors.Is(err, docgov.ErrProposalNotFound) { + writeSafeError(w, r, http.StatusNotFound, "proposal not found") + return + } + if errors.Is(err, docgov.ErrUnauthorizedActor) { + writeSafeError(w, r, http.StatusForbidden, "decide proposal failed: "+err.Error()) + return + } writeSafeError(w, r, http.StatusBadRequest, "decide proposal failed: "+err.Error()) return } diff --git a/internal/server/server.go b/internal/server/server.go index 59d25d07..630b3276 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -276,6 +276,7 @@ func (s *Server) Routes() http.Handler { mux.HandleFunc("POST /api/v1/docgov/submit-review", s.handleDocGovSubmitReview) mux.HandleFunc("POST /api/v1/docgov/review-decision", s.handleDocGovReviewDecision) mux.HandleFunc("POST /api/v1/docgov/approver-decision", s.handleDocGovApproverDecision) + mux.HandleFunc("POST /api/v1/docgov/proposal", s.handleDocGovProposeModification) mux.HandleFunc("POST /api/v1/docgov/proposal/decide", s.handleDocGovDecideProposal) mux.HandleFunc("GET /api/v1/docgov/comments", s.handleDocGovListComments) mux.HandleFunc("POST /api/v1/docgov/comments", s.handleDocGovAddComment) diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 730436ae..589a6faa 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -18,6 +18,7 @@ import ( "chat-agent-system/internal/bizdev" "chat-agent-system/internal/chat" "chat-agent-system/internal/config" + "chat-agent-system/internal/docgov" "chat-agent-system/internal/stream" ) @@ -461,3 +462,63 @@ func TestServer_ContextAndDocGovAPIs(t *testing.T) { t.Fatalf("expected 200 from audit logs, got %d", w.Code) } } + +func TestServer_DocGovProposeAndDecideHTTP(t *testing.T) { + srv := newTestServer(t) + token := loginTestAdmin(t, srv) + ctx := context.Background() + docID := "doc_proj1_proposal_http" + + if _, err := srv.gov.CreateOrGetDocument(ctx, docID, 1, "feasibility_reg", "法规评估", "V0.1", "", 1, "admin", 2, "r", 3, "a"); err != nil { + t.Fatalf("CreateOrGetDocument: %v", err) + } + if _, err := srv.gov.RecordDocumentViewed(ctx, docID, 1, docgov.RoleAuthor, "admin"); err != nil { + t.Fatalf("claim: %v", err) + } + + proposeBody, _ := json.Marshal(ProposeModificationRequest{ + DocID: docID, + TriggeredBy: "web_ui", + ProposedChanges: "补充 EMC 滤波指标", + ImpactAnalysis: "影响原理图选型", + RiskAssessment: "低风险", + }) + req := httptest.NewRequest(http.MethodPost, "/api/v1/docgov/proposal", bytes.NewReader(proposeBody)) + withBearerAuth(req, token) + w := httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("propose want 200 got %d %s", w.Code, w.Body.String()) + } + var prop map[string]any + if err := json.NewDecoder(w.Body).Decode(&prop); err != nil { + t.Fatalf("decode propose: %v", err) + } + proposalID, _ := prop["proposal_id"].(string) + if proposalID == "" { + t.Fatalf("missing proposal_id: %#v", prop) + } + if status, _ := prop["status"].(string); status != "pending_author_approval" { + t.Fatalf("want pending_author_approval got %v", prop["status"]) + } + + decideBody, _ := json.Marshal(DecideProposalRequest{ + ProposalID: proposalID, + Approve: true, + Reason: "同意补充", + }) + req = httptest.NewRequest(http.MethodPost, "/api/v1/docgov/proposal/decide", bytes.NewReader(decideBody)) + withBearerAuth(req, token) + w = httptest.NewRecorder() + srv.Routes().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("decide want 200 got %d %s", w.Code, w.Body.String()) + } + var decided map[string]any + if err := json.NewDecoder(w.Body).Decode(&decided); err != nil { + t.Fatalf("decode decide: %v", err) + } + if status, _ := decided["status"].(string); status != "approved_by_author" { + t.Fatalf("want approved_by_author got %v", decided["status"]) + } +} diff --git a/web/src/App.tsx b/web/src/App.tsx index 6dff7349..34f13329 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -55,6 +55,8 @@ import { submitForReview, reviewDecision, approverDecision, + proposeModification, + decideModificationProposal, mapLeaseToAstEditLease, type DocGovRecord, } from './api/docgov'; @@ -1557,11 +1559,13 @@ export default function App() { const [commentingBlock, setCommentingBlock] = useState(null); const [highlightedBlockId, setHighlightedBlockId] = useState(null); const [pendingProposal, setPendingProposal] = useState<{ + proposalId: string; changes: string; impact: string; risk: string; patchBlockId?: string; } | null>(null); + const [proposalError, setProposalError] = useState(null); // 消息流:权威来源为服务端窗口;不再全量持久化到 localStorage const [messages, setMessages] = useState([]); @@ -3188,24 +3192,56 @@ export default function App() { if (text.includes('修改') || text.includes('更新') || text.includes('调整')) { if (docAST.isClaimed && currentDocRole === 'author') { - setPendingProposal({ - changes: '拟在第2节「监管分类与强制性合规标准矩阵」中,强化补充高频电刀共模抑制比设计指标。', - impact: '将直接影响电路原理图物料选型与型式检验 EMC 滤波网络参数。', - risk: '低风险。符合 GB 9706.1-2020 强标,提升系统抗扰度,无侵权风险。', - }); - - setMessages((prev) => [ - ...prev, - { - id: 'ast-' + Date.now(), - sessionId: activeSessionId, - role: 'assistant', - authorName: '水木东方协同参谋', - intentTag: '🛡️ 文档认领保护机制拦截', - content: `检测到《${docAST.title}》已处于【编制人已认领】状态。根据医疗器械质量体系规范,智能体小队不能擅自修改,已为您生成【变更方案提议】。请您确认是否准许本次修改。`, - timestamp: new Date().toTimeString().substring(0, 5), - }, - ]); + const changes = + '拟在第2节「监管分类与强制性合规标准矩阵」中,强化补充高频电刀共模抑制比设计指标。'; + const impact = '将直接影响电路原理图物料选型与型式检验 EMC 滤波网络参数。'; + const risk = '低风险。符合 GB 9706.1-2020 强标,提升系统抗扰度,无侵权风险。'; + const docId = activeDocIdRef.current; + void (async () => { + try { + setProposalError(null); + const prop = await proposeModification({ + doc_id: docId, + triggered_by: 'web_ui', + proposed_changes: changes, + impact_analysis: impact, + risk_assessment: risk, + }); + setPendingProposal({ + proposalId: prop.proposal_id, + changes: prop.proposed_changes || changes, + impact: prop.impact_analysis || impact, + risk: prop.risk_assessment || risk, + }); + setMessages((prev) => [ + ...prev, + { + id: 'ast-' + Date.now(), + sessionId: activeSessionId, + role: 'assistant', + authorName: '水木东方协同参谋', + intentTag: '🛡️ 文档认领保护机制拦截', + content: `检测到《${docAST.title}》已处于【编制人已认领】状态。根据医疗器械质量体系规范,智能体小队不能擅自修改,已为您生成【变更方案提议】(${prop.proposal_id})。请您确认是否准许本次修改。`, + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setProposalError(message); + setMessages((prev) => [ + ...prev, + { + id: 'ast-' + Date.now(), + sessionId: activeSessionId, + role: 'assistant', + authorName: '水木东方协同参谋', + intentTag: '⚠️ 变更提案失败', + content: `未能创建变更提案:${message}`, + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + } + })(); return; } } @@ -3244,32 +3280,106 @@ export default function App() { return { accepted: true }; }; - // 编制人接受变更提议 + // 编制人接受变更提议(经 HTTP decide) const handleAcceptProposal = () => { - setPendingProposal(null); - const children = docAST.rootBlock.children || []; - const pBlock = children.find((c) => c.type === 'paragraph'); - if (pBlock) { - pBlock.content += '\n\n【工程师确认增补】前端光路采集板已加装定制高频共模扼流圈,强标抗扰度提升至最高等级。'; - pBlock.metadata = { - lastModifiedBy: '智能体协同小队 (经张工确认签署)', - lastModifiedAt: new Date().toISOString().replace('T', ' ').substring(0, 16), - }; - setDocAST({ ...docAST }); - } + if (!pendingProposal?.proposalId) return; + const proposalId = pendingProposal.proposalId; + void (async () => { + try { + setProposalError(null); + await decideModificationProposal({ + proposal_id: proposalId, + approve: true, + reason: '编制人确认采纳变更提案', + }); + setPendingProposal(null); + const children = docAST.rootBlock.children || []; + const pBlock = children.find((c) => c.type === 'paragraph'); + if (pBlock) { + pBlock.content += + '\n\n【工程师确认增补】前端光路采集板已加装定制高频共模扼流圈,强标抗扰度提升至最高等级。'; + pBlock.metadata = { + lastModifiedBy: '智能体协同小队 (经张工确认签署)', + lastModifiedAt: new Date().toISOString().replace('T', ' ').substring(0, 16), + }; + setDocAST({ ...docAST }); + } - setMessages((prev) => [ - ...prev, - { - id: 'ast-p-' + Date.now(), - sessionId: activeSessionId, - role: 'assistant', - authorName: '水木东方协同参谋', - intentTag: '✅ 协同变更已生效', - content: `变更提议已成功执行并落盘至《${docAST.title}》,受控文档已同步更新。`, - timestamp: new Date().toTimeString().substring(0, 5), - }, - ]); + setMessages((prev) => [ + ...prev, + { + id: 'ast-p-' + Date.now(), + sessionId: activeSessionId, + role: 'assistant', + authorName: '水木东方协同参谋', + intentTag: '✅ 协同变更已生效', + content: `变更提议 ${proposalId} 已批准并落盘至《${docAST.title}》,受控文档已同步更新。`, + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setProposalError(message); + setMessages((prev) => [ + ...prev, + { + id: 'ast-p-err-' + Date.now(), + sessionId: activeSessionId, + role: 'assistant', + authorName: '水木东方协同参谋', + intentTag: '⚠️ 批准提案失败', + content: `批准变更提案失败:${message}`, + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + } + })(); + }; + + const handleRejectProposal = () => { + if (!pendingProposal?.proposalId) { + setPendingProposal(null); + return; + } + const proposalId = pendingProposal.proposalId; + void (async () => { + try { + setProposalError(null); + await decideModificationProposal({ + proposal_id: proposalId, + approve: false, + reason: '编制人拒绝本次变更', + }); + setPendingProposal(null); + setMessages((prev) => [ + ...prev, + { + id: 'ast-p-rej-' + Date.now(), + sessionId: activeSessionId, + role: 'assistant', + authorName: '水木东方协同参谋', + intentTag: '🚫 变更提案已拒绝', + content: `已拒绝变更提议 ${proposalId},受控文档保持原状。`, + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setProposalError(message); + setMessages((prev) => [ + ...prev, + { + id: 'ast-p-rej-err-' + Date.now(), + sessionId: activeSessionId, + role: 'assistant', + authorName: '水木东方协同参谋', + intentTag: '⚠️ 拒绝提案失败', + content: `拒绝变更提案失败:${message}`, + timestamp: new Date().toTimeString().substring(0, 5), + }, + ]); + } + })(); }; // 块操作:编排层先 PATCH,成功才提交 AST;失败保留编辑器并明确提示 @@ -4064,12 +4174,17 @@ export default function App() { {pendingProposal && (
+ {proposalError && ( +
+ {proposalError} +
+ )} setPendingProposal(null)} + onReject={handleRejectProposal} />
)} diff --git a/web/src/api/docgov.test.ts b/web/src/api/docgov.test.ts index e10911a0..bd6179b4 100644 --- a/web/src/api/docgov.test.ts +++ b/web/src/api/docgov.test.ts @@ -5,6 +5,8 @@ import { submitForReview, reviewDecision, approverDecision, + proposeModification, + decideModificationProposal, } from './docgov'; const sampleRecord = { @@ -108,4 +110,79 @@ describe('docgov lifecycle API client', () => { assert.equal(body.action, 'reject'); assert.equal(body.rollback_to, 'drafting'); }); + + it('proposeModification posts to /api/v1/docgov/proposal', async () => { + const mockFetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + lastCall = { url: String(input), init }; + return new Response( + JSON.stringify({ + proposal_id: 'prop_1', + doc_id: 'doc-1', + proposed_changes: '改一处', + status: 'pending_author_approval', + }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ); + }) as typeof fetch; + globalThis.fetch = mockFetch; + (globalThis as { window: { fetch: typeof fetch; localStorage: unknown } }).window = { + fetch: mockFetch, + localStorage: { + getItem: () => + JSON.stringify({ token: 'test-token', user: { id: 7, name: 'A' } }), + setItem: () => undefined, + removeItem: () => undefined, + }, + }; + + const prop = await proposeModification({ + doc_id: 'doc-1', + triggered_by: 'web_ui', + proposed_changes: '改一处', + impact_analysis: '影响小', + risk_assessment: '低风险', + }); + assert.equal(lastCall?.url, '/api/v1/docgov/proposal'); + assert.equal(lastCall?.init?.method, 'POST'); + const body = JSON.parse(String(lastCall?.init?.body)); + assert.equal(body.doc_id, 'doc-1'); + assert.equal(body.proposed_changes, '改一处'); + assert.equal(prop.proposal_id, 'prop_1'); + }); + + it('decideModificationProposal posts to /api/v1/docgov/proposal/decide', async () => { + const mockFetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + lastCall = { url: String(input), init }; + return new Response( + JSON.stringify({ + proposal_id: 'prop_1', + doc_id: 'doc-1', + proposed_changes: '改一处', + status: 'approved_by_author', + }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ); + }) as typeof fetch; + globalThis.fetch = mockFetch; + (globalThis as { window: { fetch: typeof fetch; localStorage: unknown } }).window = { + fetch: mockFetch, + localStorage: { + getItem: () => + JSON.stringify({ token: 'test-token', user: { id: 7, name: 'A' } }), + setItem: () => undefined, + removeItem: () => undefined, + }, + }; + + const prop = await decideModificationProposal({ + proposal_id: 'prop_1', + approve: true, + reason: '同意', + }); + assert.equal(lastCall?.url, '/api/v1/docgov/proposal/decide'); + const body = JSON.parse(String(lastCall?.init?.body)); + assert.equal(body.proposal_id, 'prop_1'); + assert.equal(body.approve, true); + assert.equal(prop.status, 'approved_by_author'); + }); }); diff --git a/web/src/api/docgov.ts b/web/src/api/docgov.ts index d023f921..0f216b8e 100644 --- a/web/src/api/docgov.ts +++ b/web/src/api/docgov.ts @@ -126,6 +126,47 @@ export async function approverDecision(payload: ApproverDecisionPayload): Promis return postDocGov('/api/v1/docgov/approver-decision', payload); } +export interface ProposeModificationPayload { + doc_id: string; + triggered_by?: string; + proposed_changes: string; + impact_analysis?: string; + risk_assessment?: string; +} + +export interface DecideModificationProposalPayload { + proposal_id: string; + approve: boolean; + reason?: string; +} + +export interface ChangeProposalRecord { + id?: number; + proposal_id: string; + doc_id: string; + project_id?: number; + triggered_by?: string; + proposed_changes: string; + impact_analysis?: string; + risk_assessment?: string; + status: string; + author_decision_reason?: string; + decided_at?: string | null; + created_at?: string; +} + +export async function proposeModification( + payload: ProposeModificationPayload, +): Promise { + return postDocGov('/api/v1/docgov/proposal', payload); +} + +export async function decideModificationProposal( + payload: DecideModificationProposalPayload, +): Promise { + return postDocGov('/api/v1/docgov/proposal/decide', payload); +} + export function mapLeaseToAstEditLease(doc: { lease_holder_type?: string; lease_holder_id?: string; From d01694b556b4a71375e8097c15f50713f3c55d41 Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 17 Sep 2026 14:07:52 +0000 Subject: [PATCH 2/3] docs(ops): record author-only pilot and proposal-http task claim Archive CAS evidence for workflow.allow_author_only_approval on dev, mark proposal HTTP audit resolved, and claim the feature branch in CURRENT. Co-authored-by: Cursor --- ...7-docgov-modification-proposal-http-gap.md | 30 ++++-------- .../2026-09-17-author-only-pilot-toggle.md | 22 +++++++++ .doc/ops/CURRENT.md | 9 ++-- ...026-09-17-document-author-only-closeout.md | 19 +++++--- ...09-17-docgov-modification-proposal-http.md | 48 +++++++++++++++++++ .../after-enable.json | 6 +++ .../before-enable.json | 5 ++ .../put-enable.txt | 3 ++ 8 files changed, 110 insertions(+), 32 deletions(-) create mode 100644 .doc/ops/2026-09-17-author-only-pilot-toggle.md create mode 100644 .doc/ops/short-term/2026-09-17-docgov-modification-proposal-http.md create mode 100644 .doc/test-reports/assets/2026-09-17-author-only-pilot/after-enable.json create mode 100644 .doc/test-reports/assets/2026-09-17-author-only-pilot/before-enable.json create mode 100644 .doc/test-reports/assets/2026-09-17-author-only-pilot/put-enable.txt diff --git a/.doc/audit/2026-09-17-docgov-modification-proposal-http-gap.md b/.doc/audit/2026-09-17-docgov-modification-proposal-http-gap.md index 72cd75bd..915d6dc6 100644 --- a/.doc/audit/2026-09-17-docgov-modification-proposal-http-gap.md +++ b/.doc/audit/2026-09-17-docgov-modification-proposal-http-gap.md @@ -1,6 +1,6 @@ --- title: "docgov 变更提案缺少 HTTP 路由" -status: open +status: resolved created: 2026-09-17 updated: 2026-09-17 severity: medium @@ -10,35 +10,23 @@ severity: medium ## 现象 -引擎侧存在 `docgov.ProposeModification` / `DecideModificationProposal`,但 `internal/server/server.go` 未注册对应 HTTP 路由。前端 `App.tsx`「变更方案提议」(`pendingProposal`)仍为本地模拟,不写库、不审计。 - -该缺口与 `duty_policy=author_only` 无关,影响全部受控文档的变更提案闭环。 +引擎侧存在 `docgov.ProposeModification` / `DecideModificationProposal`,但服务端此前仅注册 `POST /api/v1/docgov/proposal/decide`;前端 `App.tsx`「变更方案提议」(`pendingProposal`)为本地模拟。 ## 根因分析 -U6 排查审批闭环时确认:生命周期三接口(submit-review / review-decision / approver-decision)与 claim 已注册并可真实调用;变更提案路径从未暴露为 HTTP,前后端契约缺失。 - -证据: - -- `rg "ProposeModification|DecideModification|/docgov/.*proposal"`(路由层无匹配) -- `App.tsx` pendingProposal 本地状态分支 +生命周期三接口与 claim 已注册;变更提案 **propose** 路径从未暴露为 HTTP。 ## 严重度 `medium` — 不阻塞编制→审核→批准主链;阻塞「认领后小队修改须提案确认」的真实治理。 -## 关联整改 +## 处置(resolved) -- 建议后续单独立项(不并入 author-only remediation U6) -- 相关:`.doc/remediation/2026-09-17-document-author-only-plan.md` 发现 3 +- 新增 `POST /api/v1/docgov/proposal` → `ProposeModification` +- 前端 `proposeModification` / `decideModificationProposal` + App 接线 +- 单测:`TestServer_DocGovProposeAndDecideHTTP`、`web/src/api/docgov.test.ts` +- 任务:`.doc/ops/short-term/2026-09-17-docgov-modification-proposal-http.md` ## 证据 -- U6 计划与测试报告中的「非目标」声明 -- 本笔记创建于 U6 实施期间 - -## 建议下一步 - -1. 设计 `POST /api/v1/docgov/propose-modification` 与 decide 路由及 ABAC; -2. 前端替换本地 `pendingProposal` 模拟; -3. 补真实 PI + 浏览器验收报告。 +- 合入后真实 HTTP 证据见 `.doc/test-reports/assets/2026-09-17-docgov-modification-proposal-http/` diff --git a/.doc/ops/2026-09-17-author-only-pilot-toggle.md b/.doc/ops/2026-09-17-author-only-pilot-toggle.md new file mode 100644 index 00000000..138ddbc8 --- /dev/null +++ b/.doc/ops/2026-09-17-author-only-pilot-toggle.md @@ -0,0 +1,22 @@ +--- +title: "dev 试点:开放编制人兼任总开关" +status: done +created: 2026-09-17 +updated: 2026-09-17 +--- + +# dev 试点:开放编制人兼任总开关 + +## 结论 + +在运行中的 `agent-dev`(`:8080`,schema 52)经 admin CAS 将 `workflow.allow_author_only_approval` 设为 `true`(revision `0 → 1`)。**未**改默认迁移值;**未**晋升 test/main;test/main 仍保持默认 off。 + +## 证据(脱敏) + +- [before-enable.json](../../test-reports/assets/2026-09-17-author-only-pilot/before-enable.json) +- [put-enable.txt](../../test-reports/assets/2026-09-17-author-only-pilot/put-enable.txt) +- [after-enable.json](../../test-reports/assets/2026-09-17-author-only-pilot/after-enable.json) + +## 关联 + +- author-only 收口:[../handoffs/2026-09-17-document-author-only-closeout.md](../handoffs/2026-09-17-document-author-only-closeout.md) diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index d437e48d..bbd971c1 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -5,14 +5,15 @@ ## 焦点 -1. **document-author-only**:U1–U6 **已合入 `dev`**(总开关默认 off;test/main 晋升与开放授权另授) -2. **partial/deferred 全量收口**:W1–W4 **done**;W5a **done**;W6 **shadow done**;W7 **盘点 done**。test 已至 `rc-v1.0.0-beta.4`(含 optional-duty null FK) +1. **document-author-only**:U1–U6 **已合入并推送 `origin/dev`**;dev 试点总开关 **on**;test/main 仍 off、未晋升 +2. **docgov 变更提案 HTTP**:`feature/docgov-modification-proposal-http`(mutex `docgov`)in_progress 3. **红线**:禁止整支 merge FAV;不默认升 `MULTI_INTENT_PLAN_TIER=full`;`SEMANTIC_SUMMARY_REPLACE` 不随 W2 开启;FAV live 观察窗证据驱动;`FAV_DEGRADED_READ=false`;项目 143 只读 ## 并行认领 | owner | task | branch | worktree | mutex | status | |-------|------|--------|----------|-------|--------| +| Cursor | 2026-09-17-docgov-modification-proposal-http | feature/docgov-modification-proposal-http | .worktrees/feature-docgov-modification-proposal-http | docgov | in_progress | | Cursor | 2026-09-17-document-author-only-closeout | feature/document-author-only-plan | .worktrees/feature-document-author-only-plan | document-duty-policy | done | | Cursor | 2026-09-17-rdev-template-depth-s1-continued | feature/rdev-template-depth-s1-continued | - | rdev-templates | planned | | Cursor | 2026-09-17-rdev-workflow-integration | feature/rdev-core | - | workflow-def | done | @@ -129,7 +130,7 @@ | holder | phase | since | scope | queue | |--------|-------|-------|-------|-------| -| - | idle | - | - | - | +| Cursor | idle | 2026-09-17 | docgov-modification-proposal-http(待合入) | - | **phase**: `idle` | `merging` | `smoke` | `e2e` @@ -139,7 +140,7 @@ | 环境 | 端口 | schema | 状态 | |------|------|--------|------| -| dev | 8080 | **52** | healthy(含 author_only goose 00050–00052;`pi-agent`;events `:8088`;总开关默认 off) | +| dev | 8080 | **52** | healthy(含 author_only goose 00050–00052;`pi-agent`;events `:8088`;**试点** `workflow.allow_author_only_approval=true`) | | test | 8081 | **48** | healthy(`rc-v1.0.0-beta.4`;pi-agent `:18866`;events `:8089`;notify/advance/projection live、fav off;health+ready PASS;goose 00048 已迁移) | | main | 8000 | **47** | healthy(app + events + runner + DB;与 dev/test 对齐) | diff --git a/.doc/ops/handoffs/2026-09-17-document-author-only-closeout.md b/.doc/ops/handoffs/2026-09-17-document-author-only-closeout.md index daaa0b3a..9c1fefd8 100644 --- a/.doc/ops/handoffs/2026-09-17-document-author-only-closeout.md +++ b/.doc/ops/handoffs/2026-09-17-document-author-only-closeout.md @@ -13,23 +13,28 @@ to: integrator 实现与验收任务已全部收口:U1–U6 short-term 已迁入 `archive/ops/`;FEATURE-MATRIX / remediation DoD(除发布末条)已更新;U6 报告已归档。 +**远端**:已 `git-push-dev.sh` 推送 `origin/dev`(含 author-only merge);**未**晋升 test/main。 + +**dev 试点**:`workflow.allow_author_only_approval=true`(revision 1);证据见 [pilot assets](../../test-reports/assets/2026-09-17-author-only-pilot/) 与 [ops 笔记](../2026-09-17-author-only-pilot-toggle.md)。 + ## 分支 -- `feature/document-author-only-plan`(worktree:`.worktrees/feature-document-author-only-plan`) -- 相对 `origin/dev` 约 78 commits(含 U1–U6) +- `feature/document-author-only-plan`(worktree:`.worktrees/feature-document-author-only-plan`)— 已合入,待清理 +- 续作:`feature/docgov-modification-proposal-http`(变更提案 HTTP) ## 合入 `dev`(已完成) -- `merge --no-ff feature/document-author-only-plan` → `dev@09a9921` +- `merge --no-ff feature/document-author-only-plan` → 后续推送至 `origin/dev@ba92a8b` 一带 - 冒烟:`go vet` / `go test -short` 相关包 PASS;`./scripts/env-service.sh dev rebuild`;`GET /health` → `schema_version=52`、`status=ok` -- 总开关默认 off;**未** push / 未晋升 test/main / 未开放授权 +- 总开关默认迁移值仍为 off;**dev 运行态**已 CAS 试点开启;**未**晋升 test/main -## 明确不在本期 +## 明确不在本期(已转入续作) -- 变更提案 HTTP 路由([audit](../../audit/2026-09-17-docgov-modification-proposal-http-gap.md)) -- test/main 晋升与试点开放总授权 +- ~~变更提案 HTTP 路由~~ → [short-term](../short-term/2026-09-17-docgov-modification-proposal-http.md) / [audit resolved](../../audit/2026-09-17-docgov-modification-proposal-http-gap.md) +- test/main 晋升仍另授 ## 证据 - [U6 报告](../../test-reports/2026-09-17-document-author-only-u6-test-report.md) - [方案 DoD](../../remediation/2026-09-17-document-author-only-plan.md) +- [pilot](../../test-reports/assets/2026-09-17-author-only-pilot/) diff --git a/.doc/ops/short-term/2026-09-17-docgov-modification-proposal-http.md b/.doc/ops/short-term/2026-09-17-docgov-modification-proposal-http.md new file mode 100644 index 00000000..34d2cd9e --- /dev/null +++ b/.doc/ops/short-term/2026-09-17-docgov-modification-proposal-http.md @@ -0,0 +1,48 @@ +--- +title: "docgov 变更提案 HTTP + 前端接线" +status: in_progress +created: 2026-09-17 +updated: 2026-09-17 +owner: Cursor +branch: feature/docgov-modification-proposal-http +worktree: .worktrees/feature-docgov-modification-proposal-http +mutex: docgov +--- + +# docgov 变更提案 HTTP + 前端接线 + +## 目标 + +暴露 `POST /api/v1/docgov/proposal`,前端「修改」快捷分支与 `ProposalConfirmCard` 走真实 propose/decide,不再本地假装成功。 + +## 范围 + +- API:`ProposeModification` 路由;decide 补 404/403 映射 +- 前端:`docgov.ts` + `App.tsx` 接线 +- 单测 + 真实 HTTP 证据 +- 关闭 audit 缺口;dev 试点总开关证据归档 + +## 非目标 + +- 晋升 test/main +- 变更提案自动改写受控正文(批准后仍为 UI 演示增补) + +## 步骤 + +1. [x] 实现 propose handler + 路由 +2. [x] 前端 client + App 接线 +3. [x] handler/client 单测 +4. [ ] 合入 dev + rebuild + 真实 HTTP +5. [ ] push origin/dev;清理 author-only worktree + +## 验收标准 + +- [x] `POST /api/v1/docgov/proposal` 可写库(单测) +- [ ] 真实 HTTP claim → propose → decide PASS +- [ ] audit 缺口 resolved;CURRENT 认领收敛 + +## 相关链接 + +- Audit: [../../audit/2026-09-17-docgov-modification-proposal-http-gap.md](../../audit/2026-09-17-docgov-modification-proposal-http-gap.md) +- Pilot: [../../test-reports/assets/2026-09-17-author-only-pilot/](../../test-reports/assets/2026-09-17-author-only-pilot/) +- Closeout: [../handoffs/2026-09-17-document-author-only-closeout.md](../handoffs/2026-09-17-document-author-only-closeout.md) diff --git a/.doc/test-reports/assets/2026-09-17-author-only-pilot/after-enable.json b/.doc/test-reports/assets/2026-09-17-author-only-pilot/after-enable.json new file mode 100644 index 00000000..44b22b2c --- /dev/null +++ b/.doc/test-reports/assets/2026-09-17-author-only-pilot/after-enable.json @@ -0,0 +1,6 @@ +{ + "key": "workflow.allow_author_only_approval", + "value": true, + "revision": 1, + "note": "dev pilot enabled; test/main not promoted" +} \ No newline at end of file diff --git a/.doc/test-reports/assets/2026-09-17-author-only-pilot/before-enable.json b/.doc/test-reports/assets/2026-09-17-author-only-pilot/before-enable.json new file mode 100644 index 00000000..0ad538f1 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-17-author-only-pilot/before-enable.json @@ -0,0 +1,5 @@ +{ + "key": "workflow.allow_author_only_approval", + "value": false, + "revision": 0 +} \ No newline at end of file diff --git a/.doc/test-reports/assets/2026-09-17-author-only-pilot/put-enable.txt b/.doc/test-reports/assets/2026-09-17-author-only-pilot/put-enable.txt new file mode 100644 index 00000000..105af092 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-17-author-only-pilot/put-enable.txt @@ -0,0 +1,3 @@ +{"key":"workflow.allow_author_only_approval","label":"允许编制人兼任审核批准","group":"workflow","type":"bool","value":true,"default":false,"writable":true,"requiresRestart":false,"description":"授权新配置:开启后项目创建者可在激活前为文档启用编制人兼任。关闭后禁止新启用/激活兼任规划;已激活实例按冻结职责继续执行,不作为紧急熔断。","revision":1,"updatedAt":"2026-09-17T14:03:22.990984Z","updatedBy":9} + +HTTP 200 From c89a58e0267edcd5755951968421dae83e4d441a Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 17 Sep 2026 14:08:02 +0000 Subject: [PATCH 3/3] docs(ops): occupy integration slot for proposal-http merge 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 bbd971c1..53a4cf5f 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -130,7 +130,7 @@ | holder | phase | since | scope | queue | |--------|-------|-------|-------|-------| -| Cursor | idle | 2026-09-17 | docgov-modification-proposal-http(待合入) | - | +| Cursor | merging | 2026-09-17 | feature/docgov-modification-proposal-http | - | **phase**: `idle` | `merging` | `smoke` | `e2e`