flexdoc 0.3.0__tar.gz → 0.4.0__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.
- flexdoc-0.4.0/.github/release-request/notes.md +32 -0
- flexdoc-0.4.0/.github/release-request/request.json +3 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.tbd/config.yml +2 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/CHANGELOG.md +54 -1
- flexdoc-0.4.0/PKG-INFO +282 -0
- flexdoc-0.4.0/README.md +244 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/TODO.md +8 -6
- {flexdoc-0.3.0 → flexdoc-0.4.0}/docs/flexdoc-spec.md +490 -130
- flexdoc-0.4.0/docs/project/research/research-2026-07-10-text-reference-microformat.md +3546 -0
- flexdoc-0.4.0/docs/project/review/senior-engineering-review-v0.4-release-2026-07.md +253 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/docs/project/specs/active/plan-2026-07-09-flexdoc-stabilization-roadmap.md +35 -23
- {flexdoc-0.3.0/docs/project/specs/active → flexdoc-0.4.0/docs/project/specs/done}/plan-2026-06-11-structural-metadata.md +2 -2
- {flexdoc-0.3.0/docs/project/specs/active → flexdoc-0.4.0/docs/project/specs/done}/plan-2026-06-13-metrics-use-case.md +1 -1
- {flexdoc-0.3.0/docs/project/specs/active → flexdoc-0.4.0/docs/project/specs/done}/plan-2026-07-08-post-review-refinements.md +1 -1
- flexdoc-0.4.0/docs/project/specs/done/plan-2026-07-13-logical-word-metrics.md +260 -0
- flexdoc-0.4.0/docs/project/specs/done/plan-2026-07-14-native-textref-integration.md +344 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/docs/publishing.md +11 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/docs/usage.md +82 -6
- {flexdoc-0.3.0 → flexdoc-0.4.0}/examples/doc_structure.py +4 -3
- flexdoc-0.4.0/examples/textref_workflows.py +89 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/pyproject.toml +1 -1
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/__init__.py +17 -1
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/__init__.py +60 -1
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/debug.py +17 -8
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/doc_graph.py +121 -51
- flexdoc-0.4.0/src/flexdoc/docs/doc_graph_schema.json +603 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/flex_doc.py +40 -11
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/paragraphs.py +7 -3
- flexdoc-0.4.0/src/flexdoc/docs/serialization.py +26 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/sizes.py +17 -2
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/span_ref.py +122 -40
- flexdoc-0.4.0/src/flexdoc/docs/text_annotations.py +157 -0
- flexdoc-0.4.0/src/flexdoc/docs/text_ref.py +855 -0
- flexdoc-0.4.0/src/flexdoc/docs/text_ref_context.py +730 -0
- flexdoc-0.4.0/src/flexdoc/docs/text_ref_schema.json +315 -0
- flexdoc-0.4.0/src/flexdoc/util/__init__.py +23 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/util/read_time.py +5 -7
- flexdoc-0.4.0/src/flexdoc/util/token_estimate.py +25 -0
- flexdoc-0.4.0/src/flexdoc/util/word_count.py +74 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_block_types.py +3 -3
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_doc_graph.py +124 -35
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_flex_doc.py +2 -3
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_frontmatter.py +1 -1
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_sections.py +10 -10
- flexdoc-0.4.0/tests/docs/test_text_annotations.py +245 -0
- flexdoc-0.4.0/tests/docs/test_text_ref.py +399 -0
- flexdoc-0.4.0/tests/docs/test_text_ref_context.py +242 -0
- flexdoc-0.4.0/tests/docs/test_text_ref_render.py +136 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/footnotes_refs/docgraph.yaml +3 -2
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/footnotes_refs/report.yaml +4 -6
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/heading_edges/docgraph.yaml +3 -2
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/heading_edges/report.yaml +7 -12
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/inline_html/docgraph.yaml +3 -2
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/inline_html/report.yaml +1 -1
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/inline_pathology/docgraph.yaml +3 -2
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/inline_pathology/report.yaml +6 -11
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/kitchen_sink/docgraph.yaml +3 -2
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/kitchen_sink/report.yaml +3 -5
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/link_taxonomy/docgraph.yaml +3 -2
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/link_taxonomy/report.yaml +8 -14
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/malformed/docgraph.yaml +3 -2
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/malformed/report.yaml +1 -1
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/nested_list/docgraph.yaml +3 -2
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/nested_list/report.yaml +1 -1
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/preamble_only/docgraph.yaml +3 -2
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/preamble_only/report.yaml +1 -1
- flexdoc-0.4.0/tests/golden/expected/textref-annotations.md +59 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/tight_vs_loose/docgraph.yaml +3 -2
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/tight_vs_loose/report.yaml +1 -1
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/unicode/docgraph.yaml +3 -2
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/unicode/report.yaml +4 -6
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/test_golden_docs.py +7 -6
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/test_examples.py +1 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/test_root_api.py +42 -0
- flexdoc-0.4.0/tests/test_word_count.py +117 -0
- flexdoc-0.3.0/.github/release-request/notes.md +0 -49
- flexdoc-0.3.0/.github/release-request/request.json +0 -3
- flexdoc-0.3.0/PKG-INFO +0 -170
- flexdoc-0.3.0/README.md +0 -132
- flexdoc-0.3.0/docs/project/research/research-2026-07-10-text-reference-microformat.md +0 -845
- flexdoc-0.3.0/src/flexdoc/docs/doc_graph_schema.json +0 -219
- flexdoc-0.3.0/src/flexdoc/util/__init__.py +0 -11
- flexdoc-0.3.0/src/flexdoc/util/token_estimate.py +0 -34
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.agents/skills/simple-modern-uv/SKILL.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.agents/skills/simple-modern-uv/references/adopt-existing.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.agents/skills/simple-modern-uv/references/customize.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.agents/skills/simple-modern-uv/references/faq.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.agents/skills/tbd/SKILL.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.claude/.gitignore +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.claude/hooks/tbd-closing-reminder.sh +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.claude/scripts/ensure-gh-cli.sh +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.claude/scripts/tbd-session.sh +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.claude/settings.json +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.claude/skills/tbd/SKILL.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.codex/ensure-gh-cli.sh +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.codex/hooks.json +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.codex/tbd-closing-reminder.sh +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.codex/tbd-session.sh +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.copier-answers.yml +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.github/workflows/ci.yml +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.github/workflows/publish.yml +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.github/workflows/release.yml +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.gitignore +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.tbd/.gitattributes +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/.tbd/.gitignore +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/AGENTS.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/CLAUDE.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/LICENSE +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/Makefile +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/SUPPLY-CHAIN-SECURITY.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/devtools/lint.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/docs/development.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/docs/installation.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/docs/project/drafts/draft-2026-07-flexdoc-intro-post.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/docs/project/research/research-2026-05-29-document-model.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/docs/project/research/research-2026-05-30-multilayer-parsing.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/docs/project/research/research-2026-05-30-span-references.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/docs/project/review/senior-engineering-review-flexdoc-2026-07.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/docs/project/review/senior-engineering-review-flexdoc-standalone-2026-06.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/docs/project/specs/active/plan-2026-05-29-unified-document-model.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/docs/project/specs/active/plan-2026-06-11-flexdoc-extraction.md +0 -0
- {flexdoc-0.3.0/docs/project/specs/active → flexdoc-0.4.0/docs/project/specs/done}/plan-2026-05-31-doc-model-refinements.md +0 -0
- {flexdoc-0.3.0/docs/project/specs/active → flexdoc-0.4.0/docs/project/specs/done}/plan-2026-05-31-golden-doc-testing.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/examples/backfill_timestamps.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/examples/normalized_form.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/skills-lock.json +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/base_blocks.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/block_info.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/block_tree.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/block_types.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/collect.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/frontmatter.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/interval_index.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/links.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/node.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/node_table.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/render.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/search_tokens.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/sections.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/token_diffs.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/token_mapping.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/docs/wordtoks.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/html/__init__.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/html/extractor.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/html/html_in_md.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/html/html_plaintext.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/html/html_tags.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/html/timestamps.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/src/flexdoc/py.typed +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/__init__.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/__init__.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_base_blocks.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_block_info.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_blocks.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_caching_threadsafe.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_collect.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_footnote_ref.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_interval_index.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_line_endings.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_links.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_node.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_node_table.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_offsets.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_span_ref.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_spans.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_subdoc_and_empty.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_token_diffs.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_token_mapping.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_token_validation.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/docs/test_wordtoks.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/README.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/documents/footnotes_refs.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/documents/heading_edges.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/documents/inline_html.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/documents/inline_pathology.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/documents/kitchen_sink.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/documents/link_taxonomy.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/documents/malformed.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/documents/nested_list.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/documents/preamble_only.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/documents/tight_vs_loose.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/documents/unicode.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/footnotes_refs/reassembled.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/heading_edges/reassembled.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/inline_html/reassembled.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/inline_pathology/reassembled.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/kitchen_sink/reassembled.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/link_taxonomy/reassembled.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/malformed/reassembled.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/nested_list/reassembled.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/preamble_only/reassembled.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/tight_vs_loose/reassembled.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/golden/expected/unicode/reassembled.md +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/html/__init__.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/html/test_html_tag_hardening.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/html/test_html_tags.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/html/test_html_validation_and_classes.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/html/test_timestamps.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/tests/test_supply_chain.py +0 -0
- {flexdoc-0.3.0 → flexdoc-0.4.0}/uv.lock +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
## What's Changed
|
|
2
|
+
|
|
3
|
+
This release adds the portable reference and annotation layer from the 2026-07 TextRef design cycle plus cross-language logical word metrics, finalized under an independent senior release review ([review and breaking-change catalog](https://github.com/jlevy/flexdoc/blob/main/docs/project/review/senior-engineering-review-v0.4-release-2026-07.md); PRs #18, #20, #21). Because it changes documented behavior on a pre-1.0 library, it bumps the minor version (per the pre-1.0 rule in `docs/publishing.md`). If you depend on an unbounded `flexdoc>=0.3.0`, review the breaking changes below or pin `<0.4` before upgrading.
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
**Native TextRef integration**
|
|
8
|
+
|
|
9
|
+
Strict `DocRef`/`TextRef` values identify whole documents, spans, points, and semantic sections with canonical JSON and reversible `textref:0.1` URIs. `FlexDoc.references()` binds a document locator to one source snapshot: it maps every locatable public value (`for_target`, `for_span`, `for_point`, `for_section`), resolves references with typed outcomes on independent document/source-validation/selector axes (resolution never silently picks a duplicate), retrieves structured line-window context, and renders deterministic annotation views for humans and LLMs. Span quote evidence is configurable per context or per span; compact exact-less spans stay bound to one source hash.
|
|
10
|
+
|
|
11
|
+
**Consumer-owned annotations**
|
|
12
|
+
|
|
13
|
+
`TextAnnotation` and the one-document `AnnotationSet` sidecar round-trip strict JSON and safe YAML, and every sidecar entry is validated through the complete TextRef evidence contract at parse time. `DocGraph/v0.2` embeds a matching sidecar after verifying its document and source hash against the snapshot, and bounds-checks embedded positions. Committed JSON Schemas ship for both formats in the wheel.
|
|
14
|
+
|
|
15
|
+
**Cross-language logical word metrics**
|
|
16
|
+
|
|
17
|
+
`TextUnit.words` now measures normalized word-equivalent volume across natural language, CJK text, source code, URLs, and punctuation-dense content. `TextUnit.raw_words` and `raw_word_count()` keep literal whitespace counting, and `logical_word_count()` exposes the dependency-free primitive. See the [logical-word definition and validation](https://gist.github.com/jlevy/0d6d87885f6d85f31440e58b8cfce663).
|
|
18
|
+
|
|
19
|
+
### Breaking Changes
|
|
20
|
+
|
|
21
|
+
Made cleanly with no compatibility aliases, given the pre-1.0 status. Full catalog with rationales and migrations: [review doc §4](https://github.com/jlevy/flexdoc/blob/main/docs/project/review/senior-engineering-review-v0.4-release-2026-07.md).
|
|
22
|
+
|
|
23
|
+
- **`FlexDoc.graph()` requires a document locator** — `doc.graph()` becomes `doc.graph(document="path/or/id.md")`; `build_doc_graph()` and the debug helpers `doc_graph_yaml()`/`dump_views()` take the same argument. Every serialized graph is now self-identifying.
|
|
24
|
+
- **DocGraph wire contract v0.1 → v0.2** — `schema` is `"DocGraph/v0.2"`; `source.document` is required; the unqualified `source.sha256` field is replaced by algorithm-qualified `source.source_hash` (shared with TextRef); models are strict (unknown fields rejected, frozen, strict types) and validate node references, span/text consistency, and annotation bounds.
|
|
25
|
+
- **`TextUnit.words` is logical** — it matches raw counts for ordinary non-wide prose but differs for wide/fullwidth scripts, URLs, code, and symbolic content; use `TextUnit.raw_words` for the previous whitespace-split behavior. Ripples through `size()`, `size_summary()`, `section_size_tree()`, and debug reports.
|
|
26
|
+
- **Token estimates scale logical words** — `estimate_tokens(text, tokens_per_logical_word=1.6)` replaces the `chars_per_token` parameter, and `TOKENS_PER_LOGICAL_WORD` replaces `CHARS_PER_TOKEN`; estimates change numerically even at defaults.
|
|
27
|
+
- **`format_read_time()` takes logical word counts** — same signature; the default rate corresponds to roughly 450 CJK characters per minute under the default wide-character weight.
|
|
28
|
+
- **Pinned outputs change** — regenerate any golden DocGraph/report snapshots (schema string, `source` block, and `words` fields).
|
|
29
|
+
|
|
30
|
+
### Full Changelog
|
|
31
|
+
|
|
32
|
+
https://github.com/jlevy/flexdoc/compare/v0.3.0...v0.4.0
|
|
@@ -32,6 +32,7 @@ docs_cache:
|
|
|
32
32
|
shortcuts/system/skill-baseline.md: internal:shortcuts/system/skill-baseline.md
|
|
33
33
|
shortcuts/system/skill-brief.md: internal:shortcuts/system/skill-brief.md
|
|
34
34
|
shortcuts/system/skill-minimal.md: internal:shortcuts/system/skill-minimal.md
|
|
35
|
+
shortcuts/standard/address-pr-review.md: internal:shortcuts/standard/address-pr-review.md
|
|
35
36
|
shortcuts/standard/agent-handoff.md: internal:shortcuts/standard/agent-handoff.md
|
|
36
37
|
shortcuts/standard/checkout-third-party-repo.md: internal:shortcuts/standard/checkout-third-party-repo.md
|
|
37
38
|
shortcuts/standard/code-cleanup-all.md: internal:shortcuts/standard/code-cleanup-all.md
|
|
@@ -51,6 +52,7 @@ docs_cache:
|
|
|
51
52
|
shortcuts/standard/new-shortcut.md: internal:shortcuts/standard/new-shortcut.md
|
|
52
53
|
shortcuts/standard/new-validation-plan.md: internal:shortcuts/standard/new-validation-plan.md
|
|
53
54
|
shortcuts/standard/plan-implementation-with-beads.md: internal:shortcuts/standard/plan-implementation-with-beads.md
|
|
55
|
+
shortcuts/standard/pr-review-workflows.md: internal:shortcuts/standard/pr-review-workflows.md
|
|
54
56
|
shortcuts/standard/precommit-process.md: internal:shortcuts/standard/precommit-process.md
|
|
55
57
|
shortcuts/standard/review-code-python.md: internal:shortcuts/standard/review-code-python.md
|
|
56
58
|
shortcuts/standard/review-code-typescript.md: internal:shortcuts/standard/review-code-typescript.md
|
|
@@ -4,6 +4,59 @@ All notable changes to flexdoc are documented here.
|
|
|
4
4
|
This project uses [semantic versioning](https://semver.org/); while pre-1.0, breaking
|
|
5
5
|
changes bump the **minor** version (see `docs/publishing.md`).
|
|
6
6
|
|
|
7
|
+
## 0.4.0 (2026-07-20)
|
|
8
|
+
|
|
9
|
+
This is an intentional pre-1.0 API break; per the pre-1.0 rule it is a minor release,
|
|
10
|
+
not a 0.3.x patch. The release-gate review, validation evidence, and full
|
|
11
|
+
breaking-change catalog with migrations are in
|
|
12
|
+
`docs/project/review/senior-engineering-review-v0.4-release-2026-07.md`.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Native TextRef integration.** Strict `DocRef`/`TextRef` values now identify whole
|
|
17
|
+
documents, spans, points, and semantic sections with canonical JSON and reversible
|
|
18
|
+
`textref:0.1` URIs. `FlexDoc.references()` maps every locatable public document value,
|
|
19
|
+
resolves references with typed outcomes, retrieves structured source context, and
|
|
20
|
+
renders deterministic human/LLM-readable annotation views. Span quote evidence is
|
|
21
|
+
optional: callers can select a context-wide size policy or override individual spans,
|
|
22
|
+
while compact exact-less spans remain safely bound to one source hash.
|
|
23
|
+
- **Consumer-owned TextRef annotations.** `TextAnnotation` and the one-document
|
|
24
|
+
`AnnotationSet` sidecar round-trip strict JSON and safe YAML. `DocGraph/v0.2` carries
|
|
25
|
+
required document identity and source hash in every graph, with optional typed
|
|
26
|
+
annotations in the same schema. The protocol and renderer add no dependencies.
|
|
27
|
+
- **Cross-language logical word metrics.** `TextUnit.words` now measures normalized
|
|
28
|
+
word-equivalent volume across natural language, CJK text, source code, URLs, and other
|
|
29
|
+
punctuation-dense content. `TextUnit.raw_words` and `raw_word_count()` preserve
|
|
30
|
+
literal whitespace-delimited counting, while `logical_word_count()` exposes the
|
|
31
|
+
dependency-free normalized primitive.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **DocGraph is v0.2 and self-identifying.** `FlexDoc.graph()` and `build_doc_graph()`
|
|
36
|
+
now require a consumer-owned `document` locator and always return `DocGraph/v0.2`;
|
|
37
|
+
annotations optionally populate that same model, and the debug helpers
|
|
38
|
+
`doc_graph_yaml()`/`dump_views()` take the same `document` argument. In the
|
|
39
|
+
serialized graph, `source.document` is required and the unqualified `source.sha256`
|
|
40
|
+
field is replaced by `source.source_hash`, whose algorithm-qualified value is shared
|
|
41
|
+
with TextRef. All DocGraph models are now strict (unknown fields rejected, strict
|
|
42
|
+
types, frozen instances) and validate node-reference integrity, span/text
|
|
43
|
+
consistency, and annotation bounds.
|
|
44
|
+
|
|
45
|
+
- **`TextUnit.words` changes from raw to logical semantics.** It matches the raw count
|
|
46
|
+
for ordinary non-wide prose averaging 3–6 characters per word, but differs for
|
|
47
|
+
wide/fullwidth scripts, longer or shorter average word lengths, URLs, code, and other
|
|
48
|
+
symbolic content. Callers requiring the exact previous whitespace-split behavior
|
|
49
|
+
must use `TextUnit.raw_words`; there is no separate `TextUnit.logical_words` member.
|
|
50
|
+
Size summaries, section-tree defaults, and debug-report `words` fields use the logical
|
|
51
|
+
measure, and aggregate counts are rounded only after the full text is measured.
|
|
52
|
+
- **Approximate token estimates now scale logical words.** `estimate_tokens()` uses
|
|
53
|
+
`TOKENS_PER_LOGICAL_WORD` (default `1.6`) instead of the former
|
|
54
|
+
`CHARS_PER_TOKEN`/`chars_per_token` API. This is still a model-family heuristic; use
|
|
55
|
+
the target provider's tokenizer for exact counts or hard context limits.
|
|
56
|
+
- **Reading-time guidance is language-robust.** Pass logical word counts to
|
|
57
|
+
`format_read_time()`; the default rate corresponds to roughly 450 CJK characters per
|
|
58
|
+
minute under the default wide-character weight.
|
|
59
|
+
|
|
7
60
|
## 0.3.0 (2026-07-11)
|
|
8
61
|
|
|
9
62
|
Fixes from the 2026-07 pre-promotion design review
|
|
@@ -102,7 +155,7 @@ Remaining pre-1.0 design decisions and future mechanisms are collected in
|
|
|
102
155
|
## 0.2.0 (2026-06-14)
|
|
103
156
|
|
|
104
157
|
Correctness fixes and a completed inline/heading/link surface for the document-metrics
|
|
105
|
-
use case (`docs/project/specs/
|
|
158
|
+
use case (`docs/project/specs/done/plan-2026-06-13-metrics-use-case.md`, issue #6). As
|
|
106
159
|
a preview-stage library this takes the cleanest shape with no compatibility shims; the
|
|
107
160
|
API additions below include breaking signature changes (see **Changed**).
|
|
108
161
|
|
flexdoc-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flexdoc
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Document model for powerful text parsing and processing in Markdown
|
|
5
|
+
Project-URL: Repository, https://github.com/jlevy/flexdoc
|
|
6
|
+
Project-URL: Homepage, https://github.com/jlevy/flexdoc
|
|
7
|
+
Project-URL: Documentation, https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md
|
|
8
|
+
Project-URL: Changelog, https://github.com/jlevy/flexdoc/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Joshua Levy <joshua@cal.berkeley.edu>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: annotations,document-model,llm,markdown,nlp,parsing,sentences,spans,text-processing
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: <4.0,>=3.11
|
|
26
|
+
Requires-Dist: cydifflib>=1.2.0
|
|
27
|
+
Requires-Dist: flowmark>=0.7.1
|
|
28
|
+
Requires-Dist: frontmatter-format>=0.3.0
|
|
29
|
+
Requires-Dist: funlog>=0.2.1
|
|
30
|
+
Requires-Dist: marko>=2.2.2
|
|
31
|
+
Requires-Dist: prettyfmt>=0.3.0
|
|
32
|
+
Requires-Dist: pydantic>=2.13.4
|
|
33
|
+
Requires-Dist: regex>=2024.11.6
|
|
34
|
+
Requires-Dist: selectolax>=0.3.32
|
|
35
|
+
Requires-Dist: strif>=3.1.0
|
|
36
|
+
Requires-Dist: typing-extensions>=4.12.2
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# FlexDoc
|
|
40
|
+
|
|
41
|
+
[](https://pypi.org/project/flexdoc/)
|
|
42
|
+
[](https://github.com/jlevy/flexdoc/actions/workflows/ci.yml)
|
|
43
|
+
[](https://github.com/jlevy/flexdoc/blob/main/LICENSE)
|
|
44
|
+
[](https://pypi.org/project/flexdoc/)
|
|
45
|
+
|
|
46
|
+
A Markdown parser gives you a block AST but no sentences, sizes, or exact source
|
|
47
|
+
offsets. An NLP toolkit gives you sentences but no Markdown structure.
|
|
48
|
+
If you need to know which sentence is in which section, how many words (or LLM tokens) a
|
|
49
|
+
section holds, or exactly where a link sits in the original text, you end up gluing
|
|
50
|
+
tools together; the glue breaks on the first edit.
|
|
51
|
+
|
|
52
|
+
FlexDoc builds a single source-grounded model and exposes its structure as layers over
|
|
53
|
+
one shared coordinate space: exact `[start, end)` offsets into one retained, normalized
|
|
54
|
+
source string. The **Markdown layer** (blocks, inline elements, typed attributes), the
|
|
55
|
+
**textual layer** (paragraphs, sentences, word tokens), and the **document layer**
|
|
56
|
+
(heading hierarchy, table of contents) are independent parses of the same text, so
|
|
57
|
+
cross-cutting questions are simple offset queries.
|
|
58
|
+
On top of the model sit portable formats: `DocGraph` serializes any slice as
|
|
59
|
+
language-neutral JSON, and `TextRef` makes spans, points, sections, and whole documents
|
|
60
|
+
durable references that resolve back to exact source.
|
|
61
|
+
|
|
62
|
+
FlexDoc is a standalone library.
|
|
63
|
+
[chopdiff](https://github.com/jlevy/chopdiff) builds its diff-filtering and
|
|
64
|
+
windowed-transform layer on top of flexdoc.
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```shell
|
|
69
|
+
uv add flexdoc
|
|
70
|
+
# or: pip install flexdoc
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Status
|
|
74
|
+
|
|
75
|
+
**Beta** (0.4.x). The core model is established, but the later-stage mechanisms in the
|
|
76
|
+
[stabilization roadmap](https://github.com/jlevy/flexdoc/blob/main/docs/project/specs/active/plan-2026-07-09-flexdoc-stabilization-roadmap.md)
|
|
77
|
+
remain open. Breaking changes before 1.0 bump the minor version, so pin a minor version.
|
|
78
|
+
See the [changelog](https://github.com/jlevy/flexdoc/blob/main/CHANGELOG.md).
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
### Parse and Query
|
|
83
|
+
|
|
84
|
+
The primary entry point is `FlexDoc`. Parsing any input never raises: malformed
|
|
85
|
+
Markdown degrades deterministically and visibly. Every located unit carries an exact
|
|
86
|
+
`[start, end)` span into the normalized `source_text`, and `reassemble()` round-trips
|
|
87
|
+
the model back to normalized Markdown.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from flexdoc import FlexDoc, NodeKind, TextUnit
|
|
91
|
+
|
|
92
|
+
doc = FlexDoc.from_text("# Introduction\n\nSee [docs](https://example.com/docs).\n")
|
|
93
|
+
|
|
94
|
+
# Section hierarchy with rolled-up sizes:
|
|
95
|
+
print(doc.section_size_tree(units=(TextUnit.words, TextUnit.sentences)))
|
|
96
|
+
# # Introduction (8 words, 2 sentences)
|
|
97
|
+
|
|
98
|
+
# Sizes at every grain, including approximate LLM tokens:
|
|
99
|
+
print(doc.size_summary())
|
|
100
|
+
# 53 bytes (3 lines, 2 paras, 2 sents, 8 words, ~13 tok)
|
|
101
|
+
|
|
102
|
+
# One query primitive (collect()) spans all layers:
|
|
103
|
+
link = doc.collect(kinds={NodeKind.link})[0]
|
|
104
|
+
print(link.attrs["url"], link.source_span)
|
|
105
|
+
# https://example.com/docs (20, 52)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
FlexDoc delegates Markdown parsing to [marko](https://github.com/frostming/marko)
|
|
109
|
+
(CommonMark with GFM tables and footnotes) via
|
|
110
|
+
[flowmark](https://github.com/jlevy/flowmark), and adds sentence segmentation, the
|
|
111
|
+
section hierarchy, the flat node table, offset-grounded queries, references,
|
|
112
|
+
serialization, and span anchoring on top.
|
|
113
|
+
|
|
114
|
+
### Reference and Annotate: TextRef
|
|
115
|
+
|
|
116
|
+
Continuing the example, any public value maps to a
|
|
117
|
+
[`TextRef`](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md#11-textref-spanref-and-annotations):
|
|
118
|
+
a portable reference carrying quote evidence and a source hash, with canonical JSON and
|
|
119
|
+
a reversible `textref:0.1` URI projection.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
refs = doc.references(document="guide.md")
|
|
123
|
+
ref = refs.for_target(link)
|
|
124
|
+
|
|
125
|
+
print(ref.to_uri())
|
|
126
|
+
# textref:0.1?doc=guide.md&hash=sha256%3A0e91...0f86&type=span
|
|
127
|
+
# &exact=%5Bdocs%5D%28https%3A%2F%2Fexample.com%2Fdocs%29&prefix=...&start=20
|
|
128
|
+
|
|
129
|
+
res = refs.resolve(ref)
|
|
130
|
+
print(res.resolved, res.method.value, (res.span.start, res.span.end))
|
|
131
|
+
# True source_position (20, 52)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Annotations attach consumer-owned content to TextRef targets, and `AnnotationSet` is
|
|
135
|
+
their one-document JSON/YAML sidecar form:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from flexdoc import AnnotationSet, TextAnnotation, TextBody
|
|
139
|
+
|
|
140
|
+
note = TextAnnotation(id="n1", target=ref, motivations=["commenting"],
|
|
141
|
+
body=TextBody(type="text", value="Check this link."))
|
|
142
|
+
sidecar = AnnotationSet.from_annotations([note])
|
|
143
|
+
|
|
144
|
+
print(sidecar.to_yaml())
|
|
145
|
+
# format: text-annotations/0.1
|
|
146
|
+
# document: guide.md
|
|
147
|
+
# source_hash: sha256:0e91...0f86
|
|
148
|
+
# annotations:
|
|
149
|
+
# - id: n1
|
|
150
|
+
# ...
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Serialize: DocGraph
|
|
154
|
+
|
|
155
|
+
[`DocGraph`](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md#10-docgraph-the-serialized-projection)
|
|
156
|
+
is the language-neutral serialized projection: one source-anchored object carrying
|
|
157
|
+
document identity, a source hash shared with TextRef, the node table, derived views,
|
|
158
|
+
and (optionally) an embedded annotation sidecar. JSON is the wire form; YAML is the
|
|
159
|
+
human and golden-test form.
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
graph = doc.graph(document="guide.md", annotations=sidecar)
|
|
163
|
+
|
|
164
|
+
print(graph.to_yaml())
|
|
165
|
+
# schema: DocGraph/v0.2
|
|
166
|
+
# source:
|
|
167
|
+
# format: markdown
|
|
168
|
+
# offset_unit: unicode_code_points
|
|
169
|
+
# document: guide.md
|
|
170
|
+
# source_hash: sha256:0e91...0f86
|
|
171
|
+
# nodes:
|
|
172
|
+
# - id: n0001
|
|
173
|
+
# kind: heading
|
|
174
|
+
# layer: markdown
|
|
175
|
+
# source_span:
|
|
176
|
+
# start: 0
|
|
177
|
+
# end: 14
|
|
178
|
+
# attrs:
|
|
179
|
+
# level: 1
|
|
180
|
+
# ... (more nodes, then views and the embedded annotations)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Word and Token Metrics
|
|
184
|
+
|
|
185
|
+
`TextUnit.words` is a logical-word measure: it matches a whitespace count for ordinary
|
|
186
|
+
non-wide prose averaging 3–6 characters per word, but normalizes wide/fullwidth scripts,
|
|
187
|
+
long identifiers and URLs, short-token sequences, and punctuation-dense code or
|
|
188
|
+
Markdown. Use `TextUnit.raw_words` for a literal whitespace-delimited count. See the
|
|
189
|
+
[logical-word definition and validation](https://gist.github.com/jlevy/0d6d87885f6d85f31440e58b8cfce663)
|
|
190
|
+
for the rationale, reference algorithm, and multilingual examples.
|
|
191
|
+
|
|
192
|
+
### Module Map
|
|
193
|
+
|
|
194
|
+
The full public surfaces live in the submodules:
|
|
195
|
+
|
|
196
|
+
- `flexdoc.docs`: `FlexDoc`, `Paragraph`, `Sentence`, `Section`, `Block`, `BlockType`,
|
|
197
|
+
the node table, `collect()`, `DocGraph`, `TextRef`, annotation values, `SpanRef`, and
|
|
198
|
+
source-linked render/report helpers.
|
|
199
|
+
- `flexdoc.docs.wordtoks`, `flexdoc.docs.search_tokens`,
|
|
200
|
+
`flexdoc.docs.token_diffs`, and `flexdoc.docs.token_mapping`: lower-level token,
|
|
201
|
+
search, diff, and mapping utilities that are not promoted by `flexdoc.docs`.
|
|
202
|
+
- `flexdoc.html`: html-in-md, html/plaintext conversion, HTML tag helpers, the content
|
|
203
|
+
extractor, and timestamp extraction.
|
|
204
|
+
- `flexdoc.util`: raw and cross-language logical word counts, read-time estimation, and
|
|
205
|
+
approximate token-count estimation.
|
|
206
|
+
|
|
207
|
+
### Worked Examples
|
|
208
|
+
|
|
209
|
+
See [usage.md](https://github.com/jlevy/flexdoc/blob/main/docs/usage.md) for the main
|
|
210
|
+
workflows. Each worked example is a runnable script (run with
|
|
211
|
+
`uv run python examples/<name>.py` from a checkout) demonstrating one workflow
|
|
212
|
+
end to end:
|
|
213
|
+
|
|
214
|
+
- [`doc_structure.py`](https://github.com/jlevy/flexdoc/blob/main/examples/doc_structure.py)
|
|
215
|
+
covers section hierarchy, size rollups, offset lookups, and the block tree.
|
|
216
|
+
- [`normalized_form.py`](https://github.com/jlevy/flexdoc/blob/main/examples/normalized_form.py)
|
|
217
|
+
covers block-type tallies, list-density invariance, and per-section links.
|
|
218
|
+
- [`backfill_timestamps.py`](https://github.com/jlevy/flexdoc/blob/main/examples/backfill_timestamps.py)
|
|
219
|
+
aligns an edited transcript to its timestamped source via token mapping.
|
|
220
|
+
- [`textref_workflows.py`](https://github.com/jlevy/flexdoc/blob/main/examples/textref_workflows.py)
|
|
221
|
+
composes extraction provenance, context retrieval, citations, annotations, and edit
|
|
222
|
+
targets.
|
|
223
|
+
|
|
224
|
+
## Design
|
|
225
|
+
|
|
226
|
+
The design of record is
|
|
227
|
+
[flexdoc-spec.md](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md),
|
|
228
|
+
which motivates each decision and defines the invariants the tests pin. The ideas that
|
|
229
|
+
carry the model:
|
|
230
|
+
|
|
231
|
+
- **One coordinate space.** Every layer indexes the same retained, normalized source
|
|
232
|
+
string with `[start, end)` Unicode code-point offsets, so cross-layer questions reduce
|
|
233
|
+
to offset queries
|
|
234
|
+
([spec §2](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md#2-principles-and-goals)).
|
|
235
|
+
- **Layers are independent parses.** Markdown structure, prose sentences, and the
|
|
236
|
+
heading hierarchy are separate projections of the same text; no single tree has to be
|
|
237
|
+
authoritative
|
|
238
|
+
([spec §4](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md#4-core-types-nodes-and-offsets)).
|
|
239
|
+
- **Parsing never raises.** Malformed Markdown degrades deterministically and visibly,
|
|
240
|
+
pinned by the golden-test corpus
|
|
241
|
+
([spec §13](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md#13-invariants-and-non-goals)).
|
|
242
|
+
- **References carry evidence, and resolution never guesses.** Positions are trusted
|
|
243
|
+
only under a matched source hash; quotes and context corroborate; a duplicated quote
|
|
244
|
+
resolves to a typed ambiguous outcome, never a silent first match
|
|
245
|
+
([spec §11](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md#11-textref-spanref-and-annotations)).
|
|
246
|
+
`SpanRef` is the lightweight quote anchor underneath.
|
|
247
|
+
- **Serialized contracts are strict and versioned.** `DocGraph/v0.2` and `textref/0.1`
|
|
248
|
+
reject unknown fields, and both ship committed JSON Schemas:
|
|
249
|
+
[doc_graph_schema.json](https://github.com/jlevy/flexdoc/blob/main/src/flexdoc/docs/doc_graph_schema.json)
|
|
250
|
+
and
|
|
251
|
+
[text_ref_schema.json](https://github.com/jlevy/flexdoc/blob/main/src/flexdoc/docs/text_ref_schema.json)
|
|
252
|
+
([spec §10](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md#10-docgraph-the-serialized-projection)).
|
|
253
|
+
|
|
254
|
+
## Project Docs
|
|
255
|
+
|
|
256
|
+
For users:
|
|
257
|
+
|
|
258
|
+
- Design of record (the full spec):
|
|
259
|
+
[flexdoc-spec.md](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md).
|
|
260
|
+
- Usage guide: [usage.md](https://github.com/jlevy/flexdoc/blob/main/docs/usage.md).
|
|
261
|
+
- Installing uv and Python:
|
|
262
|
+
[installation.md](https://github.com/jlevy/flexdoc/blob/main/docs/installation.md).
|
|
263
|
+
|
|
264
|
+
For contributors:
|
|
265
|
+
|
|
266
|
+
- Development workflows:
|
|
267
|
+
[development.md](https://github.com/jlevy/flexdoc/blob/main/docs/development.md).
|
|
268
|
+
- Publishing to PyPI:
|
|
269
|
+
[publishing.md](https://github.com/jlevy/flexdoc/blob/main/docs/publishing.md).
|
|
270
|
+
- Dependency policy:
|
|
271
|
+
[SUPPLY-CHAIN-SECURITY.md](https://github.com/jlevy/flexdoc/blob/main/SUPPLY-CHAIN-SECURITY.md).
|
|
272
|
+
- Design history and plans (active and done):
|
|
273
|
+
[docs/project/specs/](https://github.com/jlevy/flexdoc/tree/main/docs/project/specs).
|
|
274
|
+
|
|
275
|
+
* * *
|
|
276
|
+
|
|
277
|
+
*This project was built from
|
|
278
|
+
[simple-modern-uv](https://github.com/jlevy/simple-modern-uv).*
|
|
279
|
+
|
|
280
|
+
<!-- This document follows common-doc-guidelines.md.
|
|
281
|
+
See github.com/jlevy/practical-prose and review guidelines before editing.
|
|
282
|
+
-->
|
flexdoc-0.4.0/README.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# FlexDoc
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/flexdoc/)
|
|
4
|
+
[](https://github.com/jlevy/flexdoc/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/jlevy/flexdoc/blob/main/LICENSE)
|
|
6
|
+
[](https://pypi.org/project/flexdoc/)
|
|
7
|
+
|
|
8
|
+
A Markdown parser gives you a block AST but no sentences, sizes, or exact source
|
|
9
|
+
offsets. An NLP toolkit gives you sentences but no Markdown structure.
|
|
10
|
+
If you need to know which sentence is in which section, how many words (or LLM tokens) a
|
|
11
|
+
section holds, or exactly where a link sits in the original text, you end up gluing
|
|
12
|
+
tools together; the glue breaks on the first edit.
|
|
13
|
+
|
|
14
|
+
FlexDoc builds a single source-grounded model and exposes its structure as layers over
|
|
15
|
+
one shared coordinate space: exact `[start, end)` offsets into one retained, normalized
|
|
16
|
+
source string. The **Markdown layer** (blocks, inline elements, typed attributes), the
|
|
17
|
+
**textual layer** (paragraphs, sentences, word tokens), and the **document layer**
|
|
18
|
+
(heading hierarchy, table of contents) are independent parses of the same text, so
|
|
19
|
+
cross-cutting questions are simple offset queries.
|
|
20
|
+
On top of the model sit portable formats: `DocGraph` serializes any slice as
|
|
21
|
+
language-neutral JSON, and `TextRef` makes spans, points, sections, and whole documents
|
|
22
|
+
durable references that resolve back to exact source.
|
|
23
|
+
|
|
24
|
+
FlexDoc is a standalone library.
|
|
25
|
+
[chopdiff](https://github.com/jlevy/chopdiff) builds its diff-filtering and
|
|
26
|
+
windowed-transform layer on top of flexdoc.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```shell
|
|
31
|
+
uv add flexdoc
|
|
32
|
+
# or: pip install flexdoc
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Status
|
|
36
|
+
|
|
37
|
+
**Beta** (0.4.x). The core model is established, but the later-stage mechanisms in the
|
|
38
|
+
[stabilization roadmap](https://github.com/jlevy/flexdoc/blob/main/docs/project/specs/active/plan-2026-07-09-flexdoc-stabilization-roadmap.md)
|
|
39
|
+
remain open. Breaking changes before 1.0 bump the minor version, so pin a minor version.
|
|
40
|
+
See the [changelog](https://github.com/jlevy/flexdoc/blob/main/CHANGELOG.md).
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
### Parse and Query
|
|
45
|
+
|
|
46
|
+
The primary entry point is `FlexDoc`. Parsing any input never raises: malformed
|
|
47
|
+
Markdown degrades deterministically and visibly. Every located unit carries an exact
|
|
48
|
+
`[start, end)` span into the normalized `source_text`, and `reassemble()` round-trips
|
|
49
|
+
the model back to normalized Markdown.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from flexdoc import FlexDoc, NodeKind, TextUnit
|
|
53
|
+
|
|
54
|
+
doc = FlexDoc.from_text("# Introduction\n\nSee [docs](https://example.com/docs).\n")
|
|
55
|
+
|
|
56
|
+
# Section hierarchy with rolled-up sizes:
|
|
57
|
+
print(doc.section_size_tree(units=(TextUnit.words, TextUnit.sentences)))
|
|
58
|
+
# # Introduction (8 words, 2 sentences)
|
|
59
|
+
|
|
60
|
+
# Sizes at every grain, including approximate LLM tokens:
|
|
61
|
+
print(doc.size_summary())
|
|
62
|
+
# 53 bytes (3 lines, 2 paras, 2 sents, 8 words, ~13 tok)
|
|
63
|
+
|
|
64
|
+
# One query primitive (collect()) spans all layers:
|
|
65
|
+
link = doc.collect(kinds={NodeKind.link})[0]
|
|
66
|
+
print(link.attrs["url"], link.source_span)
|
|
67
|
+
# https://example.com/docs (20, 52)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
FlexDoc delegates Markdown parsing to [marko](https://github.com/frostming/marko)
|
|
71
|
+
(CommonMark with GFM tables and footnotes) via
|
|
72
|
+
[flowmark](https://github.com/jlevy/flowmark), and adds sentence segmentation, the
|
|
73
|
+
section hierarchy, the flat node table, offset-grounded queries, references,
|
|
74
|
+
serialization, and span anchoring on top.
|
|
75
|
+
|
|
76
|
+
### Reference and Annotate: TextRef
|
|
77
|
+
|
|
78
|
+
Continuing the example, any public value maps to a
|
|
79
|
+
[`TextRef`](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md#11-textref-spanref-and-annotations):
|
|
80
|
+
a portable reference carrying quote evidence and a source hash, with canonical JSON and
|
|
81
|
+
a reversible `textref:0.1` URI projection.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
refs = doc.references(document="guide.md")
|
|
85
|
+
ref = refs.for_target(link)
|
|
86
|
+
|
|
87
|
+
print(ref.to_uri())
|
|
88
|
+
# textref:0.1?doc=guide.md&hash=sha256%3A0e91...0f86&type=span
|
|
89
|
+
# &exact=%5Bdocs%5D%28https%3A%2F%2Fexample.com%2Fdocs%29&prefix=...&start=20
|
|
90
|
+
|
|
91
|
+
res = refs.resolve(ref)
|
|
92
|
+
print(res.resolved, res.method.value, (res.span.start, res.span.end))
|
|
93
|
+
# True source_position (20, 52)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Annotations attach consumer-owned content to TextRef targets, and `AnnotationSet` is
|
|
97
|
+
their one-document JSON/YAML sidecar form:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from flexdoc import AnnotationSet, TextAnnotation, TextBody
|
|
101
|
+
|
|
102
|
+
note = TextAnnotation(id="n1", target=ref, motivations=["commenting"],
|
|
103
|
+
body=TextBody(type="text", value="Check this link."))
|
|
104
|
+
sidecar = AnnotationSet.from_annotations([note])
|
|
105
|
+
|
|
106
|
+
print(sidecar.to_yaml())
|
|
107
|
+
# format: text-annotations/0.1
|
|
108
|
+
# document: guide.md
|
|
109
|
+
# source_hash: sha256:0e91...0f86
|
|
110
|
+
# annotations:
|
|
111
|
+
# - id: n1
|
|
112
|
+
# ...
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Serialize: DocGraph
|
|
116
|
+
|
|
117
|
+
[`DocGraph`](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md#10-docgraph-the-serialized-projection)
|
|
118
|
+
is the language-neutral serialized projection: one source-anchored object carrying
|
|
119
|
+
document identity, a source hash shared with TextRef, the node table, derived views,
|
|
120
|
+
and (optionally) an embedded annotation sidecar. JSON is the wire form; YAML is the
|
|
121
|
+
human and golden-test form.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
graph = doc.graph(document="guide.md", annotations=sidecar)
|
|
125
|
+
|
|
126
|
+
print(graph.to_yaml())
|
|
127
|
+
# schema: DocGraph/v0.2
|
|
128
|
+
# source:
|
|
129
|
+
# format: markdown
|
|
130
|
+
# offset_unit: unicode_code_points
|
|
131
|
+
# document: guide.md
|
|
132
|
+
# source_hash: sha256:0e91...0f86
|
|
133
|
+
# nodes:
|
|
134
|
+
# - id: n0001
|
|
135
|
+
# kind: heading
|
|
136
|
+
# layer: markdown
|
|
137
|
+
# source_span:
|
|
138
|
+
# start: 0
|
|
139
|
+
# end: 14
|
|
140
|
+
# attrs:
|
|
141
|
+
# level: 1
|
|
142
|
+
# ... (more nodes, then views and the embedded annotations)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Word and Token Metrics
|
|
146
|
+
|
|
147
|
+
`TextUnit.words` is a logical-word measure: it matches a whitespace count for ordinary
|
|
148
|
+
non-wide prose averaging 3–6 characters per word, but normalizes wide/fullwidth scripts,
|
|
149
|
+
long identifiers and URLs, short-token sequences, and punctuation-dense code or
|
|
150
|
+
Markdown. Use `TextUnit.raw_words` for a literal whitespace-delimited count. See the
|
|
151
|
+
[logical-word definition and validation](https://gist.github.com/jlevy/0d6d87885f6d85f31440e58b8cfce663)
|
|
152
|
+
for the rationale, reference algorithm, and multilingual examples.
|
|
153
|
+
|
|
154
|
+
### Module Map
|
|
155
|
+
|
|
156
|
+
The full public surfaces live in the submodules:
|
|
157
|
+
|
|
158
|
+
- `flexdoc.docs`: `FlexDoc`, `Paragraph`, `Sentence`, `Section`, `Block`, `BlockType`,
|
|
159
|
+
the node table, `collect()`, `DocGraph`, `TextRef`, annotation values, `SpanRef`, and
|
|
160
|
+
source-linked render/report helpers.
|
|
161
|
+
- `flexdoc.docs.wordtoks`, `flexdoc.docs.search_tokens`,
|
|
162
|
+
`flexdoc.docs.token_diffs`, and `flexdoc.docs.token_mapping`: lower-level token,
|
|
163
|
+
search, diff, and mapping utilities that are not promoted by `flexdoc.docs`.
|
|
164
|
+
- `flexdoc.html`: html-in-md, html/plaintext conversion, HTML tag helpers, the content
|
|
165
|
+
extractor, and timestamp extraction.
|
|
166
|
+
- `flexdoc.util`: raw and cross-language logical word counts, read-time estimation, and
|
|
167
|
+
approximate token-count estimation.
|
|
168
|
+
|
|
169
|
+
### Worked Examples
|
|
170
|
+
|
|
171
|
+
See [usage.md](https://github.com/jlevy/flexdoc/blob/main/docs/usage.md) for the main
|
|
172
|
+
workflows. Each worked example is a runnable script (run with
|
|
173
|
+
`uv run python examples/<name>.py` from a checkout) demonstrating one workflow
|
|
174
|
+
end to end:
|
|
175
|
+
|
|
176
|
+
- [`doc_structure.py`](https://github.com/jlevy/flexdoc/blob/main/examples/doc_structure.py)
|
|
177
|
+
covers section hierarchy, size rollups, offset lookups, and the block tree.
|
|
178
|
+
- [`normalized_form.py`](https://github.com/jlevy/flexdoc/blob/main/examples/normalized_form.py)
|
|
179
|
+
covers block-type tallies, list-density invariance, and per-section links.
|
|
180
|
+
- [`backfill_timestamps.py`](https://github.com/jlevy/flexdoc/blob/main/examples/backfill_timestamps.py)
|
|
181
|
+
aligns an edited transcript to its timestamped source via token mapping.
|
|
182
|
+
- [`textref_workflows.py`](https://github.com/jlevy/flexdoc/blob/main/examples/textref_workflows.py)
|
|
183
|
+
composes extraction provenance, context retrieval, citations, annotations, and edit
|
|
184
|
+
targets.
|
|
185
|
+
|
|
186
|
+
## Design
|
|
187
|
+
|
|
188
|
+
The design of record is
|
|
189
|
+
[flexdoc-spec.md](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md),
|
|
190
|
+
which motivates each decision and defines the invariants the tests pin. The ideas that
|
|
191
|
+
carry the model:
|
|
192
|
+
|
|
193
|
+
- **One coordinate space.** Every layer indexes the same retained, normalized source
|
|
194
|
+
string with `[start, end)` Unicode code-point offsets, so cross-layer questions reduce
|
|
195
|
+
to offset queries
|
|
196
|
+
([spec §2](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md#2-principles-and-goals)).
|
|
197
|
+
- **Layers are independent parses.** Markdown structure, prose sentences, and the
|
|
198
|
+
heading hierarchy are separate projections of the same text; no single tree has to be
|
|
199
|
+
authoritative
|
|
200
|
+
([spec §4](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md#4-core-types-nodes-and-offsets)).
|
|
201
|
+
- **Parsing never raises.** Malformed Markdown degrades deterministically and visibly,
|
|
202
|
+
pinned by the golden-test corpus
|
|
203
|
+
([spec §13](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md#13-invariants-and-non-goals)).
|
|
204
|
+
- **References carry evidence, and resolution never guesses.** Positions are trusted
|
|
205
|
+
only under a matched source hash; quotes and context corroborate; a duplicated quote
|
|
206
|
+
resolves to a typed ambiguous outcome, never a silent first match
|
|
207
|
+
([spec §11](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md#11-textref-spanref-and-annotations)).
|
|
208
|
+
`SpanRef` is the lightweight quote anchor underneath.
|
|
209
|
+
- **Serialized contracts are strict and versioned.** `DocGraph/v0.2` and `textref/0.1`
|
|
210
|
+
reject unknown fields, and both ship committed JSON Schemas:
|
|
211
|
+
[doc_graph_schema.json](https://github.com/jlevy/flexdoc/blob/main/src/flexdoc/docs/doc_graph_schema.json)
|
|
212
|
+
and
|
|
213
|
+
[text_ref_schema.json](https://github.com/jlevy/flexdoc/blob/main/src/flexdoc/docs/text_ref_schema.json)
|
|
214
|
+
([spec §10](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md#10-docgraph-the-serialized-projection)).
|
|
215
|
+
|
|
216
|
+
## Project Docs
|
|
217
|
+
|
|
218
|
+
For users:
|
|
219
|
+
|
|
220
|
+
- Design of record (the full spec):
|
|
221
|
+
[flexdoc-spec.md](https://github.com/jlevy/flexdoc/blob/main/docs/flexdoc-spec.md).
|
|
222
|
+
- Usage guide: [usage.md](https://github.com/jlevy/flexdoc/blob/main/docs/usage.md).
|
|
223
|
+
- Installing uv and Python:
|
|
224
|
+
[installation.md](https://github.com/jlevy/flexdoc/blob/main/docs/installation.md).
|
|
225
|
+
|
|
226
|
+
For contributors:
|
|
227
|
+
|
|
228
|
+
- Development workflows:
|
|
229
|
+
[development.md](https://github.com/jlevy/flexdoc/blob/main/docs/development.md).
|
|
230
|
+
- Publishing to PyPI:
|
|
231
|
+
[publishing.md](https://github.com/jlevy/flexdoc/blob/main/docs/publishing.md).
|
|
232
|
+
- Dependency policy:
|
|
233
|
+
[SUPPLY-CHAIN-SECURITY.md](https://github.com/jlevy/flexdoc/blob/main/SUPPLY-CHAIN-SECURITY.md).
|
|
234
|
+
- Design history and plans (active and done):
|
|
235
|
+
[docs/project/specs/](https://github.com/jlevy/flexdoc/tree/main/docs/project/specs).
|
|
236
|
+
|
|
237
|
+
* * *
|
|
238
|
+
|
|
239
|
+
*This project was built from
|
|
240
|
+
[simple-modern-uv](https://github.com/jlevy/simple-modern-uv).*
|
|
241
|
+
|
|
242
|
+
<!-- This document follows common-doc-guidelines.md.
|
|
243
|
+
See github.com/jlevy/practical-prose and review guidelines before editing.
|
|
244
|
+
-->
|