Integrate the shared document renderer and two-trees project work #209

Merged
weichen merged 49 commits from feature/unified-document-renderer-task10 into main 2026-10-18 12:41:32 +00:00
15 changed files with 116 additions and 25 deletions
Showing only changes of commit 1afe164798 - Show all commits
+2 -2
View File
@@ -125,13 +125,13 @@ func compareLegacyAndShared(title string, source []byte, canonical model.Canonic
receipt.Dimensions = append(receipt.Dimensions, compareExactDimension("page-margins", legacyDOCX.Margins, sharedDOCX.Margins, ""))
} else {
receipt.Dimensions = append(receipt.Dimensions, intentionalDimension("page-margins", legacyDOCX.Margins, sharedDOCX.Margins,
comparisonExceptionCorporate, "organization-default candidate owns page margins"))
comparisonExceptionCorporate, "published organization default owns page margins"))
}
if legacyDOCX.Headers == sharedDOCX.Headers && legacyDOCX.Footers == sharedDOCX.Footers {
receipt.Dimensions = append(receipt.Dimensions, compareExactDimension("header-footer", []int{legacyDOCX.Headers, legacyDOCX.Footers}, []int{sharedDOCX.Headers, sharedDOCX.Footers}, ""))
} else {
receipt.Dimensions = append(receipt.Dimensions, intentionalDimension("header-footer", []int{legacyDOCX.Headers, legacyDOCX.Footers}, []int{sharedDOCX.Headers, sharedDOCX.Footers},
comparisonExceptionCorporate, "organization-default candidate adds reviewed header/footer rules"))
comparisonExceptionCorporate, "published organization default adds approved header/footer rules"))
}
if legacyDOCX.Bookmarks == sharedDOCX.Bookmarks {
receipt.Dimensions = append(receipt.Dimensions, compareExactDimension("node-bookmarks", legacyDOCX.Bookmarks, sharedDOCX.Bookmarks, ""))
+1 -1
View File
@@ -340,7 +340,7 @@ func generateWithOptions(ctx context.Context, request Request, options RenderOpt
Document: parsed.Document, Renderer: result.Manifest,
Profile: ProfileReceipt{
ProfileID: profileID, Version: profileVersion, SnapshotHash: bundle.Snapshot.SnapshotHash(),
Candidate: true, ReviewStatus: bundle.Review.Status, PublicationAllowed: bundle.Review.PublicationAllowed,
Candidate: bundle.Snapshot.State() != profile.StatePublished, ReviewStatus: bundle.Review.Status, PublicationAllowed: bundle.Review.PublicationAllowed,
Unverified: append([]string(nil), bundle.Review.Unverified...), ProfileSHA256: bundle.Review.ProfileSHA256,
AssetManifestSHA256: bundle.Review.AssetManifestSHA256, ReviewSHA256: bundle.Review.ReviewSHA256,
},
@@ -2,6 +2,6 @@
package docgen
const rendererRevision = "0d1c5121d137be817db5f98d3c13320dfa919c8a"
const rendererRevision = "31fbe54fae8bf11be664919ce173da0d68ebfdc9"
const RendererIdentity = "agent-document-renderer@" + rendererRevision
+17 -5
View File
@@ -39,7 +39,7 @@ const representativeMarkdown = `# 研发周会纪要
![公司默认样图](asset:company.default.sample-image@1.0.0)
:::callout warning
公司默认规则仍待人工视觉评审。
公司默认规则只用于普通工作区交付。
:::
:::page-break
@@ -89,10 +89,17 @@ func TestGenerateProducesDeterministicHashedDeliveryPackage(t *testing.T) {
if err := json.Unmarshal(first.Manifest.Bytes, &manifest); err != nil {
t.Fatalf("decode manifest: %v", err)
}
bundle, err := profiles.LoadCompanyDefault()
if err != nil {
t.Fatalf("load renderer company default: %v", err)
}
if manifest.SchemaVersion != ManifestSchemaVersion || manifest.RendererIdentity != RendererIdentity ||
manifest.SelectionSource != "organization-default" || !manifest.Profile.Candidate ||
manifest.Profile.ReviewStatus != "pending-human-visual-review" || manifest.Profile.PublicationAllowed {
t.Fatalf("manifest profile provenance = %#v", manifest)
manifest.SelectionSource != "organization-default" || manifest.Profile.Candidate ||
manifest.Profile.ReviewStatus != bundle.Review.Status || manifest.Profile.PublicationAllowed != bundle.Review.PublicationAllowed {
t.Fatalf("manifest profile provenance = %#v, renderer review = %#v", manifest, bundle.Review)
}
if manifest.Profile.ReviewStatus != "approved" || !manifest.Profile.PublicationAllowed {
t.Fatalf("published organization default reported review=%q publicationAllowed=%v", manifest.Profile.ReviewStatus, manifest.Profile.PublicationAllowed)
}
if manifest.Profile.ProfileSHA256 == "" || manifest.Profile.AssetManifestSHA256 == "" || manifest.Profile.ReviewSHA256 == "" {
t.Fatalf("manifest omitted source hashes: %#v", manifest.Profile)
@@ -360,7 +367,7 @@ func TestPersistedControlledMetadataRoutesBeforeDefaultProfileLoad(t *testing.T)
}
func TestRendererIdentityMatchesPublishedPin(t *testing.T) {
const want = "agent-document-renderer@0d1c5121d137be817db5f98d3c13320dfa919c8a"
const want = "agent-document-renderer@31fbe54fae8bf11be664919ce173da0d68ebfdc9"
if RendererIdentity != want {
t.Fatalf("RendererIdentity = %q, want %q", RendererIdentity, want)
}
@@ -636,6 +643,11 @@ func TestRepresentativeLegacyComparisonRecordsOnlyIntentionalDifferences(t *test
t.Errorf("comparison dimension %s = %#v, want match", name, dimension)
}
}
for _, dimension := range delivery.Comparison.Dimensions {
if dimension.ExceptionID == comparisonExceptionCorporate && !strings.Contains(dimension.Detail, "published organization default") {
t.Errorf("corporate layout exception %s detail = %q, want published organization default", dimension.Name, dimension.Detail)
}
}
}
func TestComparisonDetectsCanonicalAndDOCXTableAlignmentLoss(t *testing.T) {
@@ -1,7 +1,7 @@
---
id: base.deliverables
layer: base
version: 11
version: 12
title: 交付物工作流
description: 生成文档、表格、演示稿的工作流、样式决策与技能入口(常驻基座)
---
@@ -9,10 +9,10 @@ description: 生成文档、表格、演示稿的工作流、样式决策与技
工作流:
1. **先判路由**:普通、非受控的工作区 Word 可用组织默认候选;项目文档、受控文档、专用模板或审签输出必须交给 RDM,禁止调用 Backend 默认规则回退。组织默认候选的 manifest 状态固定显示 `pending-human-visual-review`,不能描述成已发布或已视觉批准。Excel / PPT / 图片再按用户指令、模板、参考文档、会话样式和默认主题的顺序定样式。
1. **先判路由**:组织默认规则已发布(负责人 2026-10-16 批准,manifest 评审状态 `approved`),但只用于普通、非受控的工作区 Word;项目文档、受控文档、专用模板或审签输出必须交给 RDM,禁止调用 Backend 默认规则回退。Excel / PPT / 图片再按用户指令、模板、参考文档、会话样式和默认主题的顺序定样式。
2. **规划版面**:先定读者、结构和每一节的内容,再写脚本。不要边生成边补空节。
3. **生成 Word**:调用 `render_document`,传本次交付稳定且重试复用的 `operationId`、稳定 `documentId`、标题 `name`、Markdown、语言和 `working|preview` 模式,并显式传 `controlled=false`、`projectDocument=false`;任何一项不是 false 就停止并交给 RDM。时间与 requester 由宿主冻结,禁止传入或声称自定 `frozenAt`。Markdown 用 `#`-`######` 标题、段落、引用、围栏/行内代码、粗体、斜体、删除线、安全链接标签、`-`/`1.` 列表、GFM 表格、`asset:` 图片、`:::callout warning`/`:::` 提示块和 `:::page-break`。工具先保存带稳定 node ID 的 Markdown,再生成确定性 DOCX、manifest 和完整 zip;返回值逐项列出稳定 ID 与 SHA-256。失败时按回执状态重试同一 operationId,绝不调用 legacy Word 路径回退。
4. **生成其他格式并自检**:Excel/PPT/图片用 `run_python` + house-style 对应 kit;用 lint/outline 回读。Word 的 OPC、安全、锚点与回读校验已经由统一 renderer 执行,复核 manifest 中的诊断、组织默认候选状态和 Markdown/DOCX 哈希。
4. **生成其他格式并自检**:Excel/PPT/图片用 `run_python` + house-style 对应 kit;用 lint/outline 回读。Word 的 OPC、安全、锚点与回读校验已经由统一 renderer 执行,复核 manifest 中的诊断、组织默认规则评审状态和 Markdown/DOCX 哈希。
5. **登记**:`render_document` 已登记 Markdown、DOCX、manifest 和 zip,不要重复登记。其他格式继续用 `register_artifact`。
6. **收尾**:回复只写结论摘要,并说明这次用的样式(主题、是否模板、是否仿制)以及用户还能怎么调。不要在回复里复述全文,也不要只口头说文件名。
+2 -2
View File
@@ -1,7 +1,7 @@
---
id: task.doc-minutes
layer: task
version: 3
version: 4
title: 会议纪要生成
description: 依据上传的会议记录/录音稿生成结构完整的会议纪要
priority: 70
@@ -28,4 +28,4 @@ triggers:
- 待办事项若原文未指明负责人或期限,写"未指明",不要编造。
- 版面:标题下用键值表写会议信息(时间、地点、主持人、参会人、缺席)。讨论按议题分节,每节含要点、分歧和结论。决议用真编号列表,不要再手写“1.”。待办用表格,列:序号、事项、负责人、期限、状态;原文没有的写“未指明”。发言人用统一称谓(职务或姓名);原文只有“说话人X”时保留并在文末说明。引语只留关键句,并标注是谁说的。
- 日期和数字全文一种格式。不要空节、空段落和手打项目符号。
- 交付形态:普通工作区纪要按「交付物工作流」调用 `render_document`,生成「<主题>会议纪要.md/.docx/.manifest.json/.zip」;项目或受控纪要交给 RDM。对话里只简述结论、规则候选状态和可调项,不要复述全文。
- 交付形态:普通工作区纪要按「交付物工作流」调用 `render_document`,生成「<主题>会议纪要.md/.docx/.manifest.json/.zip」;项目或受控纪要交给 RDM。对话里只简述结论、所用组织默认规则和可调项,不要复述全文。
+2 -2
View File
@@ -1,7 +1,7 @@
---
id: task.report
layer: task
version: 3
version: 4
title: 报告撰写
description: 把材料组织成正式报告/公文(含执行摘要与附录建议)
priority: 55
@@ -25,4 +25,4 @@ triggers:
- 结构服务于阅读动线:先结论后论证;每章开头一句话说清本章要点。
- 篇幅完整不注水:覆盖必要内容,删掉不改变结论的空话。
- 版面:封面(标题、受众、日期)、正文从执行摘要开始。执行摘要用强调块,控制在契约规定的篇幅内。章标题用标题样式,章首一句点明本章要点。数据进表格或附录,不埋在段落里。需要时在正文前放由标题生成的目录说明(页码以文件为准)。
- 交付普通工作区 Word 时走交付物工作流的 `render_document`,同时保留 Markdown、manifest 和归档;项目或受控报告交给 RDM。回复说明规则候选状态,不复述全文。
- 交付普通工作区 Word 时走交付物工作流的 `render_document`,同时保留 Markdown、manifest 和归档;项目或受控报告交给 RDM。回复说明所用组织默认规则,不复述全文。
@@ -1,10 +1,10 @@
---
id: tool.render_document
layer: tool
version: 1
version: 2
title: render_document 工具描述
description: 普通工作区 Markdown 到统一 Word 交付包的安全渲染入口
---
把普通、非受控的工作区 Markdown 交给共享 canonical parser、组织默认候选 profile 和 Word adapter。必须传稳定且重试复用的 operationId,并显式传 `controlled=false` 和 `projectDocument=false`;缺任一字段即拒绝。frozenAt 与 requester 由宿主首次操作时创建并持久化,调用方不能提交。成功回执包含 Markdown、DOCX、manifest、zip 的稳定 ID 与 SHA-256;失败回执明确 partial 或 unknown,重试必须沿用同一 operationId。
把普通、非受控的工作区 Markdown 交给共享 canonical parser、已发布的组织默认 profile 和 Word adapter。必须传稳定且重试复用的 operationId,并显式传 `controlled=false` 和 `projectDocument=false`;缺任一字段即拒绝。frozenAt 与 requester 由宿主首次操作时创建并持久化,调用方不能提交。成功回执包含 Markdown、DOCX、manifest、zip 的稳定 ID 与 SHA-256;失败回执明确 partial 或 unknown,重试必须沿用同一 operationId。
只允许 working 或 preview。项目文档、受控文档、专用模板、审阅稿或批准稿必须交给 RDM;失败不得改走 legacy Word builder。当前公司默认规则状态是 pending-human-visual-review,只可用于普通工作/预览交付,不能称为已发布或已视觉批准。
只允许 working 或 preview。项目文档、受控文档、专用模板、审阅稿或批准稿必须交给 RDM;失败不得改走 legacy Word builder。公司默认规则已发布(负责人 2026-10-16 批准,manifest 评审状态 `approved`),但仍只用于普通、非受控的工作/预览交付,不能代替 RDM 的项目或受控规则。
+38 -1
View File
@@ -101,16 +101,53 @@ func TestBaseDeliverablesRoutesWordThroughUnifiedRenderer(t *testing.T) {
if err != nil || !found {
t.Fatalf("base.deliverables: found=%v err=%v", found, err)
}
for _, required := range []string{"render_document", "Markdown", "manifest", "SHA-256", "pending-human-visual-review", "RDM"} {
for _, required := range []string{"render_document", "Markdown", "manifest", "SHA-256", "已发布", "2026-10-16", "approved", "RDM", "禁止调用 Backend 默认规则回退"} {
if !strings.Contains(body, required) {
t.Errorf("base.deliverables missing %q", required)
}
}
for _, stale := range []string{"pending-human-visual-review", "组织默认候选", "不能描述成已发布"} {
if strings.Contains(body, stale) {
t.Errorf("base.deliverables still describes the organization default as unpublished: %q", stale)
}
}
if strings.Contains(body, "python3 -m housestyle.build") {
t.Fatal("base.deliverables still routes production Word through legacy housestyle.build")
}
}
func TestRenderDocumentToolDescribesPublishedDefaultWithoutWideningRoute(t *testing.T) {
body, found, err := mustService(t).ModuleBody("", "tool.render_document")
if err != nil || !found {
t.Fatalf("tool.render_document: found=%v err=%v", found, err)
}
for _, required := range []string{"已发布", "2026-10-16", "approved", "working 或 preview", "RDM", "不得改走 legacy Word builder"} {
if !strings.Contains(body, required) {
t.Errorf("tool.render_document missing %q", required)
}
}
for _, stale := range []string{"pending-human-visual-review", "默认候选", "不能称为已发布"} {
if strings.Contains(body, stale) {
t.Errorf("tool.render_document still describes the organization default as unpublished: %q", stale)
}
}
}
func TestWordTaskPromptsDoNotReportOrganizationDefaultAsCandidate(t *testing.T) {
for _, id := range []string{"task.report", "task.doc-minutes"} {
body, found, err := mustService(t).ModuleBody("", id)
if err != nil || !found {
t.Fatalf("%s: found=%v err=%v", id, found, err)
}
if strings.Contains(body, "规则候选状态") {
t.Errorf("%s still asks the reply to report a candidate rule status", id)
}
if !strings.Contains(body, "RDM") {
t.Errorf("%s no longer routes project or controlled Word to RDM", id)
}
}
}
func TestEnvDeliverablePlaceholders(t *testing.T) {
svc := mustService(t)
blocks, _, err := svc.ResolveSystem(core.AgentProfile{}, TriggerCtx{Text: "你好"}, EnvData{
+1 -1
View File
@@ -7,7 +7,7 @@ description: 用统一 renderer 生成 Markdown、DOCX、manifest 和归档
先读 `$AGENT_SKILLS_DIR/house-style/SKILL.md`。普通、非受控工作区 Word 调用 `render_document`;传本次交付稳定且重试复用的 operationId、稳定 documentId、标题和 Markdown,时间由宿主持久冻结。项目或受控 Word、专用模板、审阅与批准输出必须交给 RDM,禁止默认回退。
成功结果包括 Markdown、DOCX、manifest 和 zip,逐项核对 SHA-256;当前组织默认候选仍是 `pending-human-visual-review`。Python、shell、`docx_kit` 和 `register_artifact` 都不是 Word 交付入口。迁移对照只能由 Backend 内部受信能力启动。生产 Word 的 OPC、安全、锚点与回读结果以共享 renderer manifest 为准;修订以返回的 Markdown 为权威源。
成功结果包括 Markdown、DOCX、manifest 和 zip,逐项核对 SHA-256;组织默认规则已发布(负责人 2026-10-16 批准,manifest 评审状态 `approved`),但只用于普通、非受控的 working/preview 交付。Python、shell、`docx_kit` 和 `register_artifact` 都不是 Word 交付入口。迁移对照只能由 Backend 内部受信能力启动。生产 Word 的 OPC、安全、锚点与回读结果以共享 renderer manifest 为准;修订以返回的 Markdown 为权威源。
版面范式:
+1 -1
View File
@@ -5,7 +5,7 @@ description: 交付物设计系统。普通 Word 使用统一 renderer;housest
# house-style
普通、非受控工作区 Word 的生产入口是 `render_document`:它保存 Markdown、确定性 DOCX、manifest 和 zip,并在 manifest 标注组织默认候选 `pending-human-visual-review`。项目或受控 Word、专用模板、审阅和批准输出必须交给 RDM,禁止 Backend 默认回退。
普通、非受控工作区 Word 的生产入口是 `render_document`:它保存 Markdown、确定性 DOCX、manifest 和 zip,并在 manifest 记录已发布组织默认规则的评审状态 `approved`(负责人 2026-10-16 批准);该规则只用于普通、非受控的 working/preview 交付。项目或受控 Word、专用模板、审阅和批准输出必须交给 RDM,禁止 Backend 默认回退。
housestyle 的 Excel、PowerPoint、图片构件继续用于生产。Python Word 构件不是生产或迁移入口;`housestyle.build` 永久禁用,DOC/DOCX 也不能通过 `register_artifact` 登记。
+24 -1
View File
@@ -183,11 +183,16 @@ func TestHouseStyleRoutesProductionWordThroughUnifiedRenderer(t *testing.T) {
t.Fatal(err)
}
body := string(raw)
for _, required := range []string{"render_document", "pending-human-visual-review", "RDM"} {
for _, required := range []string{"render_document", "已发布", "2026-10-16", "approved", "RDM", "禁止 Backend 默认回退"} {
if !strings.Contains(body, required) {
t.Errorf("house-style skill missing %q", required)
}
}
for _, stale := range []string{"pending-human-visual-review", "默认候选"} {
if strings.Contains(body, stale) {
t.Errorf("house-style still describes the organization default as unpublished: %q", stale)
}
}
for _, forbidden := range []string{"AGENT_SYSTEM_LEGACY_WORD_COMPARISON", "docx_kit", "new_document(template=", "dx.save("} {
if strings.Contains(body, forbidden) {
t.Fatalf("house-style still advertises Word bypass %q", forbidden)
@@ -195,6 +200,24 @@ func TestHouseStyleRoutesProductionWordThroughUnifiedRenderer(t *testing.T) {
}
}
func TestDocxAuthorDescribesPublishedDefaultOnlyForWorkspaceWord(t *testing.T) {
raw, err := recipes.ReadFile("recipes/docx-author/SKILL.md")
if err != nil {
t.Fatal(err)
}
body := string(raw)
for _, required := range []string{"render_document", "已发布", "2026-10-16", "approved", "RDM", "禁止默认回退"} {
if !strings.Contains(body, required) {
t.Errorf("docx-author skill missing %q", required)
}
}
for _, stale := range []string{"pending-human-visual-review", "默认候选"} {
if strings.Contains(body, stale) {
t.Errorf("docx-author still describes the organization default as unpublished: %q", stale)
}
}
}
func TestHouseStylePython(t *testing.T) {
if _, err := exec.LookPath("python3"); err != nil {
t.Skip("python3 not available")
+1 -1
View File
@@ -17,7 +17,7 @@ import (
func registerDocumentRenderer(r *Registry) error {
return r.Register(core.ToolDesc{
Name: "render_document", Source: core.SourceBuiltin, Edits: true, Risk: "medium",
Description: "把普通工作区 Markdown 通过统一文档渲染器生成 Markdown、DOCX、manifest 和可下载归档。只允许非受控工作区交付;项目受控文档必须交给 RDM。公司默认规则仍是待视觉评审候选,状态会写入 manifest。",
Description: "把普通工作区 Markdown 通过统一文档渲染器生成 Markdown、DOCX、manifest 和可下载归档。只允许非受控工作区交付;项目受控文档必须交给 RDM。公司默认规则已发布(负责人 2026-10-16 批准),但只用于普通工作区的 working/preview 交付,评审状态会写入 manifest。",
Parameters: json.RawMessage(`{"type":"object","additionalProperties":false,"properties":{` +
`"operationId":{"type":"string"},"name":{"type":"string"},"documentId":{"type":"string"},"documentType":{"type":"string"},` +
`"language":{"type":"string"},"markdown":{"type":"string"},` +
+19
View File
@@ -90,6 +90,25 @@ func TestRenderDocumentToolWritesMarkdownFirstAndReportsPackageHashes(t *testing
}
}
func TestRenderDocumentToolDescriptionReportsPublishedWorkspaceDefault(t *testing.T) {
registry := NewRegistry()
if err := registry.RegisterBuiltin(); err != nil {
t.Fatal(err)
}
description, _, found := registry.Get("render_document")
if !found {
t.Fatal("render_document is not registered")
}
for _, required := range []string{"已发布", "RDM", "非受控工作区"} {
if !strings.Contains(description.Description, required) {
t.Errorf("render_document description missing %q: %s", required, description.Description)
}
}
if strings.Contains(description.Description, "候选") {
t.Errorf("render_document description still calls the organization default a candidate: %s", description.Description)
}
}
func TestRenderDocumentToolSchemaIsClosedAndUsesOnlyOperationIdentity(t *testing.T) {
registry := NewRegistry()
if err := registry.RegisterBuiltin(); err != nil {