From c34279ad5510ac9b73e587438c8b4b36cf73b977 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 18 Oct 2026 06:24:36 +0000 Subject: [PATCH] Bump the renderer to 3f25999 and bind documents to published profile versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: Agent-Document-Renderer main publishes company.default@1.1.0 with a declarative TOC (spec 04, acceptance 10 §4 §7) and keeps 1.0.0 loadable by version. The fixed defaultProfileVersion "1.0.0" no longer matched LoadCompanyDefaultContext (1.1.0), so every delivery failed with a profile mismatch. New documents must take the latest published version, while documents that already bind a published version keep rendering with exactly that version, because published rules are immutable and old versions remain retrievable (spec 08). Evidence: go vet ./... ok; go test -count=1 ./... ok (all packages); go run ./internal/docgen/internal/genrendereridentity -check ok; scripts/agent-exec validate --repo . ok; scripts/dm governance check --repo . ok; gofmt -l clean; git diff --cached --check clean. New tests cover the default binding (1.1.0, TOC present), pinned 1.0.0 (no TOC) and 1.1.0 for both a request pin and a persisted-Markdown replay, and refusal of unknown versions and other profile IDs before any profile load. Impact: docgen resolves the profile version from the persisted Markdown binding or the request, defaults new documents to profiles.LatestCompanyDefaultVersion, and loads it through LoadCompanyDefaultVersionContext. The legacy migration comparison records the declared TOC as an intentional corporate-layout difference (RDR-T10-CMP-002) and keeps it out of the body visible-text dimension. RendererIdentity is regenerated for 3f259990b5d4. Refs: unified renderer plan Task 13; renderer PR #7 Co-authored-by: Cursor --- deps/agent-document-renderer | 2 +- internal/docgen/comparison.go | 54 ++++- internal/docgen/renderer.go | 70 ++++-- .../docgen/renderer_identity_generated.go | 2 +- internal/docgen/renderer_integration_test.go | 200 +++++++++++++++++- 5 files changed, 300 insertions(+), 28 deletions(-) diff --git a/deps/agent-document-renderer b/deps/agent-document-renderer index 1095077..3f25999 160000 --- a/deps/agent-document-renderer +++ b/deps/agent-document-renderer @@ -1 +1 @@ -Subproject commit 10950774736b64b5d8e544ce03d45ae5a6ceca7c +Subproject commit 3f259990b5d43ec0d318f39d496816140ab559b5 diff --git a/internal/docgen/comparison.go b/internal/docgen/comparison.go index 3996cdb..512b0b6 100644 --- a/internal/docgen/comparison.go +++ b/internal/docgen/comparison.go @@ -62,6 +62,7 @@ type docxNumbering struct { type docxComparison struct { Paragraphs []string `json:"paragraphs"` + TOC []string `json:"toc"` PageSize map[string]string `json:"pageSize"` Margins map[string]string `json:"margins"` Headers int `json:"headers"` @@ -95,7 +96,7 @@ func compareLegacyAndShared(title string, source []byte, canonical model.Canonic if legacyErr != nil || sharedErr != nil { detail := fmt.Sprintf("legacy=%v shared=%v", legacyErr, sharedErr) for _, name := range []string{ - "visible-text", "page-layout", "header-footer", "node-bookmarks", "safe-links", "security-objects", "word-semantics", + "visible-text", "table-of-contents", "page-layout", "header-footer", "node-bookmarks", "safe-links", "security-objects", "word-semantics", "word-numbering", "word-task-state", "word-thematic-rule", "word-table-alignment", } { receipt.Dimensions = append(receipt.Dimensions, ComparisonDimension{Name: name, Status: "unassessed", Detail: detail}) @@ -115,6 +116,12 @@ func compareLegacyAndShared(title string, source []byte, canonical model.Canonic } else { receipt.Dimensions = append(receipt.Dimensions, compareExactDimension("visible-text", legacyParagraphs, sharedDOCX.Paragraphs, "")) } + if len(legacyDOCX.TOC) == 0 && len(sharedDOCX.TOC) > 0 { + receipt.Dimensions = append(receipt.Dimensions, intentionalDimension("table-of-contents", legacyDOCX.TOC, sharedDOCX.TOC, + comparisonExceptionCorporate, "published organization default declares a table of contents")) + } else { + receipt.Dimensions = append(receipt.Dimensions, compareExactDimension("table-of-contents", legacyDOCX.TOC, sharedDOCX.TOC, "")) + } if equalJSON(legacyDOCX.PageSize, sharedDOCX.PageSize) { receipt.Dimensions = append(receipt.Dimensions, compareExactDimension("page-size", legacyDOCX.PageSize, sharedDOCX.PageSize, "")) @@ -501,6 +508,11 @@ func inspectComparisonDOCX(raw []byte) (docxComparison, error) { inParagraph, inText, inCell, inParagraphBorder := false, false, false, false paragraphLevel, paragraphNumID, cellAlignment := 0, "", "" paragraphRule := false + // A TOC is its field's paragraphs plus an immediately preceding caption + // paragraph excluded from the outline (outlineLvl 9). + inInstruction, paragraphTOC, paragraphCaption := false, false, false + fieldDepth, tocFieldDepth := 0, 0 + var pendingCaption []string var table [][]string var row []string for { @@ -518,10 +530,27 @@ func inspectComparisonDOCX(raw []byte) (docxComparison, error) { inParagraph = true paragraph.Reset() paragraphLevel, paragraphNumID, paragraphRule = 0, "", false + paragraphTOC, paragraphCaption = tocFieldDepth > 0, false case "t": if inParagraph { inText = true } + case "outlineLvl": + if inParagraph && xmlAttributeValue(value.Attr, "val") == "9" { + paragraphCaption = true + } + case "fldChar": + switch xmlAttributeValue(value.Attr, "fldCharType") { + case "begin": + fieldDepth++ + case "end": + if fieldDepth == tocFieldDepth { + tocFieldDepth = 0 + } + fieldDepth = max(fieldDepth-1, 0) + } + case "instrText": + inInstruction = true case "numId": if inParagraph { paragraphNumID = xmlAttributeValue(value.Attr, "val") @@ -559,15 +588,35 @@ func inspectComparisonDOCX(raw []byte) (docxComparison, error) { if inText { paragraph.Write(value) } + if inInstruction && tocFieldDepth == 0 && fieldDepth > 0 { + if fields := strings.Fields(string(value)); len(fields) > 0 && strings.EqualFold(fields[0], "TOC") { + tocFieldDepth, paragraphTOC = fieldDepth, inParagraph + } + } case xml.EndElement: switch value.Name.Local { case "t": inText = false + case "instrText": + inInstruction = false case "pBdr": inParagraphBorder = false case "p": text := normalizeComparisonText(paragraph.String()) - if text != "" { + if paragraphTOC { + result.TOC = append(result.TOC, pendingCaption...) + pendingCaption = nil + if text != "" { + result.TOC = append(result.TOC, text) + } + inParagraph = false + break + } + result.Paragraphs = append(result.Paragraphs, pendingCaption...) + pendingCaption = nil + if text != "" && paragraphCaption { + pendingCaption = []string{text} + } else if text != "" { result.Paragraphs = append(result.Paragraphs, text) } if paragraphNumID != "" { @@ -596,6 +645,7 @@ func inspectComparisonDOCX(raw []byte) (docxComparison, error) { } } } + result.Paragraphs = append(result.Paragraphs, pendingCaption...) if len(numbered) > 0 { numbering, found := parts["word/numbering.xml"] if !found { diff --git a/internal/docgen/renderer.go b/internal/docgen/renderer.go index 5ba8fb4..f60a435 100644 --- a/internal/docgen/renderer.go +++ b/internal/docgen/renderer.go @@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "sort" "strings" "time" @@ -26,7 +27,6 @@ const ( ManifestSchemaVersion = "backend-document-delivery/v1" AdapterIdentity = RendererIdentity + "/word-adapter-v1" defaultProfileID = "company.default" - defaultProfileVersion = "1.0.0" defaultRenderCapacity = 4 ) @@ -194,18 +194,15 @@ func Prepare(ctx context.Context, request Request) (PreparedDocument, error) { if err := validateWorkspaceRoute(request, parsed.Metadata); err != nil { return PreparedDocument{}, err } - if parsed.Metadata.ProfileID != defaultProfileID || parsed.Metadata.ProfileVersion != defaultProfileVersion { - return PreparedDocument{}, fmt.Errorf("%w: Markdown binds %s@%s, expected %s@%s", ErrProfileMismatch, - parsed.Metadata.ProfileID, parsed.Metadata.ProfileVersion, defaultProfileID, defaultProfileVersion) + if _, err := profileVersionFor(request, &parsed.Metadata); err != nil { + return PreparedDocument{}, err } } else { - if request.ProfileID != "" || request.ProfileVersion != "" { - if request.ProfileID != defaultProfileID || request.ProfileVersion != defaultProfileVersion { - return PreparedDocument{}, fmt.Errorf("%w: requested %s@%s, expected %s@%s", ErrProfileMismatch, - request.ProfileID, request.ProfileVersion, defaultProfileID, defaultProfileVersion) - } + profileVersion, err := profileVersionFor(request, nil) + if err != nil { + return PreparedDocument{}, err } - source, err := workspaceMarkdown(request, defaultProfileID, defaultProfileVersion) + source, err := workspaceMarkdown(request, defaultProfileID, profileVersion) if err != nil { return PreparedDocument{}, err } @@ -231,14 +228,14 @@ func Prepare(ctx context.Context, request Request) (PreparedDocument, error) { } type generatorDependencies struct { - loadDefault func(context.Context) (profiles.Bundle, error) + loadProfile func(context.Context, string) (profiles.Bundle, error) newRenderer func(*profile.AssetReader) render.Renderer legacyBuild func(string, string) ([]byte, error) } func defaultGeneratorDependencies() generatorDependencies { return generatorDependencies{ - loadDefault: profiles.LoadCompanyDefaultContext, + loadProfile: profiles.LoadCompanyDefaultVersionContext, newRenderer: func(assets *profile.AssetReader) render.Renderer { return word.NewAdapter(assets) }, legacyBuild: legacyBuild, } @@ -274,6 +271,7 @@ func generateWithOptions(ctx context.Context, request Request, options RenderOpt return Delivery{}, fmt.Errorf("%w: Markdown is %d bytes, limit is %d", ErrResourceLimit, len(request.Markdown), limits.MaxMarkdownBytes) } var parsed markdownrenderer.Result + var boundMetadata *markdownrenderer.Metadata materialized := bytes.HasPrefix(bytes.TrimSpace(request.Markdown), []byte("---")) if materialized { parsed, err = parseMarkdownContext(ctx, request.Markdown, false, limits) @@ -283,17 +281,20 @@ func generateWithOptions(ctx context.Context, request Request, options RenderOpt if err := validateWorkspaceRoute(request, parsed.Metadata); err != nil { return Delivery{}, err } + boundMetadata = &parsed.Metadata } - bundle, err := dependencies.loadDefault(ctx) + boundVersion, err := profileVersionFor(request, boundMetadata) if err != nil { - return Delivery{}, fmt.Errorf("docgen: load company default: %w", err) + return Delivery{}, err + } + bundle, err := dependencies.loadProfile(ctx, boundVersion) + if err != nil { + return Delivery{}, fmt.Errorf("docgen: load company default %s: %w", boundVersion, err) } profileID, profileVersion := bundle.Snapshot.ProfileIdentity() - if request.ProfileID != "" || request.ProfileVersion != "" { - if request.ProfileID != profileID || request.ProfileVersion != profileVersion { - return Delivery{}, fmt.Errorf("%w: requested %s@%s, available %s@%s", ErrProfileMismatch, - request.ProfileID, request.ProfileVersion, profileID, profileVersion) - } + if profileID != defaultProfileID || profileVersion != boundVersion { + return Delivery{}, fmt.Errorf("%w: loaded %s@%s, bound %s@%s", ErrProfileMismatch, + profileID, profileVersion, defaultProfileID, boundVersion) } if !materialized { source, sourceErr := workspaceMarkdown(request, profileID, profileVersion) @@ -481,6 +482,37 @@ func validateWorkspaceRoute(request Request, metadata markdownrenderer.Metadata) return nil } +// profileVersionFor returns the company-default version a document renders +// with. Published versions are immutable, so a version the persisted Markdown +// or the request already binds is kept; only a new, unbound document takes the +// latest published version. metadata is nil until Markdown is materialized. +func profileVersionFor(request Request, metadata *markdownrenderer.Metadata) (string, error) { + requested := request.ProfileID != "" || request.ProfileVersion != "" + if requested && !publishedProfileVersion(request.ProfileID, request.ProfileVersion) { + return "", fmt.Errorf("%w: requested %s@%s is not a published %s version", ErrProfileMismatch, + request.ProfileID, request.ProfileVersion, defaultProfileID) + } + if metadata == nil { + if requested { + return request.ProfileVersion, nil + } + return profiles.LatestCompanyDefaultVersion, nil + } + if !publishedProfileVersion(metadata.ProfileID, metadata.ProfileVersion) { + return "", fmt.Errorf("%w: Markdown binds %s@%s, which is not a published %s version", ErrProfileMismatch, + metadata.ProfileID, metadata.ProfileVersion, defaultProfileID) + } + if requested && request.ProfileVersion != metadata.ProfileVersion { + return "", fmt.Errorf("%w: requested %s@%s, Markdown binds %s@%s", ErrProfileMismatch, + request.ProfileID, request.ProfileVersion, metadata.ProfileID, metadata.ProfileVersion) + } + return metadata.ProfileVersion, nil +} + +func publishedProfileVersion(profileID, profileVersion string) bool { + return profileID == defaultProfileID && slices.Contains(profiles.CompanyDefaultVersions(), profileVersion) +} + func validateWorkspaceMetadata(request Request, metadata markdownrenderer.Metadata, profileID, profileVersion string) error { if err := validateWorkspaceRoute(request, metadata); err != nil { return err diff --git a/internal/docgen/renderer_identity_generated.go b/internal/docgen/renderer_identity_generated.go index 7dd2f37..14aad20 100644 --- a/internal/docgen/renderer_identity_generated.go +++ b/internal/docgen/renderer_identity_generated.go @@ -2,6 +2,6 @@ package docgen -const rendererRevision = "10950774736b64b5d8e544ce03d45ae5a6ceca7c" +const rendererRevision = "3f259990b5d43ec0d318f39d496816140ab559b5" const RendererIdentity = "agent-document-renderer@" + rendererRevision diff --git a/internal/docgen/renderer_integration_test.go b/internal/docgen/renderer_integration_test.go index 55db1fe..ca919b4 100644 --- a/internal/docgen/renderer_integration_test.go +++ b/internal/docgen/renderer_integration_test.go @@ -160,7 +160,7 @@ func TestGeneratePersistsCanonicalMarkdownWithStableNodeParity(t *testing.T) { if reparsed.Metadata.DocumentID != "workspace-minutes" || reparsed.Metadata.ProjectID != WorkspaceProjectID || reparsed.Metadata.DocumentType != "general" || reparsed.Metadata.Title != "研发周会纪要" || reparsed.Metadata.Language != "zh-CN" || reparsed.Metadata.ProfileID != "company.default" || - reparsed.Metadata.ProfileVersion != "1.0.0" || reparsed.Metadata.Controlled { + reparsed.Metadata.ProfileVersion != profiles.LatestCompanyDefaultVersion || reparsed.Metadata.Controlled { t.Fatalf("controlled metadata changed: %#v", reparsed.Metadata) } @@ -355,10 +355,10 @@ func TestPersistedControlledMetadataRoutesBeforeDefaultProfileLoad(t *testing.T) request.Title = "Controlled" loads := 0 deps := defaultGeneratorDependencies() - baseLoad := deps.loadDefault - deps.loadDefault = func(ctx context.Context) (profiles.Bundle, error) { + baseLoad := deps.loadProfile + deps.loadProfile = func(ctx context.Context, version string) (profiles.Bundle, error) { loads++ - return baseLoad(ctx) + return baseLoad(ctx, version) } _, err := generate(context.Background(), request, deps) if !errors.Is(err, ErrRDMRequired) || loads != 0 { @@ -367,7 +367,7 @@ func TestPersistedControlledMetadataRoutesBeforeDefaultProfileLoad(t *testing.T) } func TestRendererIdentityMatchesPublishedPin(t *testing.T) { - const want = "agent-document-renderer@10950774736b64b5d8e544ce03d45ae5a6ceca7c" + const want = "agent-document-renderer@3f259990b5d43ec0d318f39d496816140ab559b5" if RendererIdentity != want { t.Fatalf("RendererIdentity = %q, want %q", RendererIdentity, want) } @@ -456,6 +456,157 @@ func TestGenerateRejectsProfileAndAssetMismatch(t *testing.T) { } } +const tocFieldInstruction = `TOC \o "1-` + +func TestGenerateBindsNewDocumentsToLatestPublishedProfile(t *testing.T) { + request := workspaceRequest([]byte(representativeMarkdown)) + prepared, err := Prepare(context.Background(), request) + if err != nil { + t.Fatalf("Prepare: %v", err) + } + delivery, err := Generate(context.Background(), request) + if err != nil { + t.Fatalf("Generate: %v", err) + } + latest, err := profiles.LoadCompanyDefault() + if err != nil { + t.Fatal(err) + } + if _, version := latest.Snapshot.ProfileIdentity(); version != profiles.LatestCompanyDefaultVersion { + t.Fatalf("renderer latest company default = %s, want %s", version, profiles.LatestCompanyDefaultVersion) + } + for name, markdown := range map[string][]byte{"Prepare": prepared.Markdown.Bytes, "Generate": delivery.Markdown.Bytes} { + metadata := persistedMetadata(t, markdown) + if metadata.ProfileID != "company.default" || metadata.ProfileVersion != profiles.LatestCompanyDefaultVersion { + t.Fatalf("%s bound new document to %s@%s, want company.default@%s", name, metadata.ProfileID, metadata.ProfileVersion, profiles.LatestCompanyDefaultVersion) + } + } + if delivery.Profile.Version != profiles.LatestCompanyDefaultVersion || delivery.Profile.SnapshotHash != latest.Snapshot.SnapshotHash() { + t.Fatalf("delivery profile receipt = %#v, want latest snapshot %s", delivery.Profile, latest.Snapshot.SnapshotHash()) + } + documentXML := docxPart(t, delivery.DOCX.Bytes, "word/document.xml") + if !strings.Contains(documentXML, tocFieldInstruction) || !strings.Contains(docxVisibleText(t, delivery.DOCX.Bytes), "目录") { + t.Fatal("new document rendered with the latest company default omitted its declared TOC") + } +} + +func TestGenerateKeepsPinnedPublishedProfileVersion(t *testing.T) { + for _, test := range []struct { + version string + toc bool + }{ + {version: "1.0.0", toc: false}, + {version: "1.1.0", toc: true}, + } { + t.Run(test.version, func(t *testing.T) { + pinned, err := profiles.LoadCompanyDefaultVersion(test.version) + if err != nil { + t.Fatal(err) + } + request := workspaceRequest([]byte(representativeMarkdown)) + request.ProfileID, request.ProfileVersion = "company.default", test.version + prepared, err := Prepare(context.Background(), request) + if err != nil { + t.Fatalf("Prepare pinned %s: %v", test.version, err) + } + if metadata := persistedMetadata(t, prepared.Markdown.Bytes); metadata.ProfileVersion != test.version { + t.Fatalf("Prepare bound pinned request to %s, want %s", metadata.ProfileVersion, test.version) + } + first, err := Generate(context.Background(), request) + if err != nil { + t.Fatalf("Generate pinned %s: %v", test.version, err) + } + + replay := workspaceRequest(first.Markdown.Bytes) + second, err := Generate(context.Background(), replay) + if err != nil { + t.Fatalf("Generate persisted %s Markdown: %v", test.version, err) + } + for name, delivery := range map[string]Delivery{"pinned request": first, "persisted replay": second} { + if delivery.Profile.ProfileID != "company.default" || delivery.Profile.Version != test.version || + delivery.Profile.SnapshotHash != pinned.Snapshot.SnapshotHash() || delivery.Profile.ProfileSHA256 != pinned.Review.ProfileSHA256 { + t.Fatalf("%s profile receipt = %#v, want company.default@%s snapshot %s", name, delivery.Profile, test.version, pinned.Snapshot.SnapshotHash()) + } + if metadata := persistedMetadata(t, delivery.Markdown.Bytes); metadata.ProfileVersion != test.version { + t.Fatalf("%s Markdown binds %s, want %s", name, metadata.ProfileVersion, test.version) + } + if got := strings.Contains(docxPart(t, delivery.DOCX.Bytes, "word/document.xml"), tocFieldInstruction); got != test.toc { + t.Fatalf("%s TOC present = %v, want %v for company.default@%s", name, got, test.toc, test.version) + } + } + if !bytes.Equal(first.DOCX.Bytes, second.DOCX.Bytes) { + t.Fatal("replaying persisted Markdown changed the pinned delivery bytes") + } + }) + } +} + +func TestProfileBindingRefusesUnpublishedVersionsBeforeLoad(t *testing.T) { + persisted, err := Generate(context.Background(), workspaceRequest([]byte(representativeMarkdown))) + if err != nil { + t.Fatal(err) + } + latestBinding := []byte("profileVersion: \"" + profiles.LatestCompanyDefaultVersion + "\"\n") + if !bytes.Contains(persisted.Markdown.Bytes, latestBinding) { + t.Fatalf("persisted Markdown does not carry %q", latestBinding) + } + pinnedOld := workspaceRequest(bytes.Replace(persisted.Markdown.Bytes, latestBinding, []byte("profileVersion: \"1.0.0\"\n"), 1)) + pinnedOld.ProfileID, pinnedOld.ProfileVersion = "company.default", profiles.LatestCompanyDefaultVersion + + tests := map[string]Request{} + for name, identity := range map[string][2]string{ + "unknown version": {"company.default", "9.9.9"}, + "other profile": {"company.other", "1.0.0"}, + "missing version": {"company.default", ""}, + "missing profile id": {"", "1.0.0"}, + } { + request := workspaceRequest([]byte(representativeMarkdown)) + request.ProfileID, request.ProfileVersion = identity[0], identity[1] + tests["requested "+name] = request + } + tests["persisted unknown version"] = workspaceRequest(bytes.Replace(persisted.Markdown.Bytes, latestBinding, []byte("profileVersion: \"9.9.9\"\n"), 1)) + tests["persisted other profile"] = workspaceRequest(bytes.Replace(persisted.Markdown.Bytes, + []byte("profileId: \"company.default\"\n"), []byte("profileId: \"company.other\"\n"), 1)) + tests["request disagrees with persisted binding"] = pinnedOld + + for name, request := range tests { + t.Run(name, func(t *testing.T) { + if _, err := Prepare(context.Background(), request); !errors.Is(err, ErrProfileMismatch) { + t.Fatalf("Prepare error = %v, want ErrProfileMismatch", err) + } + loads := 0 + deps := defaultGeneratorDependencies() + baseLoad := deps.loadProfile + deps.loadProfile = func(ctx context.Context, version string) (profiles.Bundle, error) { + loads++ + return baseLoad(ctx, version) + } + if _, err := generate(context.Background(), request, deps); !errors.Is(err, ErrProfileMismatch) || loads != 0 { + t.Fatalf("Generate error = %v profileLoads=%d, want ErrProfileMismatch before load", err, loads) + } + }) + } +} + +func TestGenerateRefusesLoadedProfileWithUnexpectedIdentity(t *testing.T) { + deps := defaultGeneratorDependencies() + deps.loadProfile = func(ctx context.Context, _ string) (profiles.Bundle, error) { + return profiles.LoadCompanyDefaultVersionContext(ctx, "1.0.0") + } + if _, err := generate(context.Background(), workspaceRequest([]byte(representativeMarkdown)), deps); !errors.Is(err, ErrProfileMismatch) { + t.Fatalf("Generate with a loader returning the wrong version error = %v, want ErrProfileMismatch", err) + } +} + +func persistedMetadata(t *testing.T, markdown []byte) markdownrenderer.Metadata { + t.Helper() + parsed, err := markdownrenderer.Parse(markdown, markdownrenderer.Options{MaxBytes: 2 << 20, MaxNodes: 10_000, MaxDepth: 64}) + if err != nil { + t.Fatalf("persisted Markdown is not parseable: %v", err) + } + return parsed.Metadata +} + func TestGenerateEnforcesLimitsAndCancellation(t *testing.T) { request := workspaceRequest([]byte(representativeMarkdown)) request.Limits.MaxMarkdownBytes = 32 @@ -713,6 +864,45 @@ func TestRepresentativeLegacyComparisonRecordsOnlyIntentionalDifferences(t *test t.Errorf("corporate layout exception %s detail = %q, want published organization default", dimension.Name, dimension.Detail) } } + if dimension := comparisonDimension(t, delivery.Comparison, "table-of-contents"); dimension.Status != "intentional" || dimension.ExceptionID != comparisonExceptionCorporate { + t.Errorf("declared TOC comparison = %#v, want intentional %s", dimension, comparisonExceptionCorporate) + } + if dimension := comparisonDimension(t, delivery.Comparison, "visible-text"); dimension.Status == "accidental" { + t.Errorf("declared TOC leaked into the body visible-text comparison: %#v", dimension) + } + + request.ProfileID, request.ProfileVersion = "company.default", "1.0.0" + pinned, err := generateWithOptions(context.Background(), request, RenderOptions{MigrationComparison: true}, defaultGeneratorDependencies()) + if err != nil { + t.Fatal(err) + } + if pinned.Comparison.Assessment != "assessed" || len(pinned.Comparison.AccidentalDifferences) != 0 { + t.Fatalf("pinned 1.0.0 comparison has accidental differences: %#v", pinned.Comparison.AccidentalDifferences) + } + if dimension := comparisonDimension(t, pinned.Comparison, "table-of-contents"); dimension.Status != "match" { + t.Errorf("pinned 1.0.0 TOC comparison = %#v, want match without a TOC", dimension) + } +} + +func TestComparisonKeepsBodyTextLossAccidentalBesideDeclaredTOC(t *testing.T) { + const source = "# Report\n\n## Scope\n\nScope body.\n\n## Result\n\nResult body.\n" + request := workspaceRequest([]byte(source)) + request.Title = "Report" + delivery, err := Generate(context.Background(), request) + if err != nil { + t.Fatal(err) + } + legacy, err := legacyBuild(request.Title, "# Report\n\n## Scope\n\nScope body.\n\n## Result\n\nDifferent body.\n") + if err != nil { + t.Fatal(err) + } + receipt := compareLegacyAndShared(request.Title, []byte(source), delivery.Document, legacy, delivery.DOCX.Bytes) + if dimension := comparisonDimension(t, receipt, "table-of-contents"); dimension.Status != "intentional" { + t.Fatalf("declared TOC comparison = %#v, want intentional", dimension) + } + if dimension := comparisonDimension(t, receipt, "visible-text"); dimension.Status != "accidental" { + t.Fatalf("body text loss beside a TOC = %#v, want accidental", dimension) + } } func TestComparisonDetectsCanonicalAndDOCXTableAlignmentLoss(t *testing.T) {