unaltraweb 0.7.0 → 0.7.2
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.
- checksums.yaml +4 -4
- data/Makefile +27 -5
- data/docs/_documentation/en/33-reusable-workflows.md +28 -3
- data/docs/_documentation/en/40-distribution.md +57 -19
- data/docs/_documentation/en/43-workspace-path-policies.md +13 -0
- data/docs/_documentation/en/46-live-runtime.md +6 -3
- data/docs/_documentation/en/47-private-practice-pdfs.md +130 -0
- data/docs/agents/consolidation-0.7.2.md +97 -0
- data/docs/agents/d0-runtime-identity.md +22 -5
- data/docs/agents/deploy-provenance-86.md +86 -0
- data/docs/agents/deployment-integration-0.7.1.md +142 -0
- data/docs/agents/manual-authoring-components.md +23 -0
- data/docs/agents/mcp-contract.md +112 -6
- data/docs/agents/owner-closeout-85.md +269 -0
- data/docs/agents/owner-closeout-93.md +185 -0
- data/docs/agents/practice-editions-98.md +816 -0
- data/docs/agents/practice-pdf-installable-98.md +250 -0
- data/docs/agents/practice-runtime-archives-98.md +147 -0
- data/docs/agents/private-practice-verifier-98.md +296 -0
- data/docs/agents/source-ci-98.md +148 -0
- data/scripts/check_practice_verifier_runtime.py +69 -0
- data/scripts/export_practice_runtime_archives.py +138 -0
- data/scripts/manual/filters/practice-finish.lua +36 -0
- data/scripts/manual/filters/practice-layout.lua +84 -0
- data/scripts/manual/practice_pdf.py +31 -0
- data/scripts/manual/render_practice.py +125 -0
- data/scripts/manual/templates/practice.tex +95 -0
- data/scripts/prepare_practice_candidate.py +159 -0
- data/scripts/prepare_practice_editions_candidate.py +119 -0
- data/scripts/test_wheel_install.py +16 -1
- data/scripts/unaltraweb-mcp-bootstrap.sh +1 -1
- data/scripts/validate_distribution.py +51 -5
- data/scripts/validate_workflows.py +33 -4
- data/src/unaltraweb_mcp/component-contract.json +59 -22
- data/src/unaltraweb_mcp/component-contract.schema.json +35 -2
- data/src/unaltraweb_mcp/distribution.py +42 -2
- data/src/unaltraweb_mcp/editorial_sources.py +62 -4
- metadata +22 -2
|
@@ -21,6 +21,8 @@ CONTENT_ROOTS = ("_pages", "_posts", "_news", "_projects", "_outputs", "_chapter
|
|
|
21
21
|
TEXT_SUFFIXES = {".md", ".markdown", ".html", ".qmd", ".rmd"}
|
|
22
22
|
PUBLIC_FIELDS = {"title", "short_title", "description", "excerpt", "summary", "bio", "caption", "alt"}
|
|
23
23
|
GENRES = {"prose", "bio", "news", "chapter", "procedure", "reference", "preface", "quote", "example", "cv"}
|
|
24
|
+
PRACTICE_SOURCE = re.compile(r"practiques/([a-z0-9][a-z0-9-]{0,63})/(ca|es|en)/(alumnat|docent)/(LLEGIU-ME|LEEME|README)\.md\Z")
|
|
25
|
+
PRACTICE_NAMES = {"ca": "LLEGIU-ME", "es": "LEEME", "en": "README"}
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
def default_language(config: dict[str, Any]) -> str:
|
|
@@ -107,6 +109,51 @@ def relative_path(raw: str) -> str:
|
|
|
107
109
|
return raw
|
|
108
110
|
|
|
109
111
|
|
|
112
|
+
def private_practice_readings(config: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
113
|
+
"""One bounded author-declared mapping shared by private review and PDFs."""
|
|
114
|
+
section = (config.get("unaltraweb") or {}).get("practices") or {}
|
|
115
|
+
if not isinstance(section, dict):
|
|
116
|
+
raise EditorialError("unaltraweb.practices must be a mapping")
|
|
117
|
+
rows = section.get("readings", [])
|
|
118
|
+
if not isinstance(rows, list) or len(rows) > 128:
|
|
119
|
+
raise EditorialError("Private reading mappings must be a bounded list")
|
|
120
|
+
result, identities, names = {}, set(), set()
|
|
121
|
+
for row in rows:
|
|
122
|
+
if (not isinstance(row, dict) or not {"source", "slug", "lang", "recipient"} <= row.keys()
|
|
123
|
+
or set(row) - {"source", "slug", "lang", "recipient", "bindings"}
|
|
124
|
+
or not all(isinstance(row[key], str) for key in ("source", "slug", "lang", "recipient"))):
|
|
125
|
+
raise EditorialError("Private reading mapping requires source/slug/lang/recipient")
|
|
126
|
+
if "bindings" in row and (not isinstance(row["bindings"], list) or len(row["bindings"]) > 200):
|
|
127
|
+
raise EditorialError("Reading resource bindings must be a bounded list")
|
|
128
|
+
source = relative_path(row["source"])
|
|
129
|
+
if (Path(source).parts[0] not in {"practiques", "_practices"} or Path(source).suffix != ".md"
|
|
130
|
+
or len(Path(source).parts) < 3 or any(part.startswith(".") for part in Path(source).parts)
|
|
131
|
+
or not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,63}", row["slug"])
|
|
132
|
+
or row["lang"] not in PRACTICE_NAMES or row["recipient"] not in {"alumnat", "docent"}):
|
|
133
|
+
raise EditorialError("Invalid confined private reading mapping")
|
|
134
|
+
identity = (row["slug"], row["lang"], row["recipient"])
|
|
135
|
+
canonical = PRACTICE_SOURCE.fullmatch(source)
|
|
136
|
+
if canonical and (canonical.groups()[:3] != identity or canonical[4] != PRACTICE_NAMES[row["lang"]]):
|
|
137
|
+
raise EditorialError("Mapping conflicts with the canonical reading identity")
|
|
138
|
+
other = "docent" if row["recipient"] == "alumnat" else "alumnat"
|
|
139
|
+
if other in Path(source).parts or source.casefold() in names or identity in identities:
|
|
140
|
+
raise EditorialError("Duplicate or cross-recipient private reading mapping")
|
|
141
|
+
result[source] = dict(row)
|
|
142
|
+
names.add(source.casefold())
|
|
143
|
+
identities.add(identity)
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def private_practice_identity(config: dict[str, Any], source: str) -> dict[str, Any] | None:
|
|
148
|
+
declared = private_practice_readings(config)
|
|
149
|
+
if source in declared:
|
|
150
|
+
return declared[source]
|
|
151
|
+
match = PRACTICE_SOURCE.fullmatch(source)
|
|
152
|
+
if match and match[4] == PRACTICE_NAMES[match[2]]:
|
|
153
|
+
return {"source": source, "slug": match[1], "lang": match[2], "recipient": match[3]}
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
110
157
|
class Reader:
|
|
111
158
|
"""Descriptor-relative reads; never follows an input symlink or creates files."""
|
|
112
159
|
def __init__(self, root: Path, *, max_bytes: int | None = None, max_total_bytes: int | None = None):
|
|
@@ -427,8 +474,10 @@ def corpus(reader: Reader, config: dict[str, Any], profile: str, target: str = "
|
|
|
427
474
|
extra = config.get("collections") or {}
|
|
428
475
|
if not manual_only and isinstance(extra, dict):
|
|
429
476
|
roots.update("_" + key for key in extra if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]*", str(key)))
|
|
477
|
+
roots.difference_update({"practiques", "_practices"})
|
|
478
|
+
private = private_practice_identity(config, target) if target and profile == "unaltremanual" else None
|
|
430
479
|
root_pages = reader.root_pages() if not manual_only else []
|
|
431
|
-
if target and target != "_config.yml" and target.split("/")[0] not in roots and target not in root_pages:
|
|
480
|
+
if target and not private and target != "_config.yml" and target.split("/")[0] not in roots and target not in root_pages:
|
|
432
481
|
raise EditorialError("Editorial target must be reader-facing content or public metadata.")
|
|
433
482
|
paths = ([target] if Path(target).suffix else reader.walk(target)) if target else [path for root in sorted(roots) for path in reader.walk(root)]
|
|
434
483
|
if not target:
|
|
@@ -443,7 +492,7 @@ def corpus(reader: Reader, config: dict[str, Any], profile: str, target: str = "
|
|
|
443
492
|
raise EditorialError("Site languages must be a list of language codes.")
|
|
444
493
|
sources, fragments, skipped = {}, [], []
|
|
445
494
|
for path in sorted(set(paths)):
|
|
446
|
-
if any(isinstance(pattern, str) and (fnmatch.fnmatch(path, pattern) or path.startswith(pattern.rstrip("/") + "/")) for pattern in excludes):
|
|
495
|
+
if not private and any(isinstance(pattern, str) and (fnmatch.fnmatch(path, pattern) or path.startswith(pattern.rstrip("/") + "/")) for pattern in excludes):
|
|
447
496
|
skipped.append({"path": path, "reason": "excluded by site configuration"})
|
|
448
497
|
continue
|
|
449
498
|
suffix = Path(path).suffix.lower()
|
|
@@ -456,6 +505,8 @@ def corpus(reader: Reader, config: dict[str, Any], profile: str, target: str = "
|
|
|
456
505
|
continue
|
|
457
506
|
lang = next((part for part in Path(path).parts if part in languages), default_lang)
|
|
458
507
|
genre = "news" if path.startswith(("_news/", "_posts/")) else "chapter" if path.startswith(f"_{collection}/") else "reference" if profile == "unaltredocs" else "prose"
|
|
508
|
+
if private:
|
|
509
|
+
lang, genre = private["lang"], "procedure"
|
|
459
510
|
if metadata:
|
|
460
511
|
if suffix == ".json":
|
|
461
512
|
data = strict_json(content)
|
|
@@ -469,12 +520,18 @@ def corpus(reader: Reader, config: dict[str, Any], profile: str, target: str = "
|
|
|
469
520
|
profiles = [profiles]
|
|
470
521
|
if not isinstance(profiles, list) or any(not isinstance(item, str) for item in profiles):
|
|
471
522
|
raise EditorialError(f"Invalid profiles in {path}.")
|
|
472
|
-
if front.get("
|
|
523
|
+
if private and front.get("lang", lang) != lang:
|
|
524
|
+
raise EditorialError("Reading language differs from its declared mapping")
|
|
525
|
+
if not private and (front.get("published") is False or (profiles and profile not in profiles)):
|
|
473
526
|
skipped.append({"path": path, "reason": "not published in this profile"})
|
|
474
527
|
continue
|
|
475
528
|
if manual_only and path.startswith("_pages/") and front.get("layout") not in {"manual-home", "manual-chapter"} and "unaltremanual" not in profiles:
|
|
476
529
|
continue
|
|
477
530
|
sources[path] = {"path": path, "sha256": digest(content.encode()), "text": content, "editable_source": path}
|
|
531
|
+
if private:
|
|
532
|
+
sources[path].update(scope="private-practice", reading=private)
|
|
533
|
+
for unit in units:
|
|
534
|
+
unit["scope"] = "private-practice"
|
|
478
535
|
fragments.extend(units)
|
|
479
536
|
if len(fragments) > MAX_FRAGMENTS:
|
|
480
537
|
raise EditorialError("Editorial fragment budget exceeded; select a smaller target.")
|
|
@@ -501,4 +558,5 @@ def corpus(reader: Reader, config: dict[str, Any], profile: str, target: str = "
|
|
|
501
558
|
if target and not sources:
|
|
502
559
|
raise EditorialError(f"No selected publishable sources for target: {target}")
|
|
503
560
|
return {"sources": sources, "fragments": fragments, "skipped": skipped,
|
|
504
|
-
"coverage": "
|
|
561
|
+
"coverage": ("Explicitly selected private practice reading; no publication or author approval implied." if private else
|
|
562
|
+
"Markdown/HTML prose and public metadata fields; code, math and quotations require semantic/rendered review.")}
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: unaltraweb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dosquartsdedocs
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: classifier-reborn
|
|
@@ -783,6 +783,7 @@ files:
|
|
|
783
783
|
- docs/_documentation/en/44-editorial-review.md
|
|
784
784
|
- docs/_documentation/en/45-retained-documents.md
|
|
785
785
|
- docs/_documentation/en/46-live-runtime.md
|
|
786
|
+
- docs/_documentation/en/47-private-practice-pdfs.md
|
|
786
787
|
- docs/_documentation/en/50-faq.md
|
|
787
788
|
- docs/_pages/ca/index.md
|
|
788
789
|
- docs/_pages/en/index.md
|
|
@@ -800,15 +801,25 @@ files:
|
|
|
800
801
|
- docs/agents/action-prompts/50-bibliography-entry.txt
|
|
801
802
|
- docs/agents/action-prompts/60-bibliometrics-refresh.txt
|
|
802
803
|
- docs/agents/action-prompts/70-build-and-review.txt
|
|
804
|
+
- docs/agents/consolidation-0.7.2.md
|
|
803
805
|
- docs/agents/d0-runtime-identity.md
|
|
806
|
+
- docs/agents/deploy-provenance-86.md
|
|
807
|
+
- docs/agents/deployment-integration-0.7.1.md
|
|
804
808
|
- docs/agents/manual-authoring-components.md
|
|
805
809
|
- docs/agents/mcp-contract.md
|
|
806
810
|
- docs/agents/owner-closeout-76.md
|
|
807
811
|
- docs/agents/owner-closeout-80.md
|
|
812
|
+
- docs/agents/owner-closeout-85.md
|
|
813
|
+
- docs/agents/owner-closeout-93.md
|
|
808
814
|
- docs/agents/owner-followup-76.md
|
|
809
815
|
- docs/agents/owner-preparation-2026-09-29.md
|
|
816
|
+
- docs/agents/practice-editions-98.md
|
|
817
|
+
- docs/agents/practice-pdf-installable-98.md
|
|
818
|
+
- docs/agents/practice-runtime-archives-98.md
|
|
819
|
+
- docs/agents/private-practice-verifier-98.md
|
|
810
820
|
- docs/agents/quarto-computation-adoption.md
|
|
811
821
|
- docs/agents/retained-letter-import.md
|
|
822
|
+
- docs/agents/source-ci-98.md
|
|
812
823
|
- docs/agents/visual-companions-0.4.0.md
|
|
813
824
|
- docs/assets/img/caption-credits-demo.svg
|
|
814
825
|
- docs/assets/img/unaltraweb-docs-hero.svg
|
|
@@ -819,12 +830,14 @@ files:
|
|
|
819
830
|
- scripts/biblio/metrics_common.py
|
|
820
831
|
- scripts/biblio/metrics_merge_meta.py
|
|
821
832
|
- scripts/biblio/metrics_update.py
|
|
833
|
+
- scripts/check_practice_verifier_runtime.py
|
|
822
834
|
- scripts/computations/python/Dockerfile
|
|
823
835
|
- scripts/computations/r/Dockerfile
|
|
824
836
|
- scripts/computations/render.py
|
|
825
837
|
- scripts/cv/render_pdf_preview.sh
|
|
826
838
|
- scripts/deploy/publish_branch.sh
|
|
827
839
|
- scripts/editorial_check.py
|
|
840
|
+
- scripts/export_practice_runtime_archives.py
|
|
828
841
|
- scripts/generate_logo_variants.py
|
|
829
842
|
- scripts/image_background_check.py
|
|
830
843
|
- scripts/manual/Dockerfile
|
|
@@ -832,9 +845,16 @@ files:
|
|
|
832
845
|
- scripts/manual/filters/bibliography.lua
|
|
833
846
|
- scripts/manual/filters/code-blocks.lua
|
|
834
847
|
- scripts/manual/filters/figure-captions.lua
|
|
848
|
+
- scripts/manual/filters/practice-finish.lua
|
|
849
|
+
- scripts/manual/filters/practice-layout.lua
|
|
850
|
+
- scripts/manual/practice_pdf.py
|
|
835
851
|
- scripts/manual/publish_release.sh
|
|
852
|
+
- scripts/manual/render_practice.py
|
|
836
853
|
- scripts/manual/templates/manual.tex
|
|
854
|
+
- scripts/manual/templates/practice.tex
|
|
837
855
|
- scripts/manual/verify_release_assets.py
|
|
856
|
+
- scripts/prepare_practice_candidate.py
|
|
857
|
+
- scripts/prepare_practice_editions_candidate.py
|
|
838
858
|
- scripts/test_gem_build.py
|
|
839
859
|
- scripts/test_reproducible_jekyll_build.py
|
|
840
860
|
- scripts/test_wheel_install.py
|