typsphinx 0.6.2__tar.gz → 0.6.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. {typsphinx-0.6.2 → typsphinx-0.6.4}/PKG-INFO +16 -15
  2. {typsphinx-0.6.2 → typsphinx-0.6.4}/README.md +14 -13
  3. {typsphinx-0.6.2 → typsphinx-0.6.4}/pyproject.toml +2 -2
  4. typsphinx-0.6.4/tests/test_no_stale_github_io_links.py +151 -0
  5. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_pdf_render_gate.py +328 -0
  6. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_preview_version_sync.py +44 -1
  7. typsphinx-0.6.4/tests/test_readthedocs_config.py +359 -0
  8. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_template_engine.py +428 -0
  9. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_translator.py +309 -0
  10. typsphinx-0.6.4/tests/test_typst_elements_pass_through_gate.py +422 -0
  11. typsphinx-0.6.4/tests/test_typst_lang_gate.py +860 -0
  12. {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx/__init__.py +0 -1
  13. {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx/template_engine.py +267 -24
  14. {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx/templates/base.typ +2 -1
  15. {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx/translator.py +167 -25
  16. {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx/writer.py +51 -6
  17. {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx.egg-info/PKG-INFO +16 -15
  18. {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx.egg-info/SOURCES.txt +4 -4
  19. typsphinx-0.6.2/tests/test_config_toctree_defaults.py +0 -245
  20. typsphinx-0.6.2/tests/test_documentation_configuration.py +0 -156
  21. typsphinx-0.6.2/tests/test_documentation_installation.py +0 -143
  22. typsphinx-0.6.2/tests/test_documentation_usage.py +0 -153
  23. {typsphinx-0.6.2 → typsphinx-0.6.4}/LICENSE +0 -0
  24. {typsphinx-0.6.2 → typsphinx-0.6.4}/setup.cfg +0 -0
  25. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_abbr_pep_separator_render_gate.py +0 -0
  26. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_admonitions.py +0 -0
  27. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_builder.py +0 -0
  28. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_builder_output_stem.py +0 -0
  29. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_builder_requirement13.py +0 -0
  30. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_codly_caption_listitem_leak_render_gate.py +0 -0
  31. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_config.py +0 -0
  32. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_config_other_options.py +0 -0
  33. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_config_template_mapping.py +0 -0
  34. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_confval_field_body_render_gate.py +0 -0
  35. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_confval_field_spacing_render_gate.py +0 -0
  36. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_corpus_gate.py +0 -0
  37. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_cross_doc_label_namespace_render_gate.py +0 -0
  38. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_deflist_definition_multiblock_render_gate.py +0 -0
  39. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_deflist_nested_definition_render_gate.py +0 -0
  40. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_deflist_term_concat_render_gate.py +0 -0
  41. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_deflist_term_inline_children_gate.py +0 -0
  42. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_desc_bodyless_concat_render_gate.py +0 -0
  43. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_desc_container_propagated_target_render_gate.py +0 -0
  44. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_desc_sig_space_render_gate.py +0 -0
  45. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_desc_signature_anchor_render_gate.py +0 -0
  46. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_desc_signature_concat_render_gate.py +0 -0
  47. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_duplicate_include_label_render_gate.py +0 -0
  48. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_entry_points.py +0 -0
  49. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_epigraph_render_gate.py +0 -0
  50. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_examples_basic.py +0 -0
  51. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_examples_charged_ieee_gate.py +0 -0
  52. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_extension.py +0 -0
  53. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_external_link_style_render_gate.py +0 -0
  54. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_field_list_in_list_item_render_gate.py +0 -0
  55. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_footnotes.py +0 -0
  56. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_glob_image_render_gate.py +0 -0
  57. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_inline_literal_overflow_render_gate.py +0 -0
  58. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_inline_references.py +0 -0
  59. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_integration_advanced.py +0 -0
  60. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_integration_basic.py +0 -0
  61. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_integration_multi_doc.py +0 -0
  62. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_integration_nested_toctree.py +0 -0
  63. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_label_at_char_render_gate.py +0 -0
  64. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_line_blocks.py +0 -0
  65. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_list_item_nested_block_render_gate.py +0 -0
  66. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_math_fallback.py +0 -0
  67. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_math_mitex.py +0 -0
  68. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_math_native.py +0 -0
  69. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_missing_and_malformed_master_gate.py +0 -0
  70. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_nested_master_render_gate.py +0 -0
  71. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_nested_toctree_paths.py +0 -0
  72. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_package_only_config_gate.py +0 -0
  73. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_package_template_routing.py +0 -0
  74. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_paragraph_concat_render_gate.py +0 -0
  75. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_paragraph_propagated_target_render_gate.py +0 -0
  76. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_paragraph_soft_newline_render_gate.py +0 -0
  77. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_pdf_generation.py +0 -0
  78. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_preview_smoke_gate.py +0 -0
  79. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_readme_version_sync.py +0 -0
  80. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_ref_target_nested_list_render_gate.py +0 -0
  81. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_rubric_option_concat_render_gate.py +0 -0
  82. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_rubric_propagated_target_render_gate.py +0 -0
  83. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_static_asset_copy_gate.py +0 -0
  84. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_substitution_definition_render_gate.py +0 -0
  85. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_table_in_list_item_render_gate.py +0 -0
  86. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_target_label_render_gate.py +0 -0
  87. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_target_name_render_gate.py +0 -0
  88. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_template_assets.py +0 -0
  89. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_template_codly.py +0 -0
  90. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_template_import_path.py +0 -0
  91. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_template_mitex.py +0 -0
  92. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_toctree_requirement13.py +0 -0
  93. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_topics.py +0 -0
  94. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_typst_string_escape_gate.py +0 -0
  95. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_wide_table_render_gate.py +0 -0
  96. {typsphinx-0.6.2 → typsphinx-0.6.4}/tests/test_xref_orphan_degrade_render_gate.py +0 -0
  97. {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx/builder.py +0 -0
  98. {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx/pdf.py +0 -0
  99. {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx.egg-info/dependency_links.txt +0 -0
  100. {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx.egg-info/entry_points.txt +0 -0
  101. {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx.egg-info/requires.txt +0 -0
  102. {typsphinx-0.6.2 → typsphinx-0.6.4}/typsphinx.egg-info/top_level.txt +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typsphinx
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: Sphinx extension for Typst output
5
5
  Author-email: YuSabo <yusabo90002@gmail.com>
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/YuSabo90002/typsphinx
8
- Project-URL: Documentation, https://github.com/YuSabo90002/typsphinx#readme
8
+ Project-URL: Documentation, https://typsphinx.readthedocs.io/
9
9
  Project-URL: Repository, https://github.com/YuSabo90002/typsphinx
10
10
  Project-URL: Issues, https://github.com/YuSabo90002/typsphinx/issues
11
11
  Keywords: sphinx,typst,documentation,pdf
@@ -49,11 +49,11 @@ Dynamic: license-file
49
49
  [![Python Support](https://img.shields.io/pypi/pyversions/typsphinx.svg)](https://pypi.org/project/typsphinx/)
50
50
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
51
51
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
52
- [![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://yusabo90002.github.io/typsphinx/)
52
+ [![Documentation Status](https://app.readthedocs.org/projects/typsphinx/badge/?version=latest)](https://typsphinx.readthedocs.io/)
53
53
 
54
54
  Sphinx extension for Typst output format support.
55
55
 
56
- **[📖 Documentation](https://yusabo90002.github.io/typsphinx/)** | **[🐛 Issue Tracker](https://github.com/YuSabo90002/typsphinx/issues)** | **[📦 PyPI](https://pypi.org/project/typsphinx/)**
56
+ **[📖 Documentation](https://typsphinx.readthedocs.io/)** | **[🐛 Issue Tracker](https://github.com/YuSabo90002/typsphinx/issues)** | **[📦 PyPI](https://pypi.org/project/typsphinx/)**
57
57
 
58
58
  ## Overview
59
59
 
@@ -249,7 +249,6 @@ Below are the main configuration options. This is not the complete set — see [
249
249
  - `typst_template`: Custom template path
250
250
  - `typst_elements`: Template parameters (paper size, fonts, etc.)
251
251
  - `typst_template_mapping`: Sphinx metadata to template parameter mapping
252
- - `typst_toctree_defaults`: Default toctree options
253
252
 
254
253
  ## Development
255
254
 
@@ -309,17 +308,19 @@ typsphinx/
309
308
 
310
309
  ## Documentation
311
310
 
312
- **📖 Full documentation is available at [yusabo90002.github.io/typsphinx](https://yusabo90002.github.io/typsphinx/)**
311
+ **📖 Full documentation is available at [typsphinx.readthedocs.io](https://typsphinx.readthedocs.io/)**
312
+
313
+ 日本語ドキュメントは [typsphinx.readthedocs.io/ja/latest/](https://typsphinx.readthedocs.io/ja/latest/) にあります。
313
314
 
314
315
  Quick links:
315
316
 
316
- - [Installation Guide](https://yusabo90002.github.io/typsphinx/installation.html)
317
- - [Quick Start](https://yusabo90002.github.io/typsphinx/quickstart.html)
318
- - [User Guide](https://yusabo90002.github.io/typsphinx/user_guide/)
319
- - [Configuration Reference](https://yusabo90002.github.io/typsphinx/user_guide/configuration.html)
320
- - [Examples](https://yusabo90002.github.io/typsphinx/examples/)
321
- - [API Reference](https://yusabo90002.github.io/typsphinx/api/)
322
- - [Contributing Guide](https://yusabo90002.github.io/typsphinx/contributing.html)
317
+ - [Installation Guide](https://typsphinx.readthedocs.io/en/latest/installation.html)
318
+ - [Quick Start](https://typsphinx.readthedocs.io/en/latest/quickstart.html)
319
+ - [User Guide](https://typsphinx.readthedocs.io/en/latest/user_guide/)
320
+ - [Configuration Reference](https://typsphinx.readthedocs.io/en/latest/user_guide/configuration.html)
321
+ - [Examples](https://typsphinx.readthedocs.io/en/latest/examples/)
322
+ - [API Reference](https://typsphinx.readthedocs.io/en/latest/api/)
323
+ - [Contributing Guide](https://typsphinx.readthedocs.io/en/latest/contributing.html)
323
324
 
324
325
  ## Contributing
325
326
 
@@ -349,7 +350,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
349
350
  - Integrates [mitex](https://github.com/mitex-rs/mitex) for LaTeX math
350
351
  - Uses [codly](https://typst.app/universe/package/codly) for code highlighting
351
352
  - Uses [gentle-clues](https://typst.app/universe/package/gentle-clues) for admonitions
352
- - Developed with [Claude Code](https://claude.ai/code) and [GSD](https://github.com/open-gsd/gsd-core) (spec-driven development for AI coding agents)
353
+ - Developed with [Claude Code](https://claude.com/product/claude-code) and [GSD](https://github.com/open-gsd/gsd-core) (spec-driven development for AI coding agents)
353
354
 
354
355
  ## Version History
355
356
 
@@ -357,5 +358,5 @@ See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
357
358
 
358
359
  ---
359
360
 
360
- **Status**: Stable (v0.6.2) - Production ready
361
+ **Status**: Stable (v0.6.4) - Production ready
361
362
  **Python**: 3.12+ | **Sphinx**: 9.1+ | **Typst**: 0.15+
@@ -5,11 +5,11 @@
5
5
  [![Python Support](https://img.shields.io/pypi/pyversions/typsphinx.svg)](https://pypi.org/project/typsphinx/)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
8
- [![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://yusabo90002.github.io/typsphinx/)
8
+ [![Documentation Status](https://app.readthedocs.org/projects/typsphinx/badge/?version=latest)](https://typsphinx.readthedocs.io/)
9
9
 
10
10
  Sphinx extension for Typst output format support.
11
11
 
12
- **[📖 Documentation](https://yusabo90002.github.io/typsphinx/)** | **[🐛 Issue Tracker](https://github.com/YuSabo90002/typsphinx/issues)** | **[📦 PyPI](https://pypi.org/project/typsphinx/)**
12
+ **[📖 Documentation](https://typsphinx.readthedocs.io/)** | **[🐛 Issue Tracker](https://github.com/YuSabo90002/typsphinx/issues)** | **[📦 PyPI](https://pypi.org/project/typsphinx/)**
13
13
 
14
14
  ## Overview
15
15
 
@@ -205,7 +205,6 @@ Below are the main configuration options. This is not the complete set — see [
205
205
  - `typst_template`: Custom template path
206
206
  - `typst_elements`: Template parameters (paper size, fonts, etc.)
207
207
  - `typst_template_mapping`: Sphinx metadata to template parameter mapping
208
- - `typst_toctree_defaults`: Default toctree options
209
208
 
210
209
  ## Development
211
210
 
@@ -265,17 +264,19 @@ typsphinx/
265
264
 
266
265
  ## Documentation
267
266
 
268
- **📖 Full documentation is available at [yusabo90002.github.io/typsphinx](https://yusabo90002.github.io/typsphinx/)**
267
+ **📖 Full documentation is available at [typsphinx.readthedocs.io](https://typsphinx.readthedocs.io/)**
268
+
269
+ 日本語ドキュメントは [typsphinx.readthedocs.io/ja/latest/](https://typsphinx.readthedocs.io/ja/latest/) にあります。
269
270
 
270
271
  Quick links:
271
272
 
272
- - [Installation Guide](https://yusabo90002.github.io/typsphinx/installation.html)
273
- - [Quick Start](https://yusabo90002.github.io/typsphinx/quickstart.html)
274
- - [User Guide](https://yusabo90002.github.io/typsphinx/user_guide/)
275
- - [Configuration Reference](https://yusabo90002.github.io/typsphinx/user_guide/configuration.html)
276
- - [Examples](https://yusabo90002.github.io/typsphinx/examples/)
277
- - [API Reference](https://yusabo90002.github.io/typsphinx/api/)
278
- - [Contributing Guide](https://yusabo90002.github.io/typsphinx/contributing.html)
273
+ - [Installation Guide](https://typsphinx.readthedocs.io/en/latest/installation.html)
274
+ - [Quick Start](https://typsphinx.readthedocs.io/en/latest/quickstart.html)
275
+ - [User Guide](https://typsphinx.readthedocs.io/en/latest/user_guide/)
276
+ - [Configuration Reference](https://typsphinx.readthedocs.io/en/latest/user_guide/configuration.html)
277
+ - [Examples](https://typsphinx.readthedocs.io/en/latest/examples/)
278
+ - [API Reference](https://typsphinx.readthedocs.io/en/latest/api/)
279
+ - [Contributing Guide](https://typsphinx.readthedocs.io/en/latest/contributing.html)
279
280
 
280
281
  ## Contributing
281
282
 
@@ -305,7 +306,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
305
306
  - Integrates [mitex](https://github.com/mitex-rs/mitex) for LaTeX math
306
307
  - Uses [codly](https://typst.app/universe/package/codly) for code highlighting
307
308
  - Uses [gentle-clues](https://typst.app/universe/package/gentle-clues) for admonitions
308
- - Developed with [Claude Code](https://claude.ai/code) and [GSD](https://github.com/open-gsd/gsd-core) (spec-driven development for AI coding agents)
309
+ - Developed with [Claude Code](https://claude.com/product/claude-code) and [GSD](https://github.com/open-gsd/gsd-core) (spec-driven development for AI coding agents)
309
310
 
310
311
  ## Version History
311
312
 
@@ -313,5 +314,5 @@ See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
313
314
 
314
315
  ---
315
316
 
316
- **Status**: Stable (v0.6.2) - Production ready
317
+ **Status**: Stable (v0.6.4) - Production ready
317
318
  **Python**: 3.12+ | **Sphinx**: 9.1+ | **Typst**: 0.15+
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "typsphinx"
7
- version = "0.6.2"
7
+ version = "0.6.4"
8
8
  description = "Sphinx extension for Typst output"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -53,7 +53,7 @@ docs = [
53
53
 
54
54
  [project.urls]
55
55
  Homepage = "https://github.com/YuSabo90002/typsphinx"
56
- Documentation = "https://github.com/YuSabo90002/typsphinx#readme"
56
+ Documentation = "https://typsphinx.readthedocs.io/"
57
57
  Repository = "https://github.com/YuSabo90002/typsphinx"
58
58
  Issues = "https://github.com/YuSabo90002/typsphinx/issues"
59
59
 
@@ -0,0 +1,151 @@
1
+ """
2
+ Regression guard for the retired-host -> Read the Docs URL rewrite (DOC-09).
3
+
4
+ Phase 31 rewrote every published documentation URL in README.md and
5
+ pyproject.toml: they used to point at a GitHub Pages host that had been
6
+ retired. Seven of README.md's deep links pointed at paths that no longer
7
+ resolved on that retired host and returned HTTP 404 for months; an external
8
+ user hit one of them and filed Issue #119. This module locks the rewrite
9
+ behind four hermetic invariants so a future edit that reintroduces the
10
+ retired host, drops or reorders a deep link, or version-pins a top-level
11
+ link fails a `pytest` run immediately instead of silently regressing a
12
+ second time.
13
+
14
+ All four tests parse raw file text (and `pyproject.toml` via `tomllib`) --
15
+ never package metadata -- and make no network calls. The real-HTTP checks
16
+ (curl sweeps, CI Link Check) are a separate instrument; see
17
+ `.planning/phases/31-published-url-cutover-repo-wide-link-guard/`.
18
+ """
19
+
20
+ import re
21
+ import tomllib
22
+ from pathlib import Path
23
+
24
+ REPO_ROOT = Path(__file__).resolve().parents[1]
25
+ README_PATH = REPO_ROOT / "README.md"
26
+ PYPROJECT_PATH = REPO_ROOT / "pyproject.toml"
27
+
28
+ # The retired documentation host, built from two concatenated fragments
29
+ # split across the dot. This phase's success criteria require that a fresh
30
+ # repo-wide grep for the retired host find matches only in CHANGELOG.md. If
31
+ # this module spelled the host as one literal, it would itself be a
32
+ # permanent match, making that success criterion unverifiable without a
33
+ # permanent exclusion list. Splitting the literal keeps the repo-wide grep
34
+ # clean and honest.
35
+ _RETIRED_HOST_PREFIX = "yusabo90002.github"
36
+ _RETIRED_HOST_SUFFIX = "io"
37
+ _RETIRED_HOST = f"{_RETIRED_HOST_PREFIX}.{_RETIRED_HOST_SUFFIX}"
38
+
39
+ # Matches any URL on the Read the Docs documentation host.
40
+ _READTHEDOCS_URL_RE = re.compile(r"https://typsphinx\.readthedocs\.io/[^\s)\]]*")
41
+
42
+ # Matches a README deep link's suffix -- everything after `/en/latest/`.
43
+ _DEEP_LINK_RE = re.compile(r"https://typsphinx\.readthedocs\.io/en/latest/([^\s)\]]+)")
44
+
45
+ # A language segment (`/en/` or `/ja/`) anywhere in a URL -- used to
46
+ # partition Read the Docs URLs into "deep" (language-scoped) vs. top-level.
47
+ _LANGUAGE_SEGMENT_RE = re.compile(r"/(en|ja)/")
48
+
49
+ # A version-like path segment (`/latest/`, `/stable/`, `/v1.2.3/`, ...)
50
+ # appearing anywhere in a URL, independent of whether it is preceded by a
51
+ # language segment. Used to catch a top-level link that got version-pinned
52
+ # without also picking up a language prefix.
53
+ _VERSION_LIKE_SEGMENT_RE = re.compile(r"/(latest|stable|v?\d+\.\d+(?:\.\d+)?)(?:/|$)")
54
+
55
+ # The seven quick-link deep-link suffixes, in the order they must appear in
56
+ # README.md's "## Documentation" section. A dropped, reordered, or renamed
57
+ # suffix must fail this test, not just a wrong host.
58
+ _EXPECTED_DEEP_LINK_SUFFIXES = (
59
+ "installation.html",
60
+ "quickstart.html",
61
+ "user_guide/",
62
+ "user_guide/configuration.html",
63
+ "examples/",
64
+ "api/",
65
+ "contributing.html",
66
+ )
67
+
68
+
69
+ def _read_readme_text() -> str:
70
+ return README_PATH.read_text(encoding="utf-8")
71
+
72
+
73
+ def test_readme_documentation_links_point_at_readthedocs():
74
+ """README.md must contain no URL on the retired documentation host.
75
+
76
+ Guards against a vacuous pass: asserts README.md actually contains at
77
+ least one Read the Docs URL before asserting the retired host is
78
+ absent, so an accidental deletion of the whole Documentation section
79
+ would not silently satisfy this test.
80
+ """
81
+ text = _read_readme_text()
82
+ readthedocs_urls = _READTHEDOCS_URL_RE.findall(text)
83
+ assert readthedocs_urls, (
84
+ "README.md contains no typsphinx.readthedocs.io URLs at all -- has "
85
+ "the Documentation section been removed?"
86
+ )
87
+ assert _RETIRED_HOST not in text, (
88
+ f"README.md still references the retired documentation host "
89
+ f"({_RETIRED_HOST}) -- Phase 31 rewrote every link to Read the "
90
+ "Docs; see 31-03-SUMMARY.md."
91
+ )
92
+
93
+
94
+ def test_readme_deep_links_carry_language_version_prefix():
95
+ """README.md must have exactly 7 `/en/latest/` deep links, in order.
96
+
97
+ Asserts both the count and the exact suffixes so a dropped, reordered,
98
+ or silently-retargeted link is caught -- not just a wrong host.
99
+ """
100
+ text = _read_readme_text()
101
+ deep_links = _DEEP_LINK_RE.findall(text)
102
+ assert len(deep_links) == 7, (
103
+ f"expected exactly 7 README deep links under /en/latest/, found "
104
+ f"{len(deep_links)}: {deep_links}"
105
+ )
106
+ assert deep_links == list(_EXPECTED_DEEP_LINK_SUFFIXES), (
107
+ "README.md's /en/latest/ deep links do not match the expected "
108
+ f"suffixes in order. Expected {_EXPECTED_DEEP_LINK_SUFFIXES!r}, "
109
+ f"found {tuple(deep_links)!r}."
110
+ )
111
+
112
+
113
+ def test_readme_top_level_links_carry_no_version_segment():
114
+ """Top-level README Read the Docs links must stay version-less.
115
+
116
+ D-11: the badge click-through, header link, and Documentation section
117
+ lead sentence deliberately carry no `/en/` or `/ja/` segment, so Read
118
+ the Docs' root redirect follows the project's Default Version --
119
+ Phase 33's `latest` -> `stable` flip reaches these links without
120
+ another edit. A future "fix" that pins them to an explicit version
121
+ would undo that design, not tidy it.
122
+ """
123
+ text = _read_readme_text()
124
+ all_urls = _READTHEDOCS_URL_RE.findall(text)
125
+ top_level_urls = [url for url in all_urls if not _LANGUAGE_SEGMENT_RE.search(url)]
126
+ assert top_level_urls, (
127
+ "found no version-less typsphinx.readthedocs.io URLs in README.md "
128
+ "-- expected the badge click-through, header link, and section "
129
+ "lead sentence to all be bare-root links (D-11)."
130
+ )
131
+ version_pinned = [
132
+ url for url in top_level_urls if _VERSION_LIKE_SEGMENT_RE.search(url)
133
+ ]
134
+ assert not version_pinned, (
135
+ "top-level README links must stay version-less so Read the Docs' "
136
+ "Default Version flip (Phase 33) propagates without a re-edit "
137
+ f"(D-11); found a version-like segment on: {version_pinned}"
138
+ )
139
+
140
+
141
+ def test_pyproject_documentation_url_points_at_readthedocs():
142
+ """pyproject.toml's `project.urls.Documentation` must address Read the Docs."""
143
+ with open(PYPROJECT_PATH, "rb") as f:
144
+ data = tomllib.load(f)
145
+ documentation_url = data["project"]["urls"]["Documentation"]
146
+ assert "typsphinx.readthedocs.io" in documentation_url, (
147
+ f"pyproject.toml's project.urls.Documentation is "
148
+ f"{documentation_url!r}, which does not address "
149
+ "typsphinx.readthedocs.io -- PyPI's release metadata would "
150
+ "advertise a stale documentation link."
151
+ )
@@ -126,6 +126,12 @@ def table_width_render_gate_dir(fixtures_dir):
126
126
  return fixtures_dir / "table_width_render_gate"
127
127
 
128
128
 
129
+ @pytest.fixture
130
+ def captioned_table_render_gate_dir(fixtures_dir):
131
+ """Return the path to the captioned_table_render_gate fixture project."""
132
+ return fixtures_dir / "captioned_table_render_gate"
133
+
134
+
129
135
  @pytest.fixture
130
136
  def temp_build_dir(tmp_path):
131
137
  """Provide a temporary directory for build output."""
@@ -2333,3 +2339,325 @@ class TestTableWidthRenderGate:
2333
2339
  "rendered PDF text -- table-width markup/code-mode "
2334
2340
  "regression"
2335
2341
  )
2342
+
2343
+
2344
+ # ---------------------------------------------------------------------------
2345
+ # Phase 25 (TBL-01, TBL-02, GATE-01): extend the render-gate pattern to prove
2346
+ # the Plan 25-01 depart_table figure-wrap + single <label> fix renders
2347
+ # correctly through a real sphinx-build -> typst.compile() -> pypdf
2348
+ # round-trip -- two consecutive captioned tables (the stale-buffer proof:
2349
+ # 25-RESEARCH.md Verified Mechanism 2), a caption+:width: composition case,
2350
+ # a :numref:/:ref:-resolves case, and a captioned csv-table + list-table
2351
+ # case (D-05). ``TestCaptionedTablePreFixBasisFailureProof`` below closes
2352
+ # D-06's durable fail-pre-fix red->green requirement by reconstructing the
2353
+ # two pre-fix defect shapes from first principles now that Plan 25-01's fix
2354
+ # has replaced the buggy code -- the double-anchor collision (Critical
2355
+ # Pitfall 3, 25-RESEARCH.md) and the stale-buffer caption loss (Verified
2356
+ # Mechanism 2) -- mirroring the ``TestPreFixBasisFailureProof`` convention
2357
+ # already established in tests/test_package_only_config_gate.py.
2358
+ # ---------------------------------------------------------------------------
2359
+
2360
+ # Sentinel tokens for the captioned-table render gate -- distinctive,
2361
+ # ALLCAPS-no-space words chosen so `full_text.count(...)` can prove each
2362
+ # caption reached the compiled PDF exactly once (guarding against both the
2363
+ # stray-heading bug -- Verified Mechanism 1 -- and the 2nd-table
2364
+ # stale-buffer caption-loss bug -- Verified Mechanism 2). Must match the
2365
+ # sentinel tokens embedded in captioned_table_render_gate/index.rst.
2366
+ TBLCAP_FIRST_SENTINEL = "TBLCAPFIRSTSENTINEL"
2367
+ TBLCAP_SECOND_SENTINEL = "TBLCAPSECONDSENTINEL"
2368
+ TBLCAP_WIDTH_SENTINEL = "TBLCAPWIDTHSENTINEL"
2369
+ TBLCAP_CSV_SENTINEL = "TBLCAPCSVSENTINEL"
2370
+ TBLCAP_LIST_SENTINEL = "TBLCAPLISTSENTINEL"
2371
+
2372
+
2373
+ class _CaptionedTableRenderGateArtifacts:
2374
+ """
2375
+ Plain container for the one real-compile artifact shared by every
2376
+ ``TestCaptionedTableRenderGate`` test method: the emitted Typst source
2377
+ text and the pypdf-extracted PDF text. A plain class (not a dict/tuple)
2378
+ so each thin test method below can read ``.typ_source``/``.full_text``
2379
+ by name.
2380
+ """
2381
+
2382
+ def __init__(self, typ_source: str, full_text: str) -> None:
2383
+ self.typ_source = typ_source
2384
+ self.full_text = full_text
2385
+
2386
+
2387
+ @pytest.fixture(scope="class")
2388
+ def captioned_table_render_gate_artifacts(tmp_path_factory):
2389
+ """
2390
+ Build + real-compile the captioned_table_render_gate fixture ONCE per
2391
+ class and return both the emitted Typst source text and the
2392
+ pypdf-extracted PDF text.
2393
+
2394
+ Class-scoped so the four thin test methods below each assert a
2395
+ disjoint slice of the SAME real-compile artifact rather than
2396
+ re-running sphinx-build/typst.compile() four times (mirrors the
2397
+ ``topic_line_block_render_gate_pdf_text`` class-scoped fixture pattern
2398
+ above). Depends only on ``tmp_path_factory`` (session-scoped-
2399
+ compatible) -- not on the narrower, function-scoped
2400
+ ``captioned_table_render_gate_dir``/``fixtures_dir`` fixtures, to avoid
2401
+ a pytest ScopeMismatch (a class-scoped fixture may not depend on a
2402
+ narrower, function-scoped one).
2403
+ """
2404
+ source_dir = Path(__file__).parent / "fixtures" / "captioned_table_render_gate"
2405
+ build_dir = tmp_path_factory.mktemp("captioned_table_gate") / "_build"
2406
+
2407
+ result = _run_sphinx_build_typst(source_dir, build_dir)
2408
+ assert (
2409
+ result.returncode == 0
2410
+ ), f"sphinx-build failed:\nstdout: {result.stdout}\nstderr: {result.stderr}"
2411
+
2412
+ index_typ = build_dir / "index.typ"
2413
+ assert index_typ.exists(), "index.typ was not generated"
2414
+ typ_source = index_typ.read_text(encoding="utf-8")
2415
+
2416
+ # Compile the emitted .typ to PDF with typst-py, WITHOUT try/except:
2417
+ # this is the crux of GATE-01 -- the double-anchor Critical Pitfall 3
2418
+ # fatal (a captioned table's ids[0] defined twice) would abort the
2419
+ # ENTIRE compile here, failing every dependent test method loudly,
2420
+ # rather than merely failing a string-agreement check.
2421
+ pdf_output = build_dir / "index.pdf"
2422
+ typst.compile(str(index_typ), output=str(pdf_output))
2423
+
2424
+ assert pdf_output.exists(), "PDF file was not created"
2425
+ assert pdf_output.stat().st_size > 0, "PDF file is empty"
2426
+ with open(pdf_output, "rb") as f:
2427
+ magic = f.read(4)
2428
+ assert magic == b"%PDF", "Generated file is not a valid PDF"
2429
+
2430
+ reader = pypdf.PdfReader(str(pdf_output))
2431
+ full_text = "\n".join(page.extract_text() for page in reader.pages)
2432
+
2433
+ return _CaptionedTableRenderGateArtifacts(typ_source, full_text)
2434
+
2435
+
2436
+ @pytest.mark.slow
2437
+ @pytest.mark.skipif(
2438
+ not (TYPST_AVAILABLE and PYPDF_AVAILABLE),
2439
+ reason="typst-py and pypdf are both required for the GATE-01 render gate",
2440
+ )
2441
+ class TestCaptionedTableRenderGate:
2442
+ """
2443
+ Real-compile acceptance gate for TBL-01/TBL-02: the Plan 25-01
2444
+ depart_table figure-wrap + single <label> fix, covering two
2445
+ consecutive captioned tables (the stale-buffer proof), a
2446
+ caption+:width: composition case, a :numref:/:ref:-resolves case, and
2447
+ a captioned csv-table + list-table case (D-05).
2448
+
2449
+ Requirements: TBL-01, TBL-02, GATE-01 (25-CONTEXT.md, 25-RESEARCH.md
2450
+ Validation Architecture, 25-01-SUMMARY.md).
2451
+
2452
+ All four test methods share the SAME real-compile artifact via the
2453
+ class-scoped ``captioned_table_render_gate_artifacts`` fixture above --
2454
+ sphinx-build/typst.compile() run exactly once per class, not once per
2455
+ test method.
2456
+ """
2457
+
2458
+ def test_each_caption_sentinel_appears_exactly_once(
2459
+ self, captioned_table_render_gate_artifacts
2460
+ ):
2461
+ """
2462
+ Each of the five caption sentinels appears EXACTLY ONCE in the
2463
+ extracted PDF text -- proving neither the stray-heading bug
2464
+ (Verified Mechanism 1) nor the 2nd-table stale-buffer
2465
+ caption-loss bug (Verified Mechanism 2) regressed. The 2nd
2466
+ table's sentinel (TBLCAPSECONDSENTINEL) is the specific
2467
+ stale-buffer proof: a single-table fixture cannot expose it.
2468
+ """
2469
+ full_text = captioned_table_render_gate_artifacts.full_text
2470
+
2471
+ for sentinel in (
2472
+ TBLCAP_FIRST_SENTINEL,
2473
+ TBLCAP_SECOND_SENTINEL,
2474
+ TBLCAP_WIDTH_SENTINEL,
2475
+ TBLCAP_CSV_SENTINEL,
2476
+ TBLCAP_LIST_SENTINEL,
2477
+ ):
2478
+ assert full_text.count(sentinel) == 1, (
2479
+ f"Expected caption sentinel '{sentinel}' exactly once in "
2480
+ "extracted PDF text -- possible stray-heading or "
2481
+ "stale-buffer caption-loss regression (TBL-01)"
2482
+ )
2483
+
2484
+ def test_typ_source_uses_figure_kind_table_with_no_heading_above(
2485
+ self, captioned_table_render_gate_artifacts
2486
+ ):
2487
+ """
2488
+ The emitted index.typ wraps every captioned table in
2489
+ figure(..., kind: table) -- native Typst "Table N" numbering --
2490
+ and no heading() call appears anywhere in the captioned-table
2491
+ region (from the first figure( call onward): this fixture's only
2492
+ heading() is the document's own top-level title, which precedes
2493
+ every table (SC#1).
2494
+ """
2495
+ typ_source = captioned_table_render_gate_artifacts.typ_source
2496
+
2497
+ assert "figure(" in typ_source, (
2498
+ "Expected 'figure(' in generated Typst source -- depart_table "
2499
+ "figure-wrap regression"
2500
+ )
2501
+ assert "kind: table" in typ_source, (
2502
+ "Expected 'kind: table' in generated Typst source -- "
2503
+ "depart_table figure-wrap regression"
2504
+ )
2505
+
2506
+ first_figure_index = typ_source.index("figure(")
2507
+ captioned_table_region = typ_source[first_figure_index:]
2508
+ assert "heading(" not in captioned_table_region, (
2509
+ "Found a stray 'heading(' call inside the captioned-table "
2510
+ "region -- a table caption fell through to the generic "
2511
+ "section-heading path instead of being figure-wrapped "
2512
+ "(Verified Mechanism 1 regression)"
2513
+ )
2514
+
2515
+ def test_xref_numref_ref_resolve_no_empty_link(
2516
+ self, captioned_table_render_gate_artifacts
2517
+ ):
2518
+ """
2519
+ The :numref:/:ref: cross-references to the first captioned table
2520
+ both resolve to a real link(<label>, ...) anchor reference in the
2521
+ generated Typst source, with no link("", ...) leak -- mirrors
2522
+ TestXrefRefidRenderGate's D-04 contract, applied to a captioned
2523
+ table's own <label> instead of a section/glossary anchor.
2524
+ """
2525
+ typ_source = captioned_table_render_gate_artifacts.typ_source
2526
+
2527
+ assert "link(<" in typ_source, (
2528
+ "Expected at least one refid link(<...>, ...) anchor "
2529
+ "reference in the generated Typst source -- the "
2530
+ ":numref:/:ref: to a captioned table may have regressed"
2531
+ )
2532
+ assert 'link("",' not in typ_source, (
2533
+ 'Found link("", ...) in generated Typst source -- the '
2534
+ 'never-link("", ...) contract has regressed'
2535
+ )
2536
+
2537
+ def test_no_literal_source_leak(self, captioned_table_render_gate_artifacts):
2538
+ """No LEAK_SIGNATURES token reaches the extracted PDF text."""
2539
+ full_text = captioned_table_render_gate_artifacts.full_text
2540
+
2541
+ for leaked_token in LEAK_SIGNATURES:
2542
+ assert leaked_token not in full_text, (
2543
+ f"Literal Typst source '{leaked_token}' leaked into "
2544
+ "rendered PDF text -- captioned-table markup/code-mode "
2545
+ "regression"
2546
+ )
2547
+
2548
+
2549
+ @pytest.mark.slow
2550
+ @pytest.mark.skipif(
2551
+ not TYPST_AVAILABLE,
2552
+ reason="typst-py is required for the GATE-01 pre-fix-basis failure proof",
2553
+ )
2554
+ class TestCaptionedTablePreFixBasisFailureProof:
2555
+ """
2556
+ Standing pre-fix-basis failure proof (D-06): reconstructs the two
2557
+ pre-fix defect shapes from first principles -- the buggy code no
2558
+ longer exists post Plan 25-01 -- and proves each one is genuinely
2559
+ fail-pre-fix, mirroring the
2560
+ ``tests/test_package_only_config_gate.py::TestPreFixBasisFailureProof``
2561
+ convention (reconstruct the pre-fix compile basis; never assert on the
2562
+ TEXT of a compiler error message).
2563
+
2564
+ (a) Double-anchor basis (Critical Pitfall 3, 25-RESEARCH.md): a
2565
+ minimal Typst source in which one label id is defined TWICE -- once
2566
+ as a ``[#metadata(none) <dup>]`` anchor (the unconditional
2567
+ ``_emit_id_anchors`` call ``visit_table`` used to make even for a
2568
+ captioned table) and once as a ``figure(...) <dup>`` postfix (the
2569
+ NEW figure-wrap self-anchor this phase adds) -- plus a
2570
+ ``link(<dup>, ...)`` reference to that label, reproducing the actual
2571
+ :numref:/:ref: xref shape that resolves it. Live-verified while
2572
+ writing this module: Typst does NOT raise merely from defining a
2573
+ label twice with no reference to it; the "label ... occurs multiple
2574
+ times" fatal fires only when something (here, the xref's own
2575
+ ``link(<dup>, ...)``) tries to RESOLVE the ambiguous label -- exactly
2576
+ the real-world shape a :numref:/:ref: to a captioned table produces.
2577
+
2578
+ (b) Stale-buffer basis (Verified Mechanism 2, 25-RESEARCH.md): a
2579
+ reconstructed pre-fix two-table Typst source -- a plain ``table(...)``
2580
+ for each, a stray ``heading(...)`` above the first (the OLD
2581
+ stray-heading caption bug), and the SECOND caption sentinel
2582
+ deliberately ABSENT (the observed pre-fix stale-buffer shape, where a
2583
+ 2nd table's caption silently vanished into a leftover buffer from the
2584
+ first). This half is a pure string reconstruction (no compile needed)
2585
+ proving the positive gate's ``full_text.count(sentinel) == 1``
2586
+ assertion is genuinely fail-pre-fix: applied to this reconstruction,
2587
+ the second sentinel's count is 0, not 1.
2588
+
2589
+ Requirements: TBL-01, TBL-02, D-06.
2590
+ """
2591
+
2592
+ def test_double_anchor_basis_raises(self, tmp_path):
2593
+ """
2594
+ BUG basis (Critical Pitfall 3): reconstruct a captioned table
2595
+ whose id is anchored TWICE -- once via the old unconditional
2596
+ ``_emit_id_anchors`` metadata anchor, once via the new figure
2597
+ ``<label>`` postfix -- with a ``link(<dup>, ...)`` xref
2598
+ resolving it, and assert a real ``typst.compile()`` RAISES. No
2599
+ assertion matches the exception's message text (D-06) -- only
2600
+ that it raises.
2601
+ """
2602
+ reconstructed = (
2603
+ "#metadata(none) <dup>\n"
2604
+ "\n"
2605
+ "#figure(\n"
2606
+ " table(\n"
2607
+ " columns: (1fr, 1fr),\n"
2608
+ " [A], [B],\n"
2609
+ " ),\n"
2610
+ " caption: [Caption],\n"
2611
+ " kind: table,\n"
2612
+ ") <dup>\n"
2613
+ "\n"
2614
+ "See #link(<dup>, [Ref]).\n"
2615
+ )
2616
+ target = tmp_path / "double_anchor_basis.typ"
2617
+ target.write_text(reconstructed, encoding="utf-8")
2618
+
2619
+ with pytest.raises(Exception):
2620
+ typst.compile(str(target), root=str(tmp_path))
2621
+
2622
+ def test_stale_buffer_basis_second_sentinel_count_is_zero(self):
2623
+ """
2624
+ Stale-buffer basis (Verified Mechanism 2): a reconstructed pre-fix
2625
+ two-table Typst source in which the SECOND table's caption
2626
+ sentinel is deliberately absent (the observed pre-fix shape --
2627
+ the second table's caption silently vanished into a stale
2628
+ ``table_cell_content`` buffer left over from the first table).
2629
+ Asserts the positive gate's ``count(...) == 1`` check FAILS
2630
+ (count is 0) against this reconstruction -- proof the gate is
2631
+ genuinely fail-pre-fix, not vacuously green.
2632
+ """
2633
+ reconstructed_pre_fix_shape = (
2634
+ "heading(level: 1, {text(" + repr(TBLCAP_FIRST_SENTINEL) + ")})\n"
2635
+ "\n"
2636
+ "table(\n"
2637
+ " columns: (1fr, 1fr),\n"
2638
+ " [A], [B],\n"
2639
+ ")\n"
2640
+ "\n"
2641
+ "table(\n"
2642
+ " columns: (1fr, 1fr),\n"
2643
+ " [C], [D],\n"
2644
+ ")\n"
2645
+ )
2646
+
2647
+ # The pre-fix stray-heading bug: the FIRST table's caption leaked
2648
+ # into a heading() call (Verified Mechanism 1) instead of a
2649
+ # figure caption -- present here to keep the reconstruction
2650
+ # faithful to the actually-observed pre-fix output shape.
2651
+ assert TBLCAP_FIRST_SENTINEL in reconstructed_pre_fix_shape
2652
+
2653
+ # The proof this test exists for: the SECOND table's caption
2654
+ # sentinel never reached the reconstructed output at all -- the
2655
+ # stale-buffer bug swallowed it. count() is 0, not 1, so the
2656
+ # positive gate's exact-once assertion would have failed loudly
2657
+ # against this pre-fix basis.
2658
+ assert reconstructed_pre_fix_shape.count(TBLCAP_SECOND_SENTINEL) == 0, (
2659
+ "Expected the reconstructed pre-fix stale-buffer basis to have "
2660
+ "ZERO occurrences of the second table's caption sentinel -- "
2661
+ "if this fails, the reconstruction no longer reproduces the "
2662
+ "pre-fix bug shape and the fail-pre-fix proof is void"
2663
+ )