diff --git a/docs/assets/personal-workspace/steward-reports/provenance/after-html-desktop.png b/docs/assets/personal-workspace/steward-reports/provenance/after-html-desktop.png new file mode 100644 index 0000000000..43131f9e59 Binary files /dev/null and b/docs/assets/personal-workspace/steward-reports/provenance/after-html-desktop.png differ diff --git a/docs/assets/personal-workspace/steward-reports/provenance/after-html-narrow.png b/docs/assets/personal-workspace/steward-reports/provenance/after-html-narrow.png new file mode 100644 index 0000000000..33e5891667 Binary files /dev/null and b/docs/assets/personal-workspace/steward-reports/provenance/after-html-narrow.png differ diff --git a/docs/assets/personal-workspace/steward-reports/provenance/after-text-desktop.png b/docs/assets/personal-workspace/steward-reports/provenance/after-text-desktop.png new file mode 100644 index 0000000000..bc5e022ddc Binary files /dev/null and b/docs/assets/personal-workspace/steward-reports/provenance/after-text-desktop.png differ diff --git a/docs/assets/personal-workspace/steward-reports/provenance/after-text-narrow.png b/docs/assets/personal-workspace/steward-reports/provenance/after-text-narrow.png new file mode 100644 index 0000000000..ec4d46f807 Binary files /dev/null and b/docs/assets/personal-workspace/steward-reports/provenance/after-text-narrow.png differ diff --git a/docs/assets/personal-workspace/steward-reports/provenance/before-html-desktop.png b/docs/assets/personal-workspace/steward-reports/provenance/before-html-desktop.png new file mode 100644 index 0000000000..ac5c58859e Binary files /dev/null and b/docs/assets/personal-workspace/steward-reports/provenance/before-html-desktop.png differ diff --git a/docs/assets/personal-workspace/steward-reports/provenance/before-text-desktop.png b/docs/assets/personal-workspace/steward-reports/provenance/before-text-desktop.png new file mode 100644 index 0000000000..a056f7655d Binary files /dev/null and b/docs/assets/personal-workspace/steward-reports/provenance/before-text-desktop.png differ diff --git a/docs/assets/personal-workspace/steward-reports/provenance/before-text-narrow.png b/docs/assets/personal-workspace/steward-reports/provenance/before-text-narrow.png new file mode 100644 index 0000000000..4a6d1d7c25 Binary files /dev/null and b/docs/assets/personal-workspace/steward-reports/provenance/before-text-narrow.png differ diff --git a/docs/assets/personal-workspace/steward-reports/provenance/source-expanded-desktop.png b/docs/assets/personal-workspace/steward-reports/provenance/source-expanded-desktop.png new file mode 100644 index 0000000000..ece0da0adc Binary files /dev/null and b/docs/assets/personal-workspace/steward-reports/provenance/source-expanded-desktop.png differ diff --git a/docs/assets/personal-workspace/steward-reports/provenance/source-expanded-narrow.png b/docs/assets/personal-workspace/steward-reports/provenance/source-expanded-narrow.png new file mode 100644 index 0000000000..2fc2b84707 Binary files /dev/null and b/docs/assets/personal-workspace/steward-reports/provenance/source-expanded-narrow.png differ diff --git a/docs/product/periodic-reports.md b/docs/product/periodic-reports.md index 3c0cbb5926..ff16b2e654 100644 --- a/docs/product/periodic-reports.md +++ b/docs/product/periodic-reports.md @@ -16,8 +16,22 @@ This is presentation validation, not a claim that a URL is reachable, public or authorized for another audience. Source collection, frozen evidence, redaction and delivery authorization retain those responsibilities. +Text reports include the same authored overview values, labels and details as +the HTML artifact. When that overview is present, it replaces the generated +title-only opening; reports without highlights retain their compiled summary. +This exposes existing editorial content, not an independent verification of it. + +Opaque source references receive stable numbers in document order. Repeated +references share one exact, redacted entry in **Source references**, including +sources used only by supporting items. Web addresses keep their existing links. +Markdown retains the index in its full linear body. HTML opens its existing +appendix when a source number is followed and moves keyboard focus to the +reference; reopening the fragment restores it. Printing includes the full index +and restores the prior disclosure state afterward. No lookup, upload or new +source authority is introduced. + An already frozen report retains its original bytes and generation identity. -Validation can reconstruct the exact previous source-link presentation without +Validation can reconstruct the exact previous inline-reference and source-link presentations without rewriting its artifacts, invoking generation or acknowledging publication. The full frozen source, authored response, bundle, candidate and body must still match; an unsupported or altered presentation remains unavailable. New reports @@ -60,6 +74,32 @@ are 390×844 viewport emulations. | --- | --- | | ![HTML retains the same source references and substantive report](../assets/personal-workspace/steward-reports/citations/html-desktop.png) | ![The standalone HTML report at a narrow viewport](../assets/personal-workspace/steward-reports/citations/html-narrow.png) | +The following same-input synthetic report contains ten substantive items and +two repeated internal source identities. The text overview now retains its +authored numbers and source gaps; nine inline digests become two indexed +references. The text changes from 2,875 to 2,420 characters while preserving +every item title, summary, detail and next action. This is a fixture result, +not a token-cost or live-report quality measurement. The expanded HTML index +also stays within a 390-pixel viewport; its exact references were read back +from the browser's printed PDF. The same frozen pre-change Markdown/HTML +artifacts remain byte-identical under read-only reconstruction. + +| Text before | Text after | +| --- | --- | +| ![Repeated digests occupy the text report](../assets/personal-workspace/steward-reports/provenance/before-text-desktop.png) | ![Authored overview and compact source numbers](../assets/personal-workspace/steward-reports/provenance/after-text-desktop.png) | + +| Narrow text before | Narrow text after | +| --- | --- | +| ![Previous text at 390 pixels](../assets/personal-workspace/steward-reports/provenance/before-text-narrow.png) | ![Overview and complete outcomes at 390 pixels](../assets/personal-workspace/steward-reports/provenance/after-text-narrow.png) | + +| HTML before | HTML after | +| --- | --- | +| ![Inline source identities in the standalone report](../assets/personal-workspace/steward-reports/provenance/before-html-desktop.png) | ![Sources remain one action from the report body](../assets/personal-workspace/steward-reports/provenance/after-html-desktop.png) | + +| Narrow HTML | Expanded source index | Narrow expanded source index | +| --- | --- | --- | +| ![The report body at 390 pixels](../assets/personal-workspace/steward-reports/provenance/after-html-narrow.png) | ![Complete references in the existing appendix](../assets/personal-workspace/steward-reports/provenance/source-expanded-desktop.png) | ![Full source identities wrap inside the narrow report](../assets/personal-workspace/steward-reports/provenance/source-expanded-narrow.png) | + The same local calendar can explicitly repeat an already completed native Task in its original managed Session. This backend/configuration slice is described below; arbitrary new tasks from natural language and the unified Agent UI diff --git a/loopx/capabilities/periodic_report/private_generation.py b/loopx/capabilities/periodic_report/private_generation.py index 0c4efd9282..d83c78bbe4 100644 --- a/loopx/capabilities/periodic_report/private_generation.py +++ b/loopx/capabilities/periodic_report/private_generation.py @@ -252,8 +252,12 @@ def validate_private_generation(folder: Path) -> dict[str, Any]: render_periodic_report_markdown(bundle["document"]), render_periodic_report_html(bundle["document"])]) if checked != bundle: # Rendering can improve after a window was frozen. Reconstruct only the - # exact prior source-link presentation; never rewrite or regenerate it. + # exact prior presentation; never rewrite or regenerate it. # Both candidates still require full bundle, source and body equality. + checked = build_periodic_report_generation_bundle(document=bundle["document"], artifacts=[ + render_periodic_report_markdown(bundle["document"], _legacy_layout=True), + render_periodic_report_html(bundle["document"], _legacy_layout=True)]) + if checked != bundle: checked = build_periodic_report_generation_bundle(document=bundle["document"], artifacts=[ render_periodic_report_markdown(bundle["document"], _legacy_source_links=True), render_periodic_report_html(bundle["document"], _legacy_source_links=True)]) diff --git a/loopx/presentation/renderers/periodic_report_html.py b/loopx/presentation/renderers/periodic_report_html.py index b2b73a5409..60510e3b28 100644 --- a/loopx/presentation/renderers/periodic_report_html.py +++ b/loopx/presentation/renderers/periodic_report_html.py @@ -14,6 +14,7 @@ ) from ..public_safety import redact_public_text from .periodic_report_markdown import ( + _source_references, periodic_report_labels, periodic_report_source_url, render_periodic_report_markdown, @@ -204,6 +205,7 @@ def _render_item( *, index: int, labels: Mapping[str, str], + references: Mapping[str, int], _legacy_source_links: bool = False, ) -> str: title = _safe_text(item.get("title"), maximum=240) @@ -242,6 +244,12 @@ def _render_item( 'target="_blank" rel="noopener noreferrer">' f'{labels["source_link"]}' ) + elif isinstance(source_ref, str) and source_ref in references: + number = references[source_ref] + source = ( + f'' + f'{labels["source"]} {number}' + ) elif source_ref: source = ( f'{_safe_text(source_ref, maximum=500)}' @@ -269,13 +277,14 @@ def _render_section( section_index: int, item_start: int, labels: Mapping[str, str], + references: Mapping[str, int], _legacy_source_links: bool = False, ) -> tuple[str, int]: section_id = _safe_attr(section.get("section_id"), maximum=128) title = _safe_text(section.get("title"), maximum=160) items = _primary_items(section) rows = "".join( - _render_item(item, index=item_start + offset, labels=labels, + _render_item(item, index=item_start + offset, labels=labels, references=references, _legacy_source_links=_legacy_source_links) for offset, item in enumerate(items) ) @@ -297,6 +306,7 @@ def _render_supporting_sections( sections: Sequence[Mapping[str, Any]], *, labels: Mapping[str, str], + references: Mapping[str, int], _legacy_source_links: bool = False, ) -> str: groups = [] @@ -321,6 +331,13 @@ def _render_supporting_sections( if source_url else "" ) + reference = item.get("source_ref") + if isinstance(reference, str) and reference in references: + number = references[reference] + source = ( + f'' + f'{labels["source"]} {number}' + ) notes.append( '
' f'

{label}

' @@ -373,6 +390,48 @@ def _render_source_receipts( ) +def _render_source_references(references: Mapping[str, int], labels: Mapping[str, str]) -> str: + if not references: + return "" + rows = "".join( + f'
  • ' + f'{_safe_text(reference, maximum=500)}
  • ' + for reference, number in references.items() + ) + return f'

    {labels["source_references"]}

      {rows}
    ' + + +# Kept separate from the prior bytes for exact frozen-artifact reconstruction. +_SOURCE_CSS = """ +.layout > main, .layout > .rail { min-width: 0; } +.source-index { margin-bottom: 18px; } +.source-index h3 { margin: 0 0 10px; font: 600 13px/1.4 var(--sans); } +.source-index ol { padding-left: 24px; } +.source-index li { padding: 5px 0; font-size: 12px; } +.source-index code { overflow-wrap: anywhere; font: 11px/1.6 var(--mono); } +@media print { .supporting { display: block !important; } } +""" + +_SOURCE_SCRIPT = """ +(() => { + const appendix = document.querySelector('[data-supporting-context]'); + const reveal = (hash) => { + const target = document.getElementById(hash.slice(1)); + if (!target || !target.hasAttribute('data-report-source')) return; + appendix.open = true; + requestAnimationFrame(() => { target.scrollIntoView({block: 'center'}); target.focus({preventScroll: true}); }); + }; + document.querySelectorAll('[data-report-source-link]').forEach((link) => + link.addEventListener('click', () => reveal(link.hash))); + window.addEventListener('hashchange', () => reveal(location.hash)); + reveal(location.hash); + let wasOpen; + window.addEventListener('beforeprint', () => { if (wasOpen === undefined) wasOpen = appendix.open; appendix.open = true; }); + window.addEventListener('afterprint', () => { if (wasOpen !== undefined) appendix.open = wasOpen; wasOpen = undefined; }); +})(); +""" + + _CSS = """ :root { color-scheme: light; @@ -603,12 +662,15 @@ def _render_source_receipts( """ -def render_periodic_report_html(document: Mapping[str, Any], *, _legacy_source_links: bool = False) -> dict[str, Any]: +def render_periodic_report_html(document: Mapping[str, Any], *, _legacy_source_links: bool = False, + _legacy_layout: bool = False) -> dict[str, Any]: """Render one dense, self-contained report without changing its semantics.""" if document.get("schema_version") != DOCUMENT_SCHEMA: raise ValueError(f"document must use {DOCUMENT_SCHEMA}") validate_periodic_report_editorial(document) + _legacy_layout = _legacy_layout or _legacy_source_links + references = {} if _legacy_layout else _source_references(document) sections = _items(document.get("sections")) primary_sections = [ section @@ -625,7 +687,8 @@ def render_periodic_report_html(document: Mapping[str, Any], *, _legacy_source_l item_count = sum(len(_primary_items(section)) for section in sections) supporting_item_count = sum(len(_supporting_items(section)) for section in sections) document_digest = _document_digest(document) - markdown_artifact = render_periodic_report_markdown(document, _legacy_source_links=_legacy_source_links) + markdown_artifact = render_periodic_report_markdown(document, _legacy_source_links=_legacy_source_links, + _legacy_layout=_legacy_layout) filters = [ '{len(sources)}{labels["sources"]}' "" ) - supporting_sections = _render_supporting_sections(sections, labels=labels, + supporting_sections = _render_supporting_sections(sections, labels=labels, references=references, _legacy_source_links=_legacy_source_links) content = ( "\n" @@ -691,7 +757,7 @@ def render_periodic_report_html(document: Mapping[str, Any], *, _legacy_source_l '" - f"{title}" + f"{title}" '
    ' '
    ' @@ -712,6 +778,7 @@ def render_periodic_report_html(document: Mapping[str, Any], *, _legacy_source_l '
    ' f'{labels["supporting_context"]}
    ' f"{supporting_sections}" + f"{_render_source_references(references, labels)}" '
    ' '" - f"\n" + f"\n" ) content_digest = hashlib.sha256(content.encode("utf-8")).hexdigest() return { diff --git a/loopx/presentation/renderers/periodic_report_markdown.py b/loopx/presentation/renderers/periodic_report_markdown.py index a00bb6bbd0..f9f4e5b084 100644 --- a/loopx/presentation/renderers/periodic_report_markdown.py +++ b/loopx/presentation/renderers/periodic_report_markdown.py @@ -73,6 +73,17 @@ def _markdown_source(value: object, labels: Mapping[str, str], *, _legacy_source return f"{labels['source']}{labels['label_separator']}{fence}{padding}{source}{padding}{fence}" +def _source_references(document: Mapping[str, Any]) -> dict[str, int]: + """Number opaque references once, in document order, for both formats.""" + references: dict[str, int] = {} + for section in _items(document.get("sections")): + for item in _items(section.get("items")): + reference = item.get("source_ref") + if isinstance(reference, str) and reference and not periodic_report_source_url(reference): + references.setdefault(reference, len(references) + 1) + return references + + def periodic_report_labels(language: object) -> dict[str, str]: """Return the small built-in report UI vocabulary for one language.""" @@ -82,6 +93,7 @@ def periodic_report_labels(language: object) -> dict[str, str]: "no_items": "暂无条目。", "next": "下一步", "source": "来源", + "source_references": "来源标识", "supporting_appendix": "附录:投递与运行信息", "label_separator": ":", } @@ -90,6 +102,7 @@ def periodic_report_labels(language: object) -> dict[str, str]: "no_items": "No items.", "next": "Next", "source": "source", + "source_references": "Source references", "supporting_appendix": "Appendix: delivery and runtime context", "label_separator": ": ", } @@ -100,6 +113,7 @@ def _render_markdown_item( item: Mapping[str, Any], *, labels: Mapping[str, str], + references: Mapping[str, int], _legacy_source_links: bool = False, ) -> None: item_title = _safe(item.get("title"), maximum=240) @@ -109,7 +123,10 @@ def _render_markdown_item( if status: headline += f" · `{status}`" if source_ref: - headline += f" · {_markdown_source(source_ref, labels, _legacy_source_links=_legacy_source_links)}" + if isinstance(source_ref, str) and source_ref in references: + headline += f" · {labels['source']} {references[source_ref]}" + else: + headline += f" · {_markdown_source(source_ref, labels, _legacy_source_links=_legacy_source_links)}" lines.append(headline) lines.append(f" - {_safe(item.get('summary'), maximum=1000)}") for detail in _items(item.get("details")): @@ -129,12 +146,16 @@ def render_periodic_report_markdown( document: Mapping[str, Any], *, _legacy_source_links: bool = False, + _legacy_layout: bool = False, ) -> dict[str, Any]: """Render normalized sections without changing ordering or business meaning.""" if document.get("schema_version") != DOCUMENT_SCHEMA: raise ValueError(f"document must use {DOCUMENT_SCHEMA}") validate_periodic_report_editorial(document) + # Read-only reconstruction of artifacts frozen before these presentation fixes. + _legacy_layout = _legacy_layout or _legacy_source_links + references = {} if _legacy_layout else _source_references(document) title = _safe(document.get("title"), maximum=200) window = _mapping(document.get("period_window")) editorial = _mapping(document.get("editorial")) @@ -150,7 +171,15 @@ def render_periodic_report_markdown( ), ] summary = _safe(editorial.get("summary"), maximum=600) - if summary: + highlights = [] if _legacy_layout else _items(editorial.get("highlights")) + if highlights: + lines.append("") + for highlight in highlights: + label = _safe(highlight.get("label"), maximum=80) + value = _safe(highlight.get("value"), maximum=48) + detail = _safe(highlight.get("detail"), maximum=160) + lines.append(f"- **{label}{labels['label_separator']}{value}**" + (f" · {detail}" if detail else "")) + elif summary: lines.extend(["", summary]) primary_item_count = 0 supporting_item_count = 0 @@ -172,13 +201,17 @@ def render_periodic_report_markdown( lines.append(f"- {labels['no_items']}") continue for item in section_items: - _render_markdown_item(lines, item, labels=labels, _legacy_source_links=_legacy_source_links) + _render_markdown_item(lines, item, labels=labels, references=references, _legacy_source_links=_legacy_source_links) if supporting_sections: lines.extend(["", f"## {labels['supporting_appendix']}"]) for section, section_items in supporting_sections: lines.extend(["", f"### {_safe(section.get('title'), maximum=160)}", ""]) for item in section_items: - _render_markdown_item(lines, item, labels=labels, _legacy_source_links=_legacy_source_links) + _render_markdown_item(lines, item, labels=labels, references=references, _legacy_source_links=_legacy_source_links) + if references: + lines.extend(["", f"## {labels['source_references']}", ""]) + for reference, number in references.items(): + lines.append(f"{number}. {_markdown_source(reference, labels)}") content = "\n".join(lines).rstrip() + "\n" content_digest = hashlib.sha256(content.encode("utf-8")).hexdigest() document_digest = hashlib.sha256( diff --git a/tests/capabilities/test_periodic_report_private_generation.py b/tests/capabilities/test_periodic_report_private_generation.py index 6c331478c6..0f7e428b66 100644 --- a/tests/capabilities/test_periodic_report_private_generation.py +++ b/tests/capabilities/test_periodic_report_private_generation.py @@ -191,7 +191,8 @@ def host_codec_read_text(path: Path, *args, **kwargs): @pytest.mark.parametrize("tamper", [None, "report", "html", "source", "response"]) -def test_frozen_report_keeps_original_source_links_across_renderer_upgrade(tmp_path, monkeypatch, tamper): +@pytest.mark.parametrize("legacy_links", [False, True]) +def test_frozen_report_keeps_prior_presentations_across_renderer_upgrade(tmp_path, monkeypatch, tamper, legacy_links): from loopx.presentation.renderers import periodic_report_html as html from loopx.presentation.renderers import periodic_report_markdown as markdown @@ -203,15 +204,31 @@ def previous_html_source(value, **_): parsed = urlparse(raw) return escape(raw, quote=True) if parsed.scheme in {"http", "https"} and parsed.netloc else None - # Seed through the real freezer with the previous independently specified - # source rules, not through the new validator's compatibility switch. + prior_markdown = markdown.render_periodic_report_markdown + prior_html = html.render_periodic_report_html + # Seed through the real freezer, retaining both shipped inline layouts. + # The old source rules remain independently specified, not inferred from + # the new link-eligibility helper or the validator's selected fallback. with monkeypatch.context() as previous: - previous.setattr(markdown, "_markdown_source", lambda value, labels, **_: - f"[{labels['source']}]({markdown._safe(value, maximum=500)})") - previous.setattr(html, "_safe_http_url", previous_html_source) + previous.setattr(markdown, "render_periodic_report_markdown", lambda document, **kwargs: + prior_markdown(document, **{**kwargs, "_legacy_layout": True})) + previous.setattr(html, "render_periodic_report_html", lambda document, **kwargs: + prior_html(document, **{**kwargs, "_legacy_layout": True})) + if legacy_links: + previous.setattr(markdown, "_markdown_source", lambda value, labels, **_: + f"[{labels['source']}]({markdown._safe(value, maximum=500)})") + previous.setattr(html, "_safe_http_url", previous_html_source) receipt = freeze_private_report(root=root, window=window, response=response) original = {path.name: path.read_bytes() for path in folder.iterdir() if path.is_file()} - assert "[来源](sha256:" in original["report.md"].decode("utf-8") + # Independent historical renderer oracles: parent of #7413 for old links, + # e20e4b684 for safe inline references. The input/window above is fixed. + expected_text_digest = ( + "509ff2e53bbce2f703049f1684e1a9f2048826ebb8c386891650b52841a1ed23" + if legacy_links else + "8d97d55e80f759e7eaadb73c5df93afea4d5cfbc071301a9726cb47ff37f9755" + ) + assert hashlib.sha256(original["report.md"]).hexdigest() == expected_text_digest + assert ("[来源](sha256:" in original["report.md"].decode("utf-8")) is legacy_links assert "[来源](sha256:" not in markdown.render_periodic_report_markdown( json.loads(original["generation-bundle.json"])["document"])["content"] diff --git a/tests/presentation/test_periodic_report_html.py b/tests/presentation/test_periodic_report_html.py index 51a4f0cb7f..44951fcd1f 100644 --- a/tests/presentation/test_periodic_report_html.py +++ b/tests/presentation/test_periodic_report_html.py @@ -119,10 +119,7 @@ def test_html_artifact_is_self_contained_interactive_and_registry_valid() -> Non assert 'data-section-filter="completed"' in content assert 'data-presentation="editorial_dense_v2"' in content assert '' in content - assert ( - "Outcome: Release <script>alert(1)</script>. " - "Next: Observe adoption." - ) in content + assert "Release shipped" in content and "Needs review" in content assert "July 13–20, 2026" in content assert 'href="#section-completed"' in content assert "data-copy-markdown" in content @@ -184,6 +181,59 @@ def test_html_artifact_is_deterministic_for_the_same_document() -> None: assert first["artifact_ref"] == second["artifact_ref"] +@pytest.mark.parametrize("language", ["en", "zh-CN"]) +def test_text_report_preserves_authored_overview_and_companion_copy(language) -> None: + document = _document(language=language) + document["editorial"]["highlights"][0]["detail"] = "Latency fell from 18 s to 11 s; rollout remains partial." + markdown = periodic_report_markdown_renderer_adapter().render(document) + html = periodic_report_html_renderer_adapter().render(document) + + for value in ("Release shipped", "Needs review", "Latency fell from 18 s to 11 s; rollout remains partial."): + assert value in markdown["content"] + assert value in html["content"] + assert html["companion_markdown_digest"] == markdown["content_digest"] + # A generated title-only summary adds no facts to this authored overview. + assert document["editorial"]["summary"] not in markdown["content"] + + +@pytest.mark.parametrize("language", ["en", "zh-CN"]) +def test_repeated_opaque_sources_are_lossless_references_instead_of_body_noise(language) -> None: + document = _document(language=language) + first = "sha256:" + "a" * 64 + second = "sha256:" + "b" * 64 + for item in document["sections"][0]["items"]: + item["source_ref"] = first + document["sections"][1]["items"][0]["source_ref"] = second + markdown = periodic_report_markdown_renderer_adapter().render(document)["content"] + html = periodic_report_html_renderer_adapter().render(document)["content"] + + assert markdown.count(first) == markdown.count(second) == 1 + body, appendix = html.split('
    ', 1) + assert first not in body and second not in body + assert first in appendix and second in appendix + for value in ("Published & verified.", "One staged rollout completed.", "Observe adoption.", "Delivery digests were verified."): + assert value in markdown + # Two distinct identities stay distinct, including supporting-only sources. + assert 'href="#report-source-1"' in body + assert 'id="report-source-1"' in appendix + assert 'id="report-source-2"' in appendix + + +def test_report_without_highlights_keeps_its_summary_and_web_links() -> None: + document = _document() + document["editorial"].pop("highlights") + for section in document["sections"]: + for item in section["items"]: + item["source_ref"] = "https://example.test/releases/2.5" + markdown = periodic_report_markdown_renderer_adapter().render(document)["content"] + html = periodic_report_html_renderer_adapter().render(document)["content"] + assert document["editorial"]["summary"] in markdown + assert "Outcome: Release <script>alert(1)</script>." in html + assert "## Source references" not in markdown + assert "data-report-source-link" not in html + assert markdown.count("[source](https://example.test/releases/2.5)") == 3 + + def test_html_and_markdown_share_one_document_and_primary_content() -> None: document = _document() html_artifact = periodic_report_html_renderer_adapter().render(document) @@ -202,7 +252,8 @@ def test_html_and_markdown_share_one_document_and_primary_content() -> None: "Observe adoption.", "Release candidate", "Ready for review.", - "Outcome: Release <script>alert(1)</script>. Next: Observe adoption.", + "Release shipped", + "Needs review", ): assert expected in html_artifact["content"] for expected in ( @@ -212,7 +263,8 @@ def test_html_and_markdown_share_one_document_and_primary_content() -> None: "Observe adoption.", "Release candidate", "Ready for review.", - "Outcome: Release . Next: Observe adoption.", + "Release shipped", + "Needs review", ): assert expected in markdown_artifact["content"] assert "Appendix: delivery and runtime context" in markdown_artifact["content"]