Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b955eed
Project scaffolding (pyproject, src layout, configs, dormant CI, docs…
imnasnainaec Jul 17, 2026
3149084
Vendor lift-0.13.rng + assemble test corpus with provenance
imnasnainaec Jul 15, 2026
59556ad
Corpus tooling (0.12->0.13 migration, synthetic generator) + corpus s…
imnasnainaec Jul 15, 2026
2766bb4
Text model + full-document reader
imnasnainaec Jul 15, 2026
c464f44
Writer + losslessness
imnasnainaec Jul 15, 2026
f28e63f
Ranges + folder handling
imnasnainaec Jul 15, 2026
dcb1862
Validation module
imnasnainaec Jul 15, 2026
c8521b1
Negative-test corpus + validator test suite
imnasnainaec Jul 16, 2026
eb07a33
Canonical sort
imnasnainaec Jul 16, 2026
b051705
Streaming read/write
imnasnainaec Jul 16, 2026
b638102
CLI demo, documentation site, release polish
imnasnainaec Jul 16, 2026
42d22b3
docs: add 'Differences from the C# libraries' page
imnasnainaec Jul 16, 2026
6273ecb
CLI export subcommand + bulk-edit worked example docs
imnasnainaec Jul 16, 2026
f59ee8a
CI: split into build.yml and test.yml
imnasnainaec Jul 16, 2026
708f711
Pin ruff and mypy exactly in the dev extra; tidy pyproject
imnasnainaec Jul 16, 2026
8281ecb
Add .editorconfig with byte-exact carve-outs; restyle YAML configs
imnasnainaec Jul 16, 2026
4529147
Docs and editor-config batch
imnasnainaec Jul 16, 2026
6c5b256
Document deferred maintenance triggers: Python 3.11 floor, MkDocs->Ze…
imnasnainaec Jul 16, 2026
7937bce
Record created Crowdin project in crowdin.yml dormancy note
imnasnainaec Jul 17, 2026
d9c858f
Agent docs, branch/release policy, PyPI release workflow; version 0.1.0
imnasnainaec Jul 17, 2026
1918042
Docs on GitHub Pages; PyPI/project URLs in README, docs, pyproject; C…
imnasnainaec Jul 17, 2026
b339981
Validate what save() would write, not the loaded bytes
imnasnainaec Jul 17, 2026
634bf30
Exclude tests/ from the sdist; resolve the corpus license-review flag
imnasnainaec Jul 17, 2026
b320e04
Harden two review-noted edges: re-key ranges_files on save; tolerate …
imnasnainaec Jul 17, 2026
fee1e30
Trim test summaries; Set 90% coverage floor
imnasnainaec Jul 17, 2026
96a5035
Remove phase/research/decision/milestone refs
imnasnainaec Jul 17, 2026
3cecd8b
Bump actions/checkout to v7.0.0, actions/setup-python to v6.3.0
imnasnainaec Jul 17, 2026
e5947db
Fix 3 validation coverage gaps; scheme-valid dummy GUIDs in tests
imnasnainaec Jul 17, 2026
82e4fe6
Fix audit findings: doc/comment consistency, test enforcement, writer…
imnasnainaec Jul 20, 2026
e55c88b
Fix canonical-output gluing, header wrapper residue, and duplicate he…
imnasnainaec Jul 20, 2026
fe65d6f
Add JSON, strict, and no-check-media options to CLI validate
imnasnainaec Jul 21, 2026
0065b08
Note in AGENTS.md that commit messages must be self-contained
imnasnainaec Jul 21, 2026
605b200
Add LIFT-export tooling: builders, validator checks, CLI, container
imnasnainaec Jul 22, 2026
8ac02cc
Pin shell scripts to LF line endings
imnasnainaec Jul 22, 2026
ffbd524
Remove the empty [cli] install extra
imnasnainaec Jul 22, 2026
17a5081
Add zip-packaged LIFT read/write with hardened extraction
imnasnainaec Jul 22, 2026
b748aea
Treat foreign-OS absolute hrefs as absolute in _normalize_href
imnasnainaec Jul 22, 2026
1312ed2
Update provenance for resolved upstream licenses
imnasnainaec Jul 22, 2026
3da8410
Accept zip in the export command and add a from-scratch export guide
imnasnainaec Jul 23, 2026
ae30cbb
Align and polish the documentation
imnasnainaec Jul 23, 2026
0ab57e7
Drop numbered data-model quirk references from comments
imnasnainaec Jul 23, 2026
7ccf0b4
Widen duplicate-guid and undefined-range-value semantic checks
imnasnainaec Jul 23, 2026
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
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.venv/
.git/
.mypy_cache/
.ruff_cache/
.pytest_cache/
.hypothesis/
site/
__pycache__/
.coverage
tests/
33 changes: 33 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# https://editorconfig.org — VS Code needs the EditorConfig extension;
# JetBrains/Vim honor this natively. Python style itself is owned by ruff.
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.{md,toml,yml,yaml}]
indent_size = 2
indent_style = space

[*.md]
max_line_length = 80
# Trailing whitespace is meaningful in Markdown (hard line breaks).
trim_trailing_whitespace = false

# MkDocs source tree.
[docs/**.md]
# List sub-items require 4-space indentation.
indent_size = 4
# Hard-wrapped prose gets in the way of Crowdin translation.
max_line_length = unset

# Byte-exact payloads (mirrors the .gitattributes exemptions): editors must
# never "fix" these — the fidelity tests depend on the exact bytes.
[{src/sil_lift/schemas/**,tests/corpus/**,tests/tools/xslt/**}]
charset = unset
end_of_line = unset
insert_final_newline = unset
trim_trailing_whitespace = unset
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Normalize line endings for source files.
* text=auto

# Shell scripts must keep LF endings so they run in the Linux container even
# when checked out on Windows (a CRLF shebang breaks the entrypoint).
*.sh text eol=lf

# Fidelity-critical payloads: corpus fixtures and vendored schemas/XSLTs must be
# stored byte-for-byte as fetched — git must never touch their line endings
# (byte-identity round-trip tests depend on this).
tests/corpus/** -text
src/sil_lift/schemas/*.rng -text
tests/tools/xslt/* -text
30 changes: 30 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Build

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
build:
strategy:
matrix:
# The oldest and newest supported Python.
python-version: ["3.11", "3.14"]
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.python-version }}
- name: Install build tools
run: python -m pip install build==1.5.0 twine==6.2.0
- name: Build distribution
run: python -m build
- name: Check distribution metadata
run: python -m twine check dist/*
27 changes: 27 additions & 0 deletions .github/workflows/container.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Container

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Build image
run: docker build -t sil-lift:ci .
- name: Smoke test
run: |
tmp="$(mktemp -d)"
cat > "$tmp/smoke.lift" <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<lift version="0.13"><entry id="e1"><lexical-unit><form lang="en"><text>x</text></form></lexical-unit></entry></lift>
EOF
cd "$tmp"
docker run --rm -v "$PWD:/work" -w /work sil-lift:ci validate smoke.lift --format json
42 changes: 42 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Docs

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.14"
- name: Install docs dependencies
run: python -m pip install -e .[docs]
- name: Build site
run: mkdocs build --strict
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: site/

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
permissions:
id-token: write
pages: write
steps:
- name: Deploy to GitHub Pages
id: deploy
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
38 changes: 38 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Release

on:
release:
types: [published]

permissions:
contents: read

jobs:
publish:
# PyPI publish via Trusted Publishing (OIDC, no stored token).
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/sil-lift
permissions:
contents: read
id-token: write
steps:
- name: Check out the release's tagged commit
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.14"
- name: Install build tools
run: python -m pip install build==1.5.0 twine==6.2.0
- name: Build distribution
run: python -m build
- name: Check distribution metadata
run: python -m twine check dist/*
- name: Check tag matches package version
env:
TAG: ${{ github.event.release.tag_name }}
run: ls dist/sil_lift-"${TAG#v}"-py3-none-any.whl
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@6733eb7d741f0b11ec6a39b58540dab7590f9b7d # v1.14.0
42 changes: 42 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Test

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
test:
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
include:
# macOS as a canary on the latest Python only.
- python-version: "3.14"
os: macos-latest
runs-on: ${{ matrix.os }}
steps:
- name: Check out
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: python -m pip install -e .[dev]
- name: Lint
run: python -m ruff check .
- name: Check formatting
run: python -m ruff format --check .
- name: Type-check
run: python -m mypy
- name: Test
run: python -m pytest --cov=sil_lift --cov-report=term-missing
- name: Coverage summary + floor
# Only run on one matrix leg.
if: matrix.os == 'macos-latest'
run: python -m coverage report --format=markdown --fail-under=90 >> "$GITHUB_STEP_SUMMARY"
28 changes: 28 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Environments
.venv/
venv/

# Byte-compiled / caches
__pycache__/
*.py[cod]
.mypy_cache/
.ruff_cache/
.pytest_cache/
.hypothesis/
.coverage
coverage.xml
htmlcov/

# AI files
.claude/

# Build artifacts
build/
dist/
*.egg-info/

# Docs build
site/

# Generated corpus (synthetic large files — regenerate with tests/tools/generate_large.py)
tests/corpus/generated/
22 changes: 22 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Agent notes for sil-lift

Read and follow [CONTRIBUTING.md](CONTRIBUTING.md) in full — its byte-exact
corpus rules and fidelity contract are essential to avoid silently breaking this
repo.

Not covered there:

- No remote actions without asking the user first — no push, repo or project
creation, issue or PR creations, PyPI publishing, or posting anywhere.
- No destructive actions ever — no force push, branch deletion, or changing repo
settings.
- Commit messages describe the commit's diff and stand on their own. Do not
reference conversation, tasks, audits, review rounds, or other outside context
("as requested", "fixes audit finding", "per feedback") — a reader with only
the diff should understand the message fully.
- Root-level prose docs (`README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, this
file) are hard-wrapped, so a bare Markdown list marker (`+`, `-`, `*`) can be
pushed to the start of a re-wrapped line and render as a stray bullet. Where
that can bite — e.g. `sort \+ save` in the changelog — the marker is
backslash-escaped on purpose; keep the `\`. `docs/en/` is not hard-wrapped
(one line per paragraph), so it uses no such escaping.
93 changes: 93 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). During 0.x, minor
releases may contain breaking changes.

<!-- Available types of changes:
### Added
### Fixed
### Changed
### Deprecated
### Removed
### Security
-->

## [Unreleased]

## [0.1.0] - 2026-07-TBD

### Added

- Project scaffolding: package skeleton, vendored LIFT 0.13 RELAX NG schema,
test corpus with provenance, corpus-prep and large-file-generator tooling.
- Full object model: all 35 LIFT 0.13 elements as typed dataclasses;
`sil_lift.load()` / `Lexicon.load()` full-document reader with per-node
`Extras` residue capture; LIFT-version guard.
- `Lexicon.save()` writer with byte-fidelity passthrough — unchanged
documents and untouched entries are written byte-identically; touched entries
re-serialize canonically with all out-of-schema content preserved. Fidelity
contract documented in `docs/en/fidelity.md` and enforced by corpus
byte-identity tests plus Hypothesis round-trip properties.
- LIFT-folder handling: `RangesFile` (standalone `.lift-ranges` documents,
same fidelity guarantees), automatic companion discovery/tracking on load
(`Lexicon.ranges_files`), `save()` writes companions together,
`all_ranges()` merged view, `media_refs()` / `missing_media()` helpers,
build-from-scratch helpers `Lexicon.add_ranges_file()` /
`RangesFile.add_range()` / `Range.add_element()` (`save()` writes and
header-references a new companion beside the `.lift`); authored
`schemas/lift-ranges-0.13.rng` — the first schema for standalone
ranges documents (spec-faithful, built from the vendored grammar's own
defines).
- Zipped LIFT packages: `sil_lift.load()` reads a `.zip` (both the flat and
folder-wrapped layouts, junk entries like `__MACOSX` ignored),
`Lexicon.save_zip()` writes one (carrying media, `WritingSystems/`, and other
package files through verbatim); `validate`, `stats`, `check-media`, and
`export` accept a `.zip` path on the CLI. Extraction rejects path-traversal
members and is capped (entry count and a 10 GiB uncompressed total) against
zip bombs.
- Validation: `validate_file()` / `iter_problems()` /
`Lexicon.iter_problems()` returning an addressable `Problem` stream
(file/entry/line). RELAX NG layer with two documented deviations from raw
libxml2 (href masking with `uri-not-rfc` warnings; tag-grouped validation to
sidestep libxml2's interleave limitation); authored ranges schema over
companions; semantic checks: duplicate-guid (entries, and
ranges/range-elements within their own document — matching the C#
`Validator`'s document-wide guid scan), dangling-ref, range-parent,
undefined-range-value (every grammatical-info and range-keyed trait
reachable from an entry, however nested \+ NFC-normalized),
duplicate-form-lang, missing-media, dangling-ranges-href, and (opt-in via
`require_ids`) missing-id.
- Canonical sort: `Lexicon.sort()` / `RangesFile.sort()` (entries by
case-folded guid/id, ranges/range-elements by id, field definitions by tag;
informed by the C# LiftSorter, locale-independent) and
`sil_lift.canonicalize()` for fully re-serialized diff-ready output. Sorting
composes with the passthrough: sort \+ save moves untouched entries' bytes
without rewriting them. Text whitespace is never normalized.
- Streaming: `open_reader()` (lazy entry iterator with the parsed header
available up front) and `open_writer()` (header \+ one canonical chunk per
entry; byte-identical to `canonical_document` output by construction, and
optionally writing a `.lift-ranges` companion via `ranges=`), both
over the same `Entry` types as full-document mode and O(one entry) in
memory (verified on a ~340 MB generated file).
- The `sil-lift` CLI (stdlib-only, installed with the package):
`validate` / `stats` / `sort` / `check-media`, plus `export` — one row per
leaf sense (subsenses flattened) to CSV/TSV, streaming; analysis languages
auto-detected or set with `--langs`. `validate` supports `--format json`
(machine-readable findings), `--strict` (warnings become errors),
`--no-check-media` (skip the filesystem media-presence check),
`--require-ids` (error on entries/senses missing a stable id), and `-` to
read from stdin; `stats` also takes `--format json`. `validate`'s exit codes
and JSON schema are a supported interface.
- Container image and GitHub Action wrapping `sil-lift validate`, so a
non-Python CI pipeline can run the conformance check with no local Python
toolchain (`Dockerfile`, `action.yml`, `docker-entrypoint.sh`).
- Documentation site: task-oriented guides, the fidelity contract, generated
API reference, localization-ready configuration; includes worked examples
("bulk-editing glosses" and "building a LIFT export from scratch", complete
runnable scripts with verified output) and
a "Differences from the C# libraries" page summarizing where sil-lift
deviates from SIL.Lift/LiftSorter/Chorus behavior and why.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
Loading