Preserve original ordered-list numbering during sanitization

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a192e9-c28b-7533-bf25-e2341364c22a
This commit is contained in:
tegwick 2026-10-31 18:21:13 +01:00
parent 80fe387dcc
commit a9526e7fb3
3 changed files with 68 additions and 1 deletions

View file

@ -52,6 +52,7 @@ _ALLOWED_ATTRS = {
"img": ["src", "alt", "title"],
"a": ["href", "title", "rel"],
"code": ["class"],
"ol": ["start"],
"th": ["align"],
"td": ["align"],
}
@ -100,7 +101,8 @@ def render_markdown(text: str) -> str:
"""Render the supported Markdown subset.
Extensions are fenced code, tables, newline-to-break and sane lists.
Raw HTML, script, event handlers and non-http(s)/mailto URLs are removed.
Ordered-list starting numbers are preserved. Unsupported HTML, scripts,
event handlers and non-http(s)/mailto URLs are removed.
Relative asset URLs are kept so copied page images still resolve. This is
not acceptance of a private corpus or of arbitrary Bubble markup.
"""

View file

@ -0,0 +1,42 @@
{
"schema": "coulomb.ordered-list-fidelity.v1",
"verified_at": "2026-10-31T17:20:32.943192+00:00",
"source_articles_sha256": "f33967c79c0f8a60a7d504f5d5e4082f8bc9e3c34f6a3160cb200fd380831d62",
"prior_audit_sha256": "3926e24a3b903f5e1d1c7133dc5f9df1e958183432c5131949ce838eb60acf1e",
"current_audit_sha256": "d8f382e24d7adfe3c43dc71f32f8f6e647bf68239cbcdc6769a6c8fb85d666b9",
"records": 245,
"rendered_cards_verified": 242,
"source_bodies_unchanged": true,
"missing_bodies_retained": 3,
"ordered_list_starts_restored": 15,
"affected_records": 4,
"markup_change_cases_before": 6,
"markup_change_cases_after": 3,
"remaining_removed_tags": {
"topic": 3
},
"remaining_removed_attributes": [
{
"tag": "topic",
"attribute": "name",
"count": 2
}
],
"private_review_directory": "~/.config/coulomb-social/auth/exports/rendering-review-20261031-numbering",
"source_hashes": {
"coulomb_social/apps/spaces/content.py": "5d6bb5c800b3e776fff385fa73284f4916403676dc2a8bc06b8fadc7d2387633",
"tests/test_rendering_audit.py": "ae092753f4d1d006bb88f1ce11581975c105c5231819f9409ddaaf2fc83d32f5"
},
"validation": {
"focused_passed": 14,
"full_gate_passed": 650,
"full_gate_warnings": 69,
"failures": 0,
"errors": 0,
"skipped": 0,
"full_gate_seconds": 91.85
},
"production_deployed": false,
"original_bubble_render_compared": false,
"productive_fidelity_accepted": false
}

View file

@ -52,6 +52,29 @@ def test_serialized_line_break_difference_is_not_reported_as_lost_markup():
assert not audit["visible_text_changed_by_sanitization"]
def test_continued_numbered_lists_keep_their_starts_through_sanitization():
body = "2. Second item\n3. Third item\n\nSeparate paragraph.\n\n8. Eighth item"
audit, rendered = audit_body(body)
assert '<ol start="2">' in rendered
assert '<ol start="8">' in rendered
assert not audit["markup_changed_by_sanitization"]
assert not audit["visible_text_changed_by_sanitization"]
assert not audit["productive_fidelity_accepted"]
def test_list_start_permission_does_not_allow_event_handlers_or_custom_markup():
body = ('<ol start="4" onclick="synthetic()"><li>Fourth item</li></ol>'
'<topic name="synthetic">Placeholder</topic>'
'<a href="javascript:synthetic()">Link</a>')
audit, rendered = audit_body(body)
assert '<ol start="4">' in rendered
assert 'onclick' not in rendered and 'javascript:' not in rendered
assert '<topic' not in rendered and 'Placeholder' in rendered
assert {a["attribute"] for a in audit["removed_attributes"]} == {
"onclick", "name", "href",
}
def test_review_card_escapes_source_and_blocks_resources_and_interaction():
body = '<img src="https://example.invalid/synthetic.png"><script>synthetic()</script>'
audit, rendered = audit_body(body)