Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,12 @@ jobs:
run: python3 tools/governance/check_architecture.py
- name: Verify maintained documentation
run: python3 tools/governance/check_documentation.py
- name: Verify Phase 2 compatibility manifest
run: python3 tools/compatibility/check_contract_manifest.py
- name: Run governance tests
run: python3 -m unittest discover -s tools/governance/tests -p 'test_*.py'
run: |
python3 -m unittest discover -s tools/governance/tests -p 'test_*.py'
python3 -m unittest discover -s tools/compatibility/tests -p 'test_*.py'
- name: Verify PR policy
if: github.event_name == 'pull_request'
run: python3 tools/governance/check_pr_policy.py --event-path "$GITHUB_EVENT_PATH"
Expand Down Expand Up @@ -207,6 +211,8 @@ jobs:
- run: moon update
- name: Build CLI
run: moon build --target native --package ZSeanYves/markitdown/cli
- name: Run Phase 2 local compatibility lab
run: python3 tools/compatibility/run_contract_lab.py --cli ./_build/native/debug/build/cli/cli.exe
- name: Install optional dependencies
run: |
for profile in ${{ matrix.profiles }}; do
Expand Down Expand Up @@ -257,6 +263,13 @@ jobs:
run: |
moon build --target native --release --package ZSeanYves/markitdown/cli
moon build --target native --release --package ZSeanYves/markitdown/internal/bench_runner
- name: Run Phase 2 upstream corpus comparison
run: |
python3 tools/compatibility/fetch_upstream_corpus.py
python3 tools/compatibility/run_contract_lab.py \
--cli ./_build/native/release/build/cli/cli.exe \
--upstream ./env/.venv-markitdown-bench/bin/markitdown \
--upstream-corpus ./.tmp/compatibility/upstream-v0.1.7
- name: Doctor
run: _build/native/release/build/internal/bench_runner/bench_runner.exe doctor
- name: Run benchmark
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

## Unreleased

### Phase 2 compatibility laboratory

- Added a pinned MarkItDown `v0.1.7` contract manifest with Tier A/B/C format
coverage, input-kind and hint dimensions, fixture hashes, and explicit
reference-only gaps for XLS, binary Outlook MSG, and RSS/URI converters.
- Added a blocking difference taxonomy and structural compatibility runner;
unclassified differences cannot silently update goldens.
- Added native executable checks for OMML preservation diagnostics, PPTX cached
chart lowering and chart fallback/SVG asset policy, plus XML, IPYNB, ZIP and
EPUB contract representatives.
- Added exact upstream v0.1.7 fixture retrieval with SHA-256 verification,
strict reviewed structural-difference fields, CP932 CSV decoding, bounded
native stdin input (`-` with explicit `--format`), and a CI upstream corpus
comparison gate.

### Documentation and evidence governance

- Rebuilt the documentation entry points around `docs/README.md`; removed the
Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ instead of browsing files by name.
- [Optional-enhancement architecture](./architecture/optional-enhancement-architecture.md)
- [Benchmark architecture](./architecture/benchmark-architecture.md)
- [Compatibility matrix](./compatibility-matrix.md)
- [Phase 2 compatibility lab](./phase-2-compatibility-lab.md): pinned upstream
corpus, structural comparator, and executable semantic gates.
- [Dependency register](./dependency-register.md)
- [Maintenance and evolution plan](./project-maintenance-plan.md)

Expand Down
54 changes: 54 additions & 0 deletions docs/adr/0005-phase-2-compatibility-lab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# ADR 0005: Official Compatibility Laboratory and Capability Tiers

- Status: accepted
- Date: 2026-08-08
- Owners: @ZSeanYves
- Related: Phase 2 of `docs/project-maintenance-plan.md`

## Context

Phase 0-1 froze the MarkItDown `v0.1.7` reference and created a stable API,
but compatibility evidence was split between local fixtures and the external
quality lab. There was no repository-owned manifest that required input-kind,
hint, mode, structural fields, and an explicit difference decision for each
case. In particular, the upstream fixes for DOCX equations, PPTX charts and
SVG-only pictures could not be audited as one contract.

## Decision

Create `tools/compatibility/` as the Phase 2 compatibility laboratory.

- Pin the upstream tag and commit in `contract-manifest.json`.
- Use deterministic project-owned equivalents for local executable cases and
record the upstream test filename, source kind, license and fixture hash.
- Keep XLS/BIFF, binary Outlook MSG and RSS/URI converters reference-only and
explicitly unsupported in the core capability manifest.
- Compare headings, paragraphs, tables, links, assets, math and diagnostics as
separate fields. A structural difference must have one of the five reviewed
categories in `difference-categories.json`; no automatic golden updates.
- Run local cases in every declared mode, and run the upstream `0.1.7` CLI when
its managed benchmark environment is installed. The local native integration
tests cover Path/Bytes/Reader and MIME/extension/no-hint detection.

## Consequences

The Phase 2 lab can report semantic compatibility without requiring byte-for-
byte Markdown identity. Existing project enhancements such as provenance and
diagnostic sections remain independently testable. Some local equivalents are
classified `undefined_behavior` until a direct upstream fixture or explicit
contract decision is available; they cannot be silently promoted to stable
goldens.

## Verification and rollback

```bash
python3 tools/compatibility/check_contract_manifest.py
python3 -m unittest discover -s tools/compatibility/tests -p 'test_*.py'
moon check --target all --warn-list +73 --deny-warn
moon test --target native --package ZSeanYves/markitdown/internal/integration_tests --filter 'phase2*'
python3 tools/compatibility/run_contract_lab.py --cli ./_build/native/release/build/cli/cli.exe
```

Rollback removes the Phase 2 CI steps and compatibility directory while
preserving the Phase 0-1 API and architecture gates. No product package imports
the laboratory.
2 changes: 2 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ decision. Link the ADR from the implementation PR and the maintenance plan.
consolidate the package graph after the Phase 0-1 audit.
- `0004-normalize-source-root.md`: make `src/` the only MoonBit source root
without changing published package names.
- `0005-phase-2-compatibility-lab.md`: establish the pinned contract corpus,
structural diff taxonomy, and capability-tier evidence gate.
17 changes: 12 additions & 5 deletions docs/compatibility-matrix.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Compatibility Matrix

This is the Phase 0 compatibility contract. The official comparison target is
This is the compatibility contract carried from Phase 0 into the Phase 2
official compatibility laboratory. The official comparison target is
Microsoft MarkItDown `v0.1.7` at commit
`fd239d5d2be43d9b68329730206b9312c7d5a388`. The upstream tag and local fixture
hashes are recorded in `tools/governance/phase0-baseline.json`.
hashes are recorded in `tools/governance/phase0-baseline.json`; the Phase 2
case dimensions and difference taxonomy are recorded in `tools/compatibility/`.

## Compatibility axes

Expand Down Expand Up @@ -52,7 +54,7 @@ must state which of the three axes they cover.
- A golden update must include a structured old/new diff and an explanation.
A PR that only changes golden files is rejected by policy.

## Required upstream checks
## Required Upstream Checks

For each upstream release, rerun:

Expand All @@ -63,8 +65,13 @@ For each upstream release, rerun:
5. HTML/CSV encoding/JSON/RSS XML/IPYNB/ZIP/EPUB vectors;
6. CLI output, exit codes, assets, diagnostics and no-network behavior.

Differences are classified as `bug`, `upstream-feature-gap`, `intentional-
enhancement`, or `undefined`. Only classified differences may be baselined.
Differences are classified as `bug`, `upstream_feature_missing`,
`expected_enhancement`, `undefined_behavior`, or `unsupported_by_design`.
Only classified differences may be baselined. The machine-readable manifest
rejects any missing classification.

The executable Phase 2 representative lab is documented in
[`phase-2-compatibility-lab.md`](./phase-2-compatibility-lab.md).

## Reproduction

Expand Down
63 changes: 63 additions & 0 deletions docs/phase-2-compatibility-lab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Phase 2 Compatibility Lab

Phase 2 turns compatibility from a narrative claim into a reviewed, repeatable
contract. The reference is Microsoft MarkItDown `v0.1.7` at commit
`fd239d5d2be43d9b68329730206b9312c7d5a388`. The reference repository is MIT
licensed; its binary test files are not copied into this repository. Instead,
the project stores deterministic local equivalents where redistribution and
provenance are clear, and records the exact upstream file name for audit.

The machine-readable contract lives in
`tools/compatibility/contract-manifest.json`. Each case specifies a format
tier, local fixture or reference-only status, SHA-256 when local, input kinds,
hint dimensions, modes, structural signals, and one reviewed difference
category.

The only accepted categories are defined in
`tools/compatibility/difference-categories.json`: `bug`,
`upstream_feature_missing`, `expected_enhancement`, `undefined_behavior`, and
`unsupported_by_design`. An unclassified difference cannot update a golden and
fails the manifest gate.

## Current Results

The local executable lab runs 15 representative cases across DOCX,
PPTX, XLSX, PDF, HTML, CSV, JSON, XML, IPYNB, ZIP, and EPUB. The checked cases
cover OMML preservation diagnostics, cached PPTX chart lowering, chart
fallback behavior, SVG asset policy, and non-empty conversion for the remaining
formats. The native run executes 28 local mode cases. A second invocation can
fetch the 17 exact upstream v0.1.7 binary fixtures (15 executable plus two
reference-only web fixtures), compare the same structural fields against the
official CLI, and currently passes 15/15 executable upstream samples.

Three upstream scenarios remain explicit reference-only gaps:

| Scenario | Product status | Stable behavior |
| --- | --- | --- |
| Legacy XLS/BIFF | unsupported | capability status `Unsupported`; no parser alias; follow-up [#155](https://github.com/ZSeanYves/markitdown/issues/155) |
| Binary Outlook MSG | unsupported | `msg` is RFC822/EML-only; binary input is not claimed compatible; follow-up [#156](https://github.com/ZSeanYves/markitdown/issues/156) |
| RSS/Atom and URI/web converters | unsupported in core | no network access; capability status `Unsupported`; follow-up [#157](https://github.com/ZSeanYves/markitdown/issues/157) |

These are intentionally not represented by an EML alias or a network fallback.
They remain in the manifest so an accidental capability expansion is visible.
Reference-only web fixture hashes are retained for provenance, and are never
executed as network converters.

Run the gates from a clean checkout:

```bash
python3 tools/compatibility/check_contract_manifest.py
python3 tools/compatibility/fetch_upstream_corpus.py
moon build --target native --release --package ZSeanYves/markitdown/cli
python3 tools/compatibility/run_contract_lab.py \
--cli ./_build/native/release/build/cli/cli.exe
python3 tools/compatibility/run_contract_lab.py \
--cli ./_build/native/release/build/cli/cli.exe \
--upstream ./env/.venv-markitdown-bench/bin/markitdown \
--upstream-corpus ./.tmp/compatibility/upstream-v0.1.7
```

The lab is a semantic gate, not a byte-for-byte promise. Markdown structure is
compared by field, while project provenance, diagnostics, source maps, and
asset metadata are checked independently. A changed expected output requires a
classified difference and a written decision in the same PR.
9 changes: 5 additions & 4 deletions docs/project-maintenance-plan.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MoonBit MarkItDown 项目维护与演进计划

**文档状态:** 已接受;Phase 0-1.6 已实施,作为 Phase 2-6 工作基线
**文档状态:** 已接受;Phase 0-2 已实施,作为 Phase 3-6 工作基线
**版本:** 1.0
**编制日期:** 2026-08-05
**适用范围:** `ZSeanYves/markitdown` 主模块、CLI、格式读取器、转换管线、native FFI、质量实验室、发布物和外部依赖
Expand Down Expand Up @@ -89,9 +89,9 @@

Phase 1 已将包从 108 收敛到 68,将 `pub(all)` 从 223 收敛到 210,
其中可构造/可变记录从 32 降到 22;`src/` 已成为唯一源码根目录。
公共面和包数量不再列为开放阻断项。当前工作重点是
公共面和包数量不再列为开放阻断项。Phase 2 已通过兼容实验室关闭本节的兼容证据缺口,后续重点是

1. **兼容证据仍需系统化。** 本地 contract fixture 覆盖不均,缺少 XLS、二进制 MSG、RSS/网页特化能力等上游场景的明确状态
1. **Phase 2 已关闭兼容证据阻断。** `contract-manifest.json` 固定 upstream v0.1.7,15 个原始 fixture 与 28 个本地模式案例均由 CI 重跑;XLS、二进制 MSG、RSS/网页特化能力保留为明确 unsupported 缺口
2. **self baseline 需要同指纹批准。** 2026-08-07 新测量覆盖 53 行,但现有 approved baseline 的 MoonBit、quality-lab、Python/runtime、OS/runner 指纹不同,不能据此宣称回归或提升。
3. **native 安全链仍需加强。** macOS/Linux native 全量链接和运行已经阻断 CI;ASan/UBSan、长期 fuzz 和子进程失败回收仍属于后续安全出口。
4. **候选依赖仍缺替换证据。** 社区包必须先经过 adapter、双跑、规范、安全、许可证、性能和退出计划,不能按下载量直接替换。
Expand Down Expand Up @@ -253,7 +253,8 @@ flowchart LR
| Phase 1 | 完成 | `api` façade、私有 Input、typed error/code、CLI 退出码、Path/Text/Bytes/Reader、Markdown/Debug/RAG、能力/来源投影、0.8 golden、迁移文档、ADR 和架构依赖门禁 |
| Phase 1.5 | 完成 | `src/` 唯一 MoonBit source root、逻辑包名保持、benchmark runner/集成测试内部化、根目录与物理路径治理门禁 |
| Phase 1.6 | 完成 | 文档生命周期和索引、README/CHANGELOG 全面复核、陈旧文档删除、链接/性能主张 CI 门禁、MarkItDown 0.1.7 正式性能重跑 |
| Phase 2-6 | 未开始 | 必须从本文件对应阶段入口继续,不得跳过兼容、性能、安全或发布验收门 |
| Phase 2 | 完成 | `tools/compatibility/` 固定 upstream corpus、结构化差分、OMML/PPTX 回归、stdin、能力分级和 CI 门禁 |
| Phase 3-6 | 未开始 | 必须从本文件对应阶段入口继续,不得跳过依赖、安全、性能或发布验收门 |

### 阶段 0:基线冻结与治理启动(第 0-2 周)

Expand Down
16 changes: 14 additions & 2 deletions src/cli/cli.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ pub fn run_cli_with_image_ocr_provider(
)
}

///|
fn cli_input_source(opts : CliOptions) -> @input.InputSource {
if opts.input == "-" {
@input.input_from_bytes(
cli_read_stdin(512 * 1024 * 1024),
source_name="stdin." + cli_requested_format_label(opts),
)
} else {
@input.input_from_path(opts.input)
}
}

///|
fn run_cli_with_image_ocr_provider_impl(
opts : CliOptions,
Expand Down Expand Up @@ -92,7 +104,7 @@ fn run_cli_with_image_ocr_provider_and_pdf_rasterizer(
}
let execution = match
@convert.convert_input_with_provenance_and_image_ocr_provider_and_pdf_rasterizer(
@input.input_from_path(opts.input),
cli_input_source(opts),
convert_options,
image_ocr_provider,
pdf_rasterizer,
Expand Down Expand Up @@ -179,7 +191,7 @@ fn cli_convert_to_atomic_stream_file(
)
let result = match
@convert.convert_input_to_sink_unbuffered(
@input.input_from_path(opts.input),
cli_input_source(opts),
convert_options,
sink,
) {
Expand Down
15 changes: 12 additions & 3 deletions src/cli/cli_parse.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ fn cli_requested_format(opts : CliOptions) -> @input.DetectedFormat {
Some(label) => @input.parse_detected_format(label)
None => {
let detector = @input.default_format_detector()
let detected = (detector.detect)(@input.input_from_path(opts.input))
let detected = (detector.detect)(cli_input_source(opts))
detected.format
}
}
Expand Down Expand Up @@ -38,7 +38,13 @@ fn cli_requested_format_from_input(
Some(label) => @input.parse_detected_format(label)
None => {
let detector = @input.default_format_detector()
let detected = (detector.detect)(@input.input_from_path(input_path))
let detected = (detector.detect)(
if input_path == "-" {
@input.input_from_bytes(Bytes::new(0), source_name="stdin.bin")
} else {
@input.input_from_path(input_path)
},
)
detected.format
}
}
Expand Down Expand Up @@ -340,7 +346,7 @@ fn parse_cli_options_from_index(
i += 2
continue
}
if arg.has_prefix("-") {
if arg.has_prefix("-") && arg != "-" {
return Err("unsupported option: " + arg)
}
positionals.push(arg)
Expand All @@ -363,6 +369,9 @@ fn parse_cli_options_from_index(
Ok(value) => value
Err(msg) => return Err(msg)
}
if effective_positionals[0] == "-" && explicit_format is None {
return Err("stdin input requires an explicit --format")
}
let requested_format = cli_requested_format_from_input(
effective_positionals[0],
explicit_format,
Expand Down
7 changes: 7 additions & 0 deletions src/cli/cli_stdin_wbtest.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
///|
#cfg(target="native")
test "main cli routes dash input through the stdin source" {
let source = cli_input_source(cli_options("-", None, Some(Txt), Markdown))
assert_true(@input.input_path(source) is None)
assert_true(@input.input_bytes(source) is Some(_))
}
10 changes: 9 additions & 1 deletion src/cli/moon.pkg
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,13 @@ pkgtype(kind: "executable")

options(
"preferred-target": "native",
"native-stub": [ "stderr_native_stub.c", "atomic_file_sink_native_stub.c" ],
"native-stub": [
"stderr_native_stub.c",
"atomic_file_sink_native_stub.c",
"stdin_native_stub.c",
],
targets: {
"stdin_native.mbt": [ "native" ],
"stdin_portable.mbt": [ "wasm", "wasm-gc", "js" ],
},
)
8 changes: 8 additions & 0 deletions src/cli/stdin_native.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
///|
#cfg(target="native")
extern "C" fn cli_read_stdin_ffi(max_bytes : Int) -> Bytes = "markitdown_cli_read_stdin"

///|
fn cli_read_stdin(max_bytes : Int) -> Bytes {
cli_read_stdin_ffi(max_bytes)
}
Loading
Loading