dgconvert 0.1.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.
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .vscode/
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
3
+
4
+ - - -
5
+ ## 0.1.0 - 2026-08-19
6
+ #### Features
7
+ - (**cog**) add cocogitto changelog generation - (1b31be7) - ScriptAutomate, *Claude Sonnet 5*
8
+ - add build/publish just recipes and trim the sdist - (0073681) - ScriptAutomate
9
+ - add PyPI package metadata (MIT license, urls, classifiers) - (08e6afc) - ScriptAutomate
10
+ - add body_snatchers module, sourced from docufig.seed.yaml - (f127c52) - ScriptAutomate
11
+ - extract generic helper modules from dg-tgdp - (be68511) - ScriptAutomate, *Claude Sonnet 5*
12
+ #### Documentation
13
+ - remove references to sibling repos, make dgconvert standalone - (cfc6172) - ScriptAutomate
14
+ - adopt helper-library architecture and drop nox-style runner - (dc1ca93) - ScriptAutomate, *Claude Sonnet 5*
15
+ #### Miscellaneous Chores
16
+ - drop authors from pyproject.toml - (dd2b421) - ScriptAutomate
17
+ - scaffold python project with spec-kit, uv, justfile, and devcontainer - (e1fef62) - ScriptAutomate, *Claude Sonnet 5*
18
+ #### Style
19
+ - normalize justfile formatting via just --fmt - (2d3f732) - ScriptAutomate
20
+
21
+ - - -
22
+
23
+ Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto).
@@ -0,0 +1,239 @@
1
+ # dgconvert design — a helper library, not a runner
2
+
3
+ Status: extracted (2026-08-08) — the four generic modules plus the
4
+ incremental-conversion driver described below now live in
5
+ `dgconvert/src/dgconvert/`, per the extraction plan at the bottom of this
6
+ document. Seed repos depend on `dgconvert` as a real dependency rather
7
+ than owning their own copies. Originally drafted 2026-07-16 against a
8
+ still-inline reference implementation. Supersedes an earlier
9
+ fixed-pipeline architecture and an even earlier nox-style-runner draft
10
+ of this document. The compatibility contract, language decision
11
+ (Python, uv-managed), and verification strategy from that original plan
12
+ carry over unchanged.
13
+
14
+ ## Terminology
15
+
16
+ - **Garden** — a directory of notes (`notes/*.md`).
17
+ - **Greenhouse** — a DocsGarden workspace rooted at `docufig.json`,
18
+ containing one or more gardens.
19
+ - **Seed** — a pre-created garden, ready to be planted into a greenhouse.
20
+ A seed is described by its own `docufig.seed.json` (license,
21
+ topHierarchy, source provenance, bodySnatchers).
22
+ - **Seedbank** — a registry of seeds (a separate, not-yet-built dgpkm
23
+ feature; not something dgconvert or a seed repo needs to know about).
24
+
25
+ `dgconvert` produces **seeds**. See dgpkm's own documentation for the
26
+ full terminology writeup.
27
+
28
+ ## Core idea
29
+
30
+ Every seed repo owns its own conversion: its `docufig.seed.json`
31
+ (declarative — license, topHierarchy, source repo info) plus a plain
32
+ Python script that reads that config and drives the conversion
33
+ (imperative — fetch, transform, write, verify). **dgconvert is not a
34
+ task runner and does not discover or execute anything on a seed
35
+ repo's behalf** — an earlier draft of this document proposed a nox-style
36
+ `seedfile.py` + task-discovery model; that was rejected in favor of
37
+ dgconvert being nothing more than a plain library that a seed's own
38
+ script imports.
39
+
40
+ This mirrors the reasoning that was already settled before dgconvert had
41
+ any code: sources vary too widely (GitHub docs repos, MediaWiki, ebooks,
42
+ CSV data files) for a fixed pipeline or task-discovery convention to fit
43
+ all of them — a seed's own script is already the right level of control,
44
+ and dgconvert's job is only to make that script short.
45
+
46
+ ## What a seed repo looks like
47
+
48
+ | File / dir | Role | Owned by |
49
+ |---|---|---|
50
+ | `docufig.seed.json` | **What** the seed is: name, license, topHierarchy, source repo/owner info, bodySnatchers. dgpkm's schema; validated by `dgpkm doctor schema seed`. | seed maintainer, dgpkm-schema-shaped |
51
+ | `src/<pkg>/convert.py` (or similar) | **How** it's built: fetch, transform, write, verify. Imports `dgconvert` for the parts that are the same across every seed. | seed maintainer |
52
+ | `notes/*.md` | The output: the garden itself. | generated |
53
+
54
+ The first real seed repo built against this pattern is dgconvert's
55
+ reference implementation — its own conversion modules were the working
56
+ prototype this library gets generalized from, not a consumer built
57
+ against an already-finished dgconvert.
58
+
59
+ ## Library API surface
60
+
61
+ Extracted 1:1 from a first seed's own generic modules (originally
62
+ prototyped, working, with zero source-specific imports — extraction was
63
+ a house-move, not a redesign):
64
+
65
+ - **`dgconvert.frontmatter`** — single-line frontmatter emission and a
66
+ parser that mirrors dgpkm's own. Two verified constraints drive this:
67
+ dgpkm's frontmatter delimiter split (`strings.SplitN(content, "---",
68
+ 3)` in Go) is **not line-anchored**, so a value containing the literal
69
+ substring `"---"` truncates the frontmatter block early — every
70
+ emitted value is sanitized to a single line with `"---"` replaced; and
71
+ dgpkm requires `id:` to be present and `created`/`updated` to be
72
+ epoch-ms integers. No YAML library is used — round-tripping through a
73
+ real YAML parser reintroduces exactly the failure mode this avoids
74
+ (folded scalars, auto-quoting) and can't guarantee output matches
75
+ dgpkm's naive line-based reader.
76
+ - **`dgconvert.slugs`** — dotted-hierarchy slug joining and
77
+ wikilink/Dendron-link builders. Always **alias-after**
78
+ (`[[slug#anchor|Display]]`) — dgpkm cannot parse Dendron's alias-first
79
+ on-disk form.
80
+ - **`dgconvert.links`** — markdown-link scanning and rewriting via a
81
+ caller-supplied resolver callback, plus `defeat_md_regex`: dgpkm's
82
+ backlinks extractor matches **any** `](....md)` or `](....md#anchor)`,
83
+ including full external URLs, and does not skip fenced code blocks —
84
+ a link to unconverted or external content that bare-ends in `.md`
85
+ gets a harmless query string appended (`?ref_type=tags`) to defeat the
86
+ regex, confirmed necessary against real converted content (several
87
+ files reference other projects' `CHANGELOG.md`/`README.md` by full
88
+ URL with no query string).
89
+ - **`dgconvert.dgpkm`** — a `dgpkm` binary subprocess wrapper.
90
+ `dgpkm doctor backlinks` and `dgpkm doctor schema <type>` **always
91
+ exit 0**, even with findings — findings are only visible in
92
+ stdout/stderr text (`Found N broken link(s)`, `✗ ...`). This module
93
+ parses that text so a conversion script can fail loudly (non-zero
94
+ exit) when its output isn't doctor-clean.
95
+ - **`dgconvert.incremental`** — the create/skip/update/delete driver,
96
+ extracted once a second, independent consumer needed the same shape.
97
+ State lives **in each note's frontmatter** (a `sourceFileHash` sha256
98
+ key), not a separate state file. Re-running after new upstream
99
+ source: unchanged hash → skip (byte-identical, `created`/`updated`
100
+ both preserved); changed hash → rewrite in place (same `id`, same
101
+ `created`, bumped `updated`); source gone → delete the note and
102
+ report it. Callers supply a per-item `build()` closure (called only
103
+ for new/changed items, never for a skip) returning `(frontmatter,
104
+ body, extra)`; source-specific concerns (index-note generation, a
105
+ source's own path→slug table) stay in the seed repo, only the loop
106
+ itself is generic.
107
+ - **`dgconvert.body_snatchers`** — find/replace content transforms,
108
+ sourced directly from a seed's own `docufig.seed.yaml` (`bodySnatchers`
109
+ is a required field in dgpkm's own seed schema, not decorative), so a
110
+ seed's Python code never keeps a second, hand-maintained copy in sync
111
+ with that already-schema-validated manifest.
112
+
113
+ ## Seed-specific code stays in the seed repo
114
+
115
+ Everything that only makes sense for one source format lives in the seed
116
+ repo, never in dgconvert: a source's own index/manifest model, its own
117
+ path→slug table, its attribution text, its kind-index-note builders have
118
+ no dgconvert equivalent and shouldn't get one — a future MediaWiki or
119
+ ebook seed will have an equally source-specific module of its own, built
120
+ the same way, using the same generic library pieces above.
121
+
122
+ ## Non-Markdown sources: markitdown
123
+
124
+ Planned dependency (not yet added — no seed has needed it yet):
125
+ [`microsoft/markitdown`](https://github.com/microsoft/markitdown), for
126
+ converting non-Markdown sources (PDFs, Office docs, HTML scrapes, etc.)
127
+ into Markdown before the rest of the pipeline (frontmatter, slugs, link
128
+ rewriting) applies. Kept as an optional extra (`dgconvert[markitdown]`)
129
+ rather than a base dependency, since most seeds don't need it (e.g. one
130
+ real seed handles its CSV files by fencing them as code blocks rather
131
+ than converting them).
132
+
133
+ ## Bundling the `dgpkm` binary
134
+
135
+ Open design question, not yet solved: `dgconvert.dgpkm` needs a `dgpkm`
136
+ binary to shell out to. Today this is solved by an environment variable
137
+ (`DGPKM_BIN`, falling back to `dgpkm` on `PATH`) and a documented
138
+ limitation that no devcontainer bundles one, because dgpkm has no
139
+ distributable releases yet.
140
+
141
+ **Recommendation once dgpkm does cut releases:** a separate,
142
+ platform-tagged PyPI package (e.g. `dgpkm-bin`) that vendors the compiled
143
+ binary as wheel data — one wheel per platform, built in dgpkm's own
144
+ release CI, the same pattern `ruff`/`uv` use for their own Rust binaries.
145
+ `dgconvert` would depend on it, with resolution order `DGPKM_BIN` env var
146
+ → `dgpkm` on `PATH` → the bundled binary. This keeps dgconvert's own
147
+ dependency footprint honest (it depends on a real published artifact, not
148
+ a hand-rolled download-on-first-run step) and requires no changes to the
149
+ `dgconvert.dgpkm` module's public interface — only its binary-discovery
150
+ order gains a third fallback.
151
+
152
+ ## Dependencies
153
+
154
+ Kept deliberately minimal. `pyyaml` is a base dependency:
155
+ `frontmatter.emit_note`'s `extra` passthrough (arbitrary
156
+ provenance/source fields a seed's own frontmatter model doesn't cover,
157
+ e.g. a task-tracking source format's own metadata or a nested custom
158
+ object) needs real YAML serialization to round-trip nested values
159
+ correctly — dgpkm's own frontmatter reader is a real YAML parser too
160
+ (`go.yaml.in/yaml/v4`, both directions), so this isn't working around a
161
+ naive reader the way the known-field emitter still does; also used by
162
+ `dgconvert.body_snatchers` to read a seed's own `docufig.seed.yaml`
163
+ directly. Slugs, link rewriting, hashing, and subprocess handling remain
164
+ stdlib-only. Planned additions, both as extras rather than base
165
+ dependencies:
166
+
167
+ - `markitdown` — non-Markdown source conversion (see above).
168
+ - A future `dgpkm-bin` platform wheel (see above), once it exists.
169
+
170
+ Dev dependencies: `pytest`, `ruff`.
171
+
172
+ ## Toolchain parity
173
+
174
+ uv-managed, `.justfile` mirroring dgpkm's own
175
+ (`build-devcontainer` gated on `USE_CONTAINER_DEV`,
176
+ `sync`/`fmt`/`lint`/`test`/`check`, cocogitto conventional commits),
177
+ `.devcontainer/` (Python base image, uv, just, cocogitto). Unlike seed
178
+ repos, **dgconvert uses Spec Kit** (`.specify/`, this repo's `.claude/`
179
+ skills) for its own development — seed repos deliberately don't, since
180
+ they're conversion/data repos, not spec-driven feature work.
181
+
182
+ ## Extraction plan — done (2026-08-08)
183
+
184
+ 1. ~~Move the first seed's own generic modules into
185
+ `dgconvert/src/dgconvert/{frontmatter,slugs,links,dgpkm}.py`
186
+ verbatim~~ — done, plus one real addition: `frontmatter.emit_note`
187
+ gained an optional `extra` parameter (arbitrary passthrough YAML,
188
+ sorted-key, real-YAML-serialized) that the first seed doesn't use but
189
+ a second, independent consumer needs for fields with no dgpkm-native
190
+ equivalent. `dgpkm.verify`'s schema check also gained an off switch
191
+ (`check_schema=None`) since not every `dgconvert` consumer is itself
192
+ a seed with a `docufig.seed.json` to validate.
193
+ 2. ~~Point the first seed's `pyproject.toml` at `dgconvert` as a
194
+ dependency~~ — done (a real dependency, editable during local
195
+ development). Verified behavior-preserving: its own source-specific
196
+ test suite (the tests that didn't move to `dgconvert`) still passes,
197
+ and a real re-run against its already-converted `notes/` garden
198
+ produces a byte-identical `git diff` (confirmed against the
199
+ pre-refactor code as a baseline, not just the tests).
200
+ 3. ~~Extract `run_convert`'s incremental-diff shape into
201
+ `dgconvert.incremental`~~ — done, once a second, independent consumer
202
+ needed the same create/skip/update/delete-by-hash shape.
203
+ Source-specific concerns (index-note generation, a source's own
204
+ path→slug table) stayed in that seed's own repo as originally
205
+ planned; only the loop itself (a per-item `build()` closure plus
206
+ id/created/updated bookkeeping) is generic.
207
+ 4. ~~Write the corresponding `dgconvert` test suite by moving~~ — done
208
+ (`tests/test_{frontmatter,slugs,links}.py`), plus a new
209
+ `tests/test_incremental.py` for the newly-generic driver (the first
210
+ seed's own incremental test stayed in place there, since it still
211
+ exercises source-specific behavior through the shared driver).
212
+ 5. A `specs/` feature spec via `/speckit-specify` was **not** written for
213
+ this extraction — it happened as part of a larger, time-boxed
214
+ migration task rather than dgconvert's own spec-driven workflow.
215
+ Worth backfilling if dgconvert's process requires it retroactively.
216
+ 6. ~~Extract `apply_body_snatchers` into `dgconvert.body_snatchers`,
217
+ sourced from `docufig.seed.yaml` directly~~ — done, once a second,
218
+ independent consumer needed the identical shape a first seed already
219
+ had. Went one step further than the other extractions: rather than
220
+ just deduplicating the apply function, `load()` reads a seed's
221
+ `bodySnatchers` directly from its already-schema-validated
222
+ `docufig.seed.yaml`, eliminating a real hand-sync bug class (a
223
+ seed's Python-side transform table drifting out of sync with its own
224
+ manifest, seen in practice) instead of just moving where the
225
+ duplication lived.
226
+
227
+ ## Open questions
228
+
229
+ 1. ~~Exact package/module names for the extracted pieces~~ — settled:
230
+ `dgconvert.frontmatter`, `dgconvert.slugs`, `dgconvert.links`,
231
+ `dgconvert.dgpkm`, `dgconvert.incremental`, `dgconvert.body_snatchers`.
232
+ 2. Whether `dgconvert.dgpkm`'s binary-discovery fallback chain should be
233
+ configurable per-project or hardcoded to the `DGPKM_BIN` → `PATH` →
234
+ bundled order.
235
+ 3. Whether the incremental-diff pattern (state-in-frontmatter,
236
+ create/skip/update/delete) generalizes cleanly to a source that isn't
237
+ a flat file tree (e.g. a paginated API), or needs a second, distinct
238
+ pattern for that case — genuinely unknown until a seed of that shape
239
+ exists.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ScriptAutomate
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.5
2
+ Name: dgconvert
3
+ Version: 0.1.0
4
+ Summary: Helper library for building dgpkm-compatible seeds (frontmatter, slugs, link rewriting, dgpkm doctor verification)
5
+ Project-URL: Homepage, https://github.com/ScriptAutomate/dgconvert
6
+ Project-URL: Repository, https://github.com/ScriptAutomate/dgconvert
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Python: >=3.12
15
+ Requires-Dist: pyyaml>=6
16
+ Description-Content-Type: text/markdown
17
+
18
+ # dgconvert
19
+
20
+ A small Python **helper library** for building
21
+ [dgpkm](https://github.com/ScriptAutomate/dgpkm)-compatible **seeds**
22
+ (pre-created gardens, ready to plant into a greenhouse).
23
+
24
+ `dgconvert` is not a task runner. Each seed repo owns its own
25
+ `docufig.seed.json` and its own plain-Python conversion script;
26
+ `dgconvert` supplies the pieces every seed needs regardless of source
27
+ format — dotted-slug file naming, single-line frontmatter with stable
28
+ ids, markdown-link → dgpkm-wikilink rewriting, and a wrapper that
29
+ verifies output with `dgpkm doctor`.
30
+
31
+ See `DESIGN.md` for the full architecture and the verified
32
+ dgpkm-compatibility constraints it's built against. The library was
33
+ generalized out of a first seed's own inline conversion code once a
34
+ second, independent seed needed the same pieces.
35
+
36
+ ## Installing
37
+
38
+ ```bash
39
+ uv add dgconvert
40
+ # or
41
+ pip install dgconvert
42
+ ```
43
+
44
+ ## Development
45
+
46
+ Managed with [uv](https://docs.astral.sh/uv/) and [just](https://just.systems/),
47
+ using [Spec Kit](https://github.com/github/spec-kit) for feature specs
48
+ (`specs/`, `.specify/`) — unlike seed repos, which don't use Spec Kit:
49
+
50
+ ```bash
51
+ just # list tasks
52
+ just sync # uv sync
53
+ just test # uv run pytest
54
+ just check # format + lint
55
+ ```
56
+
57
+ Container-based development mirrors dgpkm: set `USE_CONTAINER_DEV` to
58
+ `podman`, `docker`, or `container`, then `just build-devcontainer` once and
59
+ every build/test recipe runs inside the dev container automatically.
60
+
61
+ ## License
62
+
63
+ [MIT](LICENSE)
@@ -0,0 +1,46 @@
1
+ # dgconvert
2
+
3
+ A small Python **helper library** for building
4
+ [dgpkm](https://github.com/ScriptAutomate/dgpkm)-compatible **seeds**
5
+ (pre-created gardens, ready to plant into a greenhouse).
6
+
7
+ `dgconvert` is not a task runner. Each seed repo owns its own
8
+ `docufig.seed.json` and its own plain-Python conversion script;
9
+ `dgconvert` supplies the pieces every seed needs regardless of source
10
+ format — dotted-slug file naming, single-line frontmatter with stable
11
+ ids, markdown-link → dgpkm-wikilink rewriting, and a wrapper that
12
+ verifies output with `dgpkm doctor`.
13
+
14
+ See `DESIGN.md` for the full architecture and the verified
15
+ dgpkm-compatibility constraints it's built against. The library was
16
+ generalized out of a first seed's own inline conversion code once a
17
+ second, independent seed needed the same pieces.
18
+
19
+ ## Installing
20
+
21
+ ```bash
22
+ uv add dgconvert
23
+ # or
24
+ pip install dgconvert
25
+ ```
26
+
27
+ ## Development
28
+
29
+ Managed with [uv](https://docs.astral.sh/uv/) and [just](https://just.systems/),
30
+ using [Spec Kit](https://github.com/github/spec-kit) for feature specs
31
+ (`specs/`, `.specify/`) — unlike seed repos, which don't use Spec Kit:
32
+
33
+ ```bash
34
+ just # list tasks
35
+ just sync # uv sync
36
+ just test # uv run pytest
37
+ just check # format + lint
38
+ ```
39
+
40
+ Container-based development mirrors dgpkm: set `USE_CONTAINER_DEV` to
41
+ `podman`, `docker`, or `container`, then `just build-devcontainer` once and
42
+ every build/test recipe runs inside the dev container automatically.
43
+
44
+ ## License
45
+
46
+ [MIT](LICENSE)
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "dgconvert"
3
+ version = "0.1.0"
4
+ description = "Helper library for building dgpkm-compatible seeds (frontmatter, slugs, link rewriting, dgpkm doctor verification)"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ classifiers = [
9
+ "Development Status :: 3 - Alpha",
10
+ "License :: OSI Approved :: MIT License",
11
+ "Programming Language :: Python :: 3",
12
+ "Programming Language :: Python :: 3.12",
13
+ "Programming Language :: Python :: 3.13",
14
+ ]
15
+ dependencies = [
16
+ "pyyaml>=6",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/ScriptAutomate/dgconvert"
21
+ Repository = "https://github.com/ScriptAutomate/dgconvert"
22
+
23
+ [dependency-groups]
24
+ dev = [
25
+ "pytest>=8",
26
+ "ruff>=0.8",
27
+ ]
28
+
29
+ [build-system]
30
+ requires = ["hatchling"]
31
+ build-backend = "hatchling.build"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/dgconvert"]
35
+
36
+ [tool.hatch.build.targets.sdist]
37
+ # Hatchling's sdist default is "everything git-tracks" -- excludes the
38
+ # repo's own dev/agent tooling (Claude Code skills, Spec Kit scaffolding,
39
+ # devcontainer, justfile) that a consumer installing this library from
40
+ # PyPI has no use for. The wheel (tool.hatch.build.targets.wheel above)
41
+ # was already correctly scoped to just src/dgconvert/ without any of
42
+ # this needing to be said twice.
43
+ exclude = [
44
+ ".claude",
45
+ ".specify",
46
+ ".devcontainer",
47
+ ".justfile",
48
+ "cog.toml",
49
+ ]
50
+
51
+ [tool.ruff]
52
+ src = ["src", "tests"]
53
+ line-length = 100
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests"]
@@ -0,0 +1,5 @@
1
+ """dgconvert — a helper library for building dgpkm-compatible seeds."""
2
+
3
+ from importlib.metadata import version as _version
4
+
5
+ __version__ = _version("dgconvert")
@@ -0,0 +1,39 @@
1
+ """Body-snatcher (find/replace) content transforms, sourced from a
2
+ seed's own docufig.seed.yaml.
3
+
4
+ `bodySnatchers` is a required field in dgpkm's own seed config JSON
5
+ Schema (validated by `dgpkm doctor schema seed`), not a decorative
6
+ one -- so it's read directly from that already-schema-validated
7
+ manifest rather than requiring a seed's own Python code to keep a
8
+ second, hand-maintained copy in sync (a real, seen-in-practice failure
9
+ mode: a seed's manifest drifting out of sync with its own Python-side
10
+ transform table after a code-only change, caught only by a direct
11
+ question, not by any tooling).
12
+
13
+ Generalized out of two seeds' identical apply_body_snatchers once a
14
+ second seed needed the exact same shape a first one already had --
15
+ the same "second consumer" trigger this library's other modules were
16
+ each generalized under.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from pathlib import Path
22
+
23
+ import yaml
24
+
25
+
26
+ def load(seed_config_path: Path) -> tuple[tuple[str, str], ...]:
27
+ """Reads bodySnatchers from a seed's docufig.seed.yaml. Each entry's
28
+ {find, replace} shape matches dgpkm's own seed config JSON Schema
29
+ exactly. A missing bodySnatchers key (defensive -- the schema
30
+ requires it, but this shouldn't hard-fail a caller that hasn't
31
+ validated yet) behaves like an empty list."""
32
+ data = yaml.safe_load(seed_config_path.read_text(encoding="utf-8")) or {}
33
+ return tuple((s["find"], s["replace"]) for s in data.get("bodySnatchers", []))
34
+
35
+
36
+ def apply(text: str, snatchers: tuple[tuple[str, str], ...]) -> str:
37
+ for find, replace in snatchers:
38
+ text = text.replace(find, replace)
39
+ return text
@@ -0,0 +1,62 @@
1
+ """Wraps the `dgpkm` binary for verification.
2
+
3
+ `dgpkm doctor backlinks` and `dgpkm doctor schema <type>` always exit 0,
4
+ even when they report findings -- findings are only visible in
5
+ stdout/stderr text ("Found N broken link(s)", "✗ ..."). This module
6
+ parses that text so the converter can fail loudly (non-zero exit) when a
7
+ conversion isn't doctor-clean, which dgpkm itself won't do for you.
8
+
9
+ Generalized beyond its original prototype in one way: `verify`'s schema
10
+ check is now optional (`check_schema=None` skips it) since not every
11
+ consumer has a `docufig.seed.json` to validate -- a standalone
12
+ converter tool can write straight into a greenhouse's root garden,
13
+ not a separate seed.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import subprocess
20
+ from pathlib import Path
21
+
22
+
23
+ def dgpkm_bin() -> str:
24
+ return os.environ.get("DGPKM_BIN", "dgpkm")
25
+
26
+
27
+ def _run(args: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
28
+ return subprocess.run(
29
+ [dgpkm_bin(), *args],
30
+ cwd=cwd,
31
+ capture_output=True,
32
+ text=True,
33
+ check=False,
34
+ )
35
+
36
+
37
+ def verify(root: Path, *, check_schema: str | None = "seed") -> list[str]:
38
+ """Run dgpkm doctor checks against the garden at `root`. Returns a list
39
+ of human-readable problem descriptions (empty = clean).
40
+
41
+ `check_schema` names the config type to pass to `dgpkm doctor schema
42
+ <type>` (e.g. "seed" for a `docufig.seed.json`); pass None to skip
43
+ that check entirely for a tool that has no such config file.
44
+ """
45
+ problems: list[str] = []
46
+
47
+ backlinks = _run(["doctor", "backlinks"], root)
48
+ if backlinks.returncode != 0:
49
+ problems.append(f"dgpkm doctor backlinks failed to run:\n{backlinks.stderr}")
50
+ elif "All backlinks are valid" not in backlinks.stdout:
51
+ problems.append(f"backlinks: broken links found\n{backlinks.stdout}")
52
+
53
+ if check_schema:
54
+ schema = _run(["doctor", "schema", check_schema], root)
55
+ if schema.returncode != 0:
56
+ problems.append(f"dgpkm doctor schema {check_schema} failed to run:\n{schema.stderr}")
57
+ elif "✓" not in schema.stdout or "✗" in schema.stderr:
58
+ problems.append(
59
+ f"{check_schema} config schema invalid:\n{schema.stdout}{schema.stderr}"
60
+ )
61
+
62
+ return problems