relcon-docs 0.1.0__py3-none-any.whl

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 @@
1
+ """Shared documentation configuration for Relaxed Constraints projects."""
@@ -0,0 +1,63 @@
1
+ # Managed by relcon-docs configure. Do not edit this file.
2
+ # Put project overrides in mkdocs.yml. Paths are relative to that file.
3
+ extra_css:
4
+ - stylesheets/relcon-docs.css
5
+ extra_javascript:
6
+ - javascripts/relcon-docs-math.js
7
+ theme:
8
+ variant: modern
9
+ font: false
10
+ features:
11
+ - content.action.copy
12
+ - content.tabs.link
13
+ - content.code.copy
14
+ - navigation.tabs
15
+ - navigation.top
16
+ - search.highlight
17
+ palette:
18
+ - media: (prefers-color-scheme)
19
+ toggle:
20
+ icon: lucide/sun-moon
21
+ name: Switch to light mode
22
+ - media: '(prefers-color-scheme: light)'
23
+ scheme: default
24
+ toggle:
25
+ icon: lucide/sun
26
+ name: Switch to dark mode
27
+ - media: '(prefers-color-scheme: dark)'
28
+ scheme: slate
29
+ toggle:
30
+ icon: lucide/moon
31
+ name: Switch to system preference
32
+ markdown_extensions:
33
+ toc:
34
+ permalink: true
35
+ permalink_title: Link to this section
36
+ pymdownx.arithmatex:
37
+ generic: true
38
+ admonition: {}
39
+ pymdownx.details: {}
40
+ pymdownx.highlight: {}
41
+ pymdownx.superfences:
42
+ custom_fences:
43
+ - name: mermaid
44
+ class: mermaid
45
+ format: pymdownx.superfences.fence_code_format
46
+ pymdownx.tabbed:
47
+ alternate_style: true
48
+ plugins:
49
+ llmstxt:
50
+ sections:
51
+ Documentation: ["*.md"]
52
+ search: {}
53
+ mkdocstrings:
54
+ handlers:
55
+ python:
56
+ options:
57
+ docstring_style: google
58
+ members_order: alphabetical
59
+ filters: ["!^_"]
60
+ show_signature_annotations: true
61
+ separate_signature: true
62
+ signature_crossrefs: true
63
+ line_length: 76
@@ -0,0 +1,23 @@
1
+ # Project-owned workflow: configure creates this once and never overwrites it.
2
+ name: Deploy documentation
3
+
4
+ on:
5
+ workflow_dispatch: # Allow manual deployment.
6
+
7
+ # Uncomment to deploy on pushes to main (for example, consumer projects):
8
+ # push:
9
+ # branches: [main]
10
+
11
+ # Or uncomment to deploy on published releases (for example, BONDI):
12
+ # release:
13
+ # types: [published]
14
+
15
+ permissions:
16
+ contents: read
17
+ pages: write
18
+ id-token: write
19
+
20
+ jobs:
21
+ docs:
22
+ # Replace @main with a reviewed commit SHA to pin the deployment implementation.
23
+ uses: relaxed-constraints/relcon-docs/.github/workflows/deploy-docs.yml@main
@@ -0,0 +1,34 @@
1
+ /* Load the pinned renderer only when a page contains mathematical notation. */
2
+ (() => {
3
+ let engine;
4
+ function renderMath() {
5
+ if (!document.querySelector('.arithmatex')) return;
6
+ if (!engine) {
7
+ engine = new Promise((resolve, reject) => {
8
+ window.MathJax = {
9
+ tex: {
10
+ inlineMath: [['\\(', '\\)']],
11
+ displayMath: [['\\[', '\\]']],
12
+ processEscapes: true,
13
+ processEnvironments: true,
14
+ },
15
+ options: { ignoreHtmlClass: '.*|', processHtmlClass: 'arithmatex' },
16
+ startup: { typeset: false },
17
+ };
18
+ const script = document.createElement('script');
19
+ script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-mml-chtml.js';
20
+ script.onload = () => resolve(window.MathJax.startup.promise);
21
+ script.onerror = () => reject(new Error('Could not load the MathJax renderer.'));
22
+ document.head.appendChild(script);
23
+ });
24
+ }
25
+ engine = engine.then(() => window.MathJax.typesetPromise()).catch(console.error);
26
+ }
27
+ if (typeof document$ !== 'undefined') {
28
+ document$.subscribe(renderMath);
29
+ } else if (document.readyState === 'loading') {
30
+ document.addEventListener('DOMContentLoaded', renderMath);
31
+ } else {
32
+ renderMath();
33
+ }
34
+ })();
@@ -0,0 +1,111 @@
1
+ /* Button colours are paired for readable labels and visible focus in each theme. */
2
+ :root,
3
+ [data-md-color-scheme="default"] {
4
+ --relcon-copy-bg: #ffffff;
5
+ --relcon-copy-hover: #edf1f5;
6
+ --relcon-copy-border: #7a8796;
7
+ --relcon-copy-text: #263445;
8
+ }
9
+
10
+ [data-md-color-scheme="slate"] {
11
+ --relcon-copy-bg: #222b38;
12
+ --relcon-copy-hover: #303e50;
13
+ --relcon-copy-border: #8193aa;
14
+ --relcon-copy-text: #edf2f8;
15
+ }
16
+
17
+ .md-typeset button[data-md-copy-url] {
18
+ display: inline-flex;
19
+ align-items: center;
20
+ gap: 0.4em;
21
+ padding: 0.35em 0.65em;
22
+ background-color: var(--relcon-copy-bg);
23
+ border: 1px solid var(--relcon-copy-border);
24
+ color: var(--relcon-copy-text);
25
+ border-radius: 0.25rem;
26
+ font-size: 0.65rem;
27
+ line-height: 1.3;
28
+ }
29
+
30
+ .md-typeset button[data-md-copy-url]::after {
31
+ content: attr(aria-label);
32
+ font-weight: 400;
33
+ }
34
+
35
+ .md-typeset button[data-md-copy-url]:focus-visible {
36
+ outline: 2px solid currentColor;
37
+ outline-offset: 2px;
38
+ }
39
+
40
+ /* Diagram roles inherit into Zensical's Mermaid shadow roots. */
41
+ :root,
42
+ [data-md-color-scheme="default"] {
43
+ --relcon-diagram-frame-bg: #f5f7fa;
44
+ --relcon-diagram-frame-border: #758399;
45
+ --relcon-diagram-node-bg: #e8eff7;
46
+ --relcon-diagram-node-border: #59718f;
47
+ --relcon-diagram-text: #223248;
48
+ --relcon-diagram-edge: #526780;
49
+ --relcon-diagram-accent-bg: #fff0cc;
50
+ --relcon-diagram-accent-border: #a56a12;
51
+ --relcon-diagram-accent-text: #623a00;
52
+ }
53
+
54
+ [data-md-color-scheme="slate"] {
55
+ --relcon-diagram-frame-bg: #202a38;
56
+ --relcon-diagram-frame-border: #778ba4;
57
+ --relcon-diagram-node-bg: #293c53;
58
+ --relcon-diagram-node-border: #8eaacb;
59
+ --relcon-diagram-text: #eef3f9;
60
+ --relcon-diagram-edge: #94a9c2;
61
+ --relcon-diagram-accent-bg: #513b18;
62
+ --relcon-diagram-accent-border: #e6b65c;
63
+ --relcon-diagram-accent-text: #fff0cd;
64
+ }
65
+
66
+ :root,
67
+ [data-md-color-scheme] {
68
+ /* Native flowcharts need no per-diagram style declarations. */
69
+ --md-mermaid-node-bg-color: var(--relcon-diagram-node-bg);
70
+ --md-mermaid-node-fg-color: var(--relcon-diagram-node-border);
71
+ --md-mermaid-label-fg-color: var(--relcon-diagram-text);
72
+ --md-mermaid-label-bg-color: var(--md-default-bg-color);
73
+ --md-mermaid-edge-color: var(--relcon-diagram-edge);
74
+ --md-mermaid-sequence-actor-bg-color: var(--relcon-diagram-node-bg);
75
+ --md-mermaid-sequence-actor-border-color: var(--relcon-diagram-node-border);
76
+ --md-mermaid-sequence-actor-fg-color: var(--relcon-diagram-text);
77
+ --md-mermaid-sequence-actor-line-color: var(--relcon-diagram-edge);
78
+ --md-mermaid-sequence-message-fg-color: var(--relcon-diagram-text);
79
+ --md-mermaid-sequence-message-line-color: var(--relcon-diagram-edge);
80
+ --md-mermaid-sequence-note-bg-color: var(--relcon-diagram-frame-bg);
81
+ --md-mermaid-sequence-note-border-color: var(--relcon-diagram-frame-border);
82
+ --md-mermaid-sequence-note-fg-color: var(--relcon-diagram-text);
83
+
84
+ /* Compatibility with diagrams authored before relcon-docs. */
85
+ --jb-frame-bg: var(--relcon-diagram-frame-bg);
86
+ --jb-frame-border: var(--relcon-diagram-frame-border);
87
+ --jb-node-bg: var(--relcon-diagram-node-bg);
88
+ --jb-node-border: var(--relcon-diagram-node-border);
89
+ --jb-diagram-text: var(--relcon-diagram-text);
90
+ }
91
+
92
+ .md-typeset button[data-md-copy-url]:hover {
93
+ background-color: var(--relcon-copy-hover);
94
+ }
95
+
96
+ .md-typeset figure {
97
+ margin: 1.5em 0;
98
+ }
99
+
100
+ .md-typeset figure img {
101
+ display: block;
102
+ max-width: 100%;
103
+ height: auto;
104
+ margin-inline: auto;
105
+ }
106
+
107
+ .md-typeset figcaption {
108
+ margin-top: 0.75em;
109
+ color: var(--md-default-fg-color--light);
110
+ text-align: center;
111
+ }
relcon_docs/cli.py ADDED
@@ -0,0 +1,243 @@
1
+ """Apply packaged defaults without rewriting project-owned configuration."""
2
+
3
+ import argparse
4
+ import difflib
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import sys
9
+ import tempfile
10
+ from importlib.metadata import version
11
+ from importlib.resources import files
12
+ from pathlib import Path
13
+
14
+ import yaml
15
+
16
+ BASE = ".relcon-docs/base.yml"
17
+ STATE = ".relcon-docs/state.json"
18
+ CSS = "docs/stylesheets/relcon-docs.css"
19
+ WORKFLOW = ".github/workflows/docs.yml"
20
+ MATH = "docs/javascripts/relcon-docs-math.js"
21
+
22
+
23
+ class ConfigurationError(Exception):
24
+ """A configuration cannot safely be applied."""
25
+
26
+
27
+ def digest(content: bytes) -> str:
28
+ return hashlib.sha256(content).hexdigest()
29
+
30
+
31
+ def managed_assets() -> dict[str, bytes]:
32
+ assets = files("relcon_docs").joinpath("assets")
33
+ return {
34
+ BASE: assets.joinpath("base.yml").read_bytes(),
35
+ CSS: assets.joinpath("relcon-docs.css").read_bytes(),
36
+ MATH: assets.joinpath("relcon-docs-math.js").read_bytes(),
37
+ }
38
+
39
+
40
+ def safe_path(root: Path, relative: str) -> Path:
41
+ """Reject symlinks and non-directory parents before reading or writing."""
42
+ path = root
43
+ for part in Path(relative).parts:
44
+ path = path / part
45
+ if path.is_symlink():
46
+ raise ConfigurationError(f"Refusing symlink: {path}")
47
+ if path != root / relative and path.exists() and not path.is_dir():
48
+ raise ConfigurationError(f"Expected a directory: {path}")
49
+ if path.exists() and not path.is_file():
50
+ raise ConfigurationError(f"Expected a file: {path}")
51
+ return path
52
+
53
+
54
+ def read_state(path: Path, assets: dict[str, bytes]) -> dict[str, str]:
55
+ if not path.exists():
56
+ return {}
57
+ try:
58
+ state = json.loads(path.read_text())
59
+ hashes = state["files"]
60
+ if (
61
+ state["schema"] != 1
62
+ or not isinstance(hashes, dict)
63
+ or set(hashes) not in ({BASE, CSS}, set(assets))
64
+ or not all(
65
+ isinstance(value, str)
66
+ and len(value) == 64
67
+ and all(char in "0123456789abcdef" for char in value)
68
+ for value in hashes.values()
69
+ )
70
+ ):
71
+ raise ValueError
72
+ except (ValueError, KeyError, TypeError) as exc:
73
+ raise ConfigurationError(
74
+ f"Invalid state file: {path}. Restore it from Git before configuring."
75
+ ) from exc
76
+ return hashes
77
+
78
+
79
+ def project_config(root: Path) -> bytes | None:
80
+ """Validate an existing project or prepare a starter, without editing it."""
81
+ if (root / "zensical.toml").exists():
82
+ raise ConfigurationError("Convert zensical.toml to mkdocs.yml first; it takes precedence.")
83
+ if (root / "mkdocs.yaml").exists():
84
+ raise ConfigurationError("Rename mkdocs.yaml to mkdocs.yml first.")
85
+ path = safe_path(root, "mkdocs.yml")
86
+ if not path.exists():
87
+ return (
88
+ "# Project-owned configuration. Override shared defaults here.\n"
89
+ "# Replace the local site_url with your published URL before deployment.\n"
90
+ f"INHERIT: {BASE}\n"
91
+ + yaml.safe_dump(
92
+ {
93
+ "site_name": root.name,
94
+ "site_url": "http://127.0.0.1:8000/",
95
+ },
96
+ sort_keys=False,
97
+ )
98
+ ).encode()
99
+
100
+ # Compose rather than construct: existing MkDocs configurations may use
101
+ # !ENV and Python-name tags, which we must neither execute nor rewrite.
102
+ try:
103
+ config = yaml.compose(path.read_text())
104
+ if not isinstance(config, yaml.MappingNode):
105
+ raise ValueError
106
+ settings = {}
107
+ for key, value in config.value:
108
+ if not isinstance(key, yaml.ScalarNode) or key.value in settings:
109
+ raise ValueError
110
+ settings[key.value] = value
111
+ inherit = settings.get("INHERIT")
112
+ if not isinstance(inherit, yaml.ScalarNode) or inherit.value != BASE:
113
+ raise ConfigurationError(f"Set INHERIT: {BASE} in mkdocs.yml first.")
114
+ docs_dir = settings.get("docs_dir")
115
+ if docs_dir is not None and (
116
+ not isinstance(docs_dir, yaml.ScalarNode) or docs_dir.value not in ("docs", "./docs")
117
+ ):
118
+ raise ConfigurationError("This version supports only docs_dir: docs.")
119
+ except (yaml.YAMLError, ValueError) as exc:
120
+ raise ConfigurationError("mkdocs.yml must be a YAML mapping with unique keys.") from exc
121
+ return None
122
+
123
+
124
+ def atomic_write(path: Path, content: bytes) -> None:
125
+ path.parent.mkdir(parents=True, exist_ok=True)
126
+ temporary = None
127
+ try:
128
+ with tempfile.NamedTemporaryFile(dir=path.parent, delete=False) as stream:
129
+ temporary = Path(stream.name)
130
+ stream.write(content)
131
+ os.chmod(temporary, 0o644)
132
+ os.replace(temporary, path)
133
+ finally:
134
+ if temporary is not None:
135
+ temporary.unlink(missing_ok=True)
136
+
137
+
138
+ def configure(root: Path, *, dry_run: bool, check: bool, force: bool) -> int:
139
+ if not root.is_dir():
140
+ raise ConfigurationError(f"Project directory does not exist: {root}")
141
+ assets = managed_assets()
142
+ paths = {name: safe_path(root, name) for name in [*assets, STATE]}
143
+ baseline = read_state(paths[STATE], assets)
144
+ starter = project_config(root)
145
+ workflow_path = safe_path(root, WORKFLOW)
146
+ changes = {}
147
+ conflicts = []
148
+
149
+ for name, desired in assets.items():
150
+ path = paths[name]
151
+ current = path.read_bytes() if path.exists() else None
152
+ if current == desired:
153
+ continue
154
+ changes[name] = desired
155
+ if name in baseline:
156
+ if current is None or digest(current) != baseline[name]:
157
+ conflicts.append(name)
158
+ elif current is not None:
159
+ conflicts.append(name)
160
+
161
+ if starter is not None:
162
+ changes["mkdocs.yml"] = starter
163
+ if not workflow_path.exists():
164
+ changes[WORKFLOW] = files("relcon_docs").joinpath("assets/docs.yml").read_bytes()
165
+ state = {
166
+ "schema": 1,
167
+ "package_version": version("relcon-docs"),
168
+ "files": {name: digest(content) for name, content in assets.items()},
169
+ }
170
+ state_bytes = (json.dumps(state, indent=2, sort_keys=True) + "\n").encode()
171
+ if not paths[STATE].exists() or paths[STATE].read_bytes() != state_bytes:
172
+ changes[STATE] = state_bytes
173
+
174
+ for name in changes:
175
+ print(f"Update {name}" if (root / name).exists() else f"Create {name}")
176
+ if dry_run:
177
+ path = root / name
178
+ before = path.read_bytes() if path.exists() else b""
179
+ print(
180
+ "".join(
181
+ difflib.unified_diff(
182
+ before.decode(errors="replace").splitlines(keepends=True),
183
+ changes[name].decode().splitlines(keepends=True),
184
+ fromfile=f"a/{name}",
185
+ tofile=f"b/{name}",
186
+ )
187
+ ),
188
+ end="",
189
+ )
190
+
191
+ if conflicts:
192
+ print("Managed-file conflicts: " + ", ".join(conflicts), file=sys.stderr)
193
+ if not force:
194
+ print(
195
+ "No files written. Move customizations to mkdocs.yml or a separate CSS file; "
196
+ "use --force to replace managed files.",
197
+ file=sys.stderr,
198
+ )
199
+ return 2
200
+ print("Explicit --force: package contents will replace these managed files.")
201
+
202
+ if check:
203
+ if not changes:
204
+ print("Configuration is current.")
205
+ return 1 if changes else 0
206
+ if dry_run:
207
+ print("Dry run: no files written.")
208
+ return 0
209
+
210
+ # Validate all destinations before writing anything. Each file replacement
211
+ # is atomic; the state is written last so an interrupted run can be retried.
212
+ for name in changes:
213
+ safe_path(root, name)
214
+ for name, content in changes.items():
215
+ atomic_write(root / name, content)
216
+ print("Configuration updated." if changes else "Configuration is current.")
217
+ return 0
218
+
219
+
220
+ def main(argv: list[str] | None = None) -> int:
221
+ parser = argparse.ArgumentParser(description=__doc__)
222
+ parser.add_argument("--version", action="version", version=version("relcon-docs"))
223
+ commands = parser.add_subparsers(dest="command", required=True)
224
+ command = commands.add_parser("configure", help="Apply shared documentation defaults")
225
+ command.add_argument(
226
+ "--project", type=Path, default=Path.cwd(), help="Project root (default: cwd)"
227
+ )
228
+ mode = command.add_mutually_exclusive_group()
229
+ mode.add_argument("--dry-run", action="store_true", help="Show diffs without writing")
230
+ mode.add_argument(
231
+ "--check", action="store_true", help="Exit 1 if updates are needed; do not write"
232
+ )
233
+ command.add_argument("--force", action="store_true", help="Replace conflicting managed files")
234
+ args = parser.parse_args(argv)
235
+ if args.check and args.force:
236
+ parser.error("--check cannot be combined with --force")
237
+ try:
238
+ return configure(
239
+ args.project.resolve(), dry_run=args.dry_run, check=args.check, force=args.force
240
+ )
241
+ except (ConfigurationError, OSError) as exc:
242
+ print(f"relcon-docs: {exc}", file=sys.stderr)
243
+ return 2
@@ -0,0 +1,262 @@
1
+ Metadata-Version: 2.5
2
+ Name: relcon-docs
3
+ Version: 0.1.0
4
+ Summary: Shared documentation defaults, styling, and configuration tools for Zensical
5
+ Project-URL: Homepage, https://relaxed-constraints.github.io/relcon-docs/
6
+ Project-URL: Repository, https://github.com/relaxed-constraints/relcon-docs
7
+ Project-URL: Issues, https://github.com/relaxed-constraints/relcon-docs/issues
8
+ Author: Relaxed Constraints
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: mkdocstrings-python>=2.0.10
13
+ Requires-Dist: pyyaml<7,>=6
14
+ Requires-Dist: ruff>=0.9
15
+ Requires-Dist: zensical<0.2,>=0.1.1
16
+ Description-Content-Type: text/markdown
17
+
18
+ # relcon-docs
19
+
20
+ Shared documentation defaults, styling, and configuration tools for Relaxed Constraints
21
+ projects using Zensical.
22
+
23
+ Install this package as a **documentation dependency**. It supplies Zensical, the
24
+ Python API handler and formatter, shared modern-theme settings, and copy-button CSS.
25
+ It has no dependency on BONDI or Yanex.
26
+
27
+ ## Install
28
+
29
+ ```sh
30
+ uv add --group docs relcon-docs
31
+ ```
32
+
33
+ Or install with `python -m pip install relcon-docs`. Keep uv.lock committed for
34
+ reproducible builds. Upgrade with `uv lock --upgrade-package relcon-docs`, sync
35
+ the docs group, and run `relcon-docs configure` to refresh shared assets.
36
+
37
+ ## Configure a project
38
+
39
+ Once installed in your project's environment:
40
+
41
+ ```sh
42
+ relcon-docs configure --dry-run
43
+ relcon-docs configure
44
+ zensical serve
45
+ ```
46
+
47
+ The project directory must already exist. A fresh project gets a minimal
48
+ `mkdocs.yml`; add your Markdown pages under `docs/` and edit navigation as needed.
49
+ The starter uses a localhost `site_url` for previews. Replace it with the published
50
+ site URL before deployment; Markdown exports require this setting.
51
+ For an existing YAML project, add `INHERIT: .relcon-docs/base.yml` first.
52
+ Convert and remove any `zensical.toml` before configuring: Zensical prefers it over YAML.
53
+
54
+ | File | Owner |
55
+ | --- | --- |
56
+ | `.github/workflows/docs.yml` | Project: deployment triggers and reusable workflow revision |
57
+ | `mkdocs.yml` | Project: metadata, navigation, API paths, Markdown-export sections and overrides |
58
+ | `.relcon-docs/base.yml` | Package: theme, Markdown extensions and API-rendering defaults |
59
+ | `docs/stylesheets/relcon-docs.css` | Package: copy buttons, diagrams and figures |
60
+ | `docs/javascripts/relcon-docs-math.js` | Package: on-demand MathJax rendering |
61
+ | `.relcon-docs/state.json` | Tool: applied version and hashes for detecting manual edits |
62
+
63
+ Commit these files. Configuration is explicit: installing or upgrading the
64
+ dependency never rewrites project files. Builds and previews use ordinary
65
+ `zensical` commands, without a wrapper or sibling checkout.
66
+
67
+ ## Upgrade and review
68
+
69
+ Upgrade the dependency and lockfile, then run:
70
+
71
+ ```sh
72
+ relcon-docs configure --dry-run
73
+ relcon-docs configure
74
+ zensical build --strict
75
+ ```
76
+
77
+ Unmodified managed files are updated. A changed or deleted managed file produces a
78
+ conflict and **no files are written**. On first adoption, an existing managed path
79
+ with different contents also conflicts. Identical contents can be adopted safely.
80
+
81
+ Move customizations into project overrides or a separate stylesheet. To explicitly
82
+ replace conflicting managed files, use `configure --force`; inspect the proposed
83
+ changes first with `configure --dry-run --force`. Force never rewrites an existing
84
+ `mkdocs.yml` or `.github/workflows/docs.yml`. Invalid state files must be restored from Git; force does not bypass
85
+ state validation or symlink protection.
86
+
87
+ Each file replacement is atomic; the full set is not a filesystem transaction.
88
+ State is written last, and an interrupted run can be retried.
89
+
90
+ ## Project overrides
91
+
92
+ Mappings merge recursively. Lists replace inherited lists, so repeat the shared
93
+ entries when overriding `theme.features` or `extra_css`. Use mapping syntax for
94
+ plugins and Markdown extensions to preserve unrelated inherited defaults:
95
+
96
+ ```yaml
97
+ INHERIT: .relcon-docs/base.yml
98
+ site_name: My research project
99
+ site_url: https://example.org/my-project/
100
+ plugins:
101
+ mkdocstrings:
102
+ handlers:
103
+ python:
104
+ paths: [src]
105
+ llmstxt:
106
+ sections:
107
+ Documentation: ["*.md"]
108
+ API reference: ["reference/**/*.md"]
109
+ ```
110
+
111
+ Markdown exports and `llms.txt` are enabled in the shared defaults for all
112
+ Markdown pages, including nested pages. Existing projects inherit them without
113
+ adding a local plugin configuration. A valid project `site_url` is required.
114
+
115
+ The shared index uses a `Documentation` section. Section mappings merge during
116
+ inheritance: override `Documentation` to change its page patterns; adding a
117
+ different section name retains the inherited section too. Most projects can omit
118
+ `llmstxt` entirely. Set `plugins.llmstxt.enabled: false` to opt out.
119
+
120
+ This first version supports the standard `docs/` source directory and direct
121
+ inheritance from `.relcon-docs/base.yml`. It does not migrate arbitrary configurations,
122
+ prune old stylesheets, or modify existing workflow files.
123
+
124
+ ## Callouts, collapsible notes and diagrams
125
+
126
+ Shared defaults enable `admonition`, `pymdownx.details` and Mermaid fenced
127
+ blocks. Projects do not need local extension configuration for these features.
128
+ For example:
129
+
130
+ ````markdown
131
+ !!! note "Research note"
132
+
133
+ Record assumptions beside your results.
134
+
135
+ ??? tip "Show details"
136
+
137
+ Put optional explanations here.
138
+
139
+ ```mermaid
140
+ graph LR
141
+ Data --> Experiment --> Results
142
+ ```
143
+ ````
144
+
145
+ Zensical initializes Mermaid on pages containing diagrams. Shared light/dark colours apply to standard flowchart nodes, labels and connectors,
146
+ and sequence actors, messages and notes. Custom diagrams can use the
147
+ `--relcon-diagram-*` CSS properties; legacy `--jb-*` aliases remain supported.
148
+ See the [diagram examples](https://relaxed-constraints.github.io/relcon-docs/diagrams/).
149
+ Project-specific overrides can remain in a separate local stylesheet.
150
+ If overriding `custom_fences`, include the Mermaid entry too: lists replace
151
+ inherited lists.
152
+
153
+ ## GitHub Pages deployment
154
+
155
+ `configure` creates `.github/workflows/docs.yml` **only when absent**. The starter
156
+ enables manual deployment (`workflow_dispatch`) and includes commented examples
157
+ for pushes to `main` and published releases. After creation the file belongs to the
158
+ project: rerunning configure, including with `--force`, leaves it byte-for-byte
159
+ unchanged. It is not included in the managed-file hashes. A missing starter is
160
+ reported by `--check` and shown by `--dry-run`; ordinary configure recreates it.
161
+
162
+ The starter calls the shared
163
+ [`deploy-docs.yml`](https://github.com/relaxed-constraints/relcon-docs/blob/main/.github/workflows/deploy-docs.yml)
164
+ workflow. It checks out the **caller repository and triggering revision**, installs
165
+ its locked `docs` dependency group, checks managed configuration, runs a strict
166
+ Zensical build, uploads `site/`, and deploys it to GitHub Pages. It does not update
167
+ configuration during deployment.
168
+
169
+ Choose the project's policy by editing the starter's `on` mapping. For BONDI,
170
+ keep automatic deployment limited to published releases:
171
+
172
+ ```yaml
173
+ on:
174
+ workflow_dispatch:
175
+ release:
176
+ types: [published]
177
+ ```
178
+
179
+ A consumer project can instead publish on each push to `main`:
180
+
181
+ ```yaml
182
+ on:
183
+ workflow_dispatch:
184
+ push:
185
+ branches: [main]
186
+ ```
187
+
188
+ The caller grants `contents: read`, `pages: write` and `id-token: write`.
189
+ The build job uses read-only repository access; the deployment job gets Pages
190
+ permissions and uses the `github-pages` environment. Deployments share a concurrency
191
+ group and do not cancel an in-progress deployment.
192
+
193
+ ### Repository setup and workflow updates
194
+
195
+ - Set **Settings → Pages → Source → GitHub Actions** once in each repository.
196
+ - Set the project's `site_url` to its published URL before deploying.
197
+ - Commit `uv.lock`, provide a `docs` dependency group, and use the standard
198
+ `docs/` source and `site/` output directories.
199
+ - For release deployment, allow the release tags under the `github-pages`
200
+ environment's deployment protection rules.
201
+ - When an existing deployment workflow uses another name (such as `pages.yml`),
202
+ consolidate it with the starter before enabling automatic triggers. Configure
203
+ does not remove or alter other workflows.
204
+
205
+ The starter references `@main`; use a reviewed full commit SHA for reproducible
206
+ deployment tooling. No `v1` tag is assumed. The workflow revision and Python package
207
+ dependency are independent: upgrading the package never rewrites the caller's
208
+ workflow reference or triggers.
209
+
210
+ Optional caller job inputs are `python-version` (default `"3.11"`) and
211
+ `uv-version` (default `"0.13.6"`). For example:
212
+
213
+ ```yaml
214
+ jobs:
215
+ docs:
216
+ uses: relaxed-constraints/relcon-docs/.github/workflows/deploy-docs.yml@main
217
+ with:
218
+ python-version: "3.12"
219
+ ```
220
+
221
+ ## CLI reference
222
+
223
+ `relcon-docs configure [--project PATH] [--dry-run | --check] [--force]`
224
+
225
+ - `--project`: target an existing project root; defaults to the working directory.
226
+ - `--dry-run`: print proposed file diffs without writing. Conflicts still fail unless forced.
227
+ - `--check`: write nothing; exit 0 when current, 1 for pending updates, 2 for errors/conflicts.
228
+ - `--force`: explicitly replace conflicting managed files; incompatible with `--check`.
229
+ - `relcon-docs --version`: display the installed package version.
230
+
231
+ Use `relcon-docs configure --check` in CI before `zensical build --strict`.
232
+
233
+ ## Development
234
+
235
+ ```sh
236
+ uv sync --locked
237
+ uv run pytest
238
+ uv run ruff check .
239
+ uv run ruff format --check .
240
+ uv run relcon-docs configure --check
241
+ uv run zensical build --strict
242
+ uv build
243
+ ```
244
+
245
+ For a local consumer trial before publishing, install the built wheel in an isolated
246
+ environment. The installed package contains all assets; consumers need no checkout
247
+ of this repository.
248
+
249
+ ## Feature examples
250
+
251
+ The documentation includes walkthroughs for Markdown, Mermaid diagrams, equations
252
+ and figures. Equations use the shared Arithmatex configuration and a pinned
253
+ MathJax renderer loaded from jsDelivr only on pages containing mathematics.
254
+ The site itself uses relcon-docs and its reusable deployment workflow.
255
+
256
+ ### API member defaults
257
+
258
+ API members are ordered alphabetically. Members whose names start with `_` are
259
+ excluded from automatic member listings, including double-underscore names.
260
+ Projects can override `members_order` and `filters` under
261
+ `plugins.mkdocstrings.handlers.python.options` in `mkdocs.yml`, or on an
262
+ individual API directive when a specific private or special method needs documenting.
@@ -0,0 +1,11 @@
1
+ relcon_docs/__init__.py,sha256=W0NXnT-yLV2EiR47avbXl4Wc0z0TVreVpty1L55oENs,75
2
+ relcon_docs/cli.py,sha256=9p-VR4ZVCQpyxkqN2VyVFr8Zax_3A5hGE_4L-gJReK4,9201
3
+ relcon_docs/assets/base.yml,sha256=62HfufJqTk2Zjwq-2IP7186KvxLVMfoWE134Ja2uOck,1542
4
+ relcon_docs/assets/docs.yml,sha256=eGpQdIndm2jeVWazXyuRDHZqFgEQxsyVziuxCk5dWCk,624
5
+ relcon_docs/assets/relcon-docs-math.js,sha256=uOexX0GfTlIINXASL1N-pwNrtSklbX8XKq5RRJ1SNmE,1284
6
+ relcon_docs/assets/relcon-docs.css,sha256=r7xrqQrU6A6deb3YmkqCWsL_Yq9MIT0ij2eL6HyBCoU,3520
7
+ relcon_docs-0.1.0.dist-info/METADATA,sha256=RVken39weXKGH7djUsVHlsoJVsSauUbD6tZ1Btb_YtI,10364
8
+ relcon_docs-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
9
+ relcon_docs-0.1.0.dist-info/entry_points.txt,sha256=ay_WbSM4YntoyOebqfHlmKovXjpjX5TEauCAmoK_xoo,53
10
+ relcon_docs-0.1.0.dist-info/licenses/LICENSE,sha256=YboKyi6DcoknxJ9-nAyx50FWQP1Bi7fpBbDGP8IZRcc,1076
11
+ relcon_docs-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ relcon-docs = relcon_docs.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Relaxed Constraints
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.