unaltraweb 0.7.1 → 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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/Makefile +26 -4
  3. data/docs/_documentation/en/40-distribution.md +33 -10
  4. data/docs/_documentation/en/43-workspace-path-policies.md +13 -0
  5. data/docs/_documentation/en/47-private-practice-pdfs.md +130 -0
  6. data/docs/agents/consolidation-0.7.2.md +97 -0
  7. data/docs/agents/d0-runtime-identity.md +8 -0
  8. data/docs/agents/deployment-integration-0.7.1.md +4 -0
  9. data/docs/agents/manual-authoring-components.md +23 -0
  10. data/docs/agents/mcp-contract.md +109 -4
  11. data/docs/agents/owner-closeout-93.md +185 -0
  12. data/docs/agents/practice-editions-98.md +816 -0
  13. data/docs/agents/practice-pdf-installable-98.md +250 -0
  14. data/docs/agents/practice-runtime-archives-98.md +147 -0
  15. data/docs/agents/private-practice-verifier-98.md +296 -0
  16. data/docs/agents/source-ci-98.md +148 -0
  17. data/scripts/check_practice_verifier_runtime.py +69 -0
  18. data/scripts/export_practice_runtime_archives.py +138 -0
  19. data/scripts/manual/filters/practice-finish.lua +36 -0
  20. data/scripts/manual/filters/practice-layout.lua +84 -0
  21. data/scripts/manual/practice_pdf.py +31 -0
  22. data/scripts/manual/render_practice.py +125 -0
  23. data/scripts/manual/templates/practice.tex +95 -0
  24. data/scripts/prepare_practice_candidate.py +159 -0
  25. data/scripts/prepare_practice_editions_candidate.py +119 -0
  26. data/scripts/test_wheel_install.py +15 -0
  27. data/scripts/unaltraweb-mcp-bootstrap.sh +1 -1
  28. data/scripts/validate_distribution.py +18 -1
  29. data/src/unaltraweb_mcp/component-contract.json +28 -17
  30. data/src/unaltraweb_mcp/editorial_sources.py +62 -4
  31. metadata +19 -2
@@ -103,6 +103,10 @@ def main() -> int:
103
103
  "unaltraweb_mcp/editorial_sources.py",
104
104
  "unaltraweb_mcp/image_backgrounds.py",
105
105
  "unaltraweb_mcp/image_probe.py",
106
+ "unaltraweb_mcp/source_checks.py",
107
+ "unaltraweb_mcp/source-check-policy-v1.schema.json",
108
+ "unaltraweb_mcp/practice_verifier.py",
109
+ "unaltraweb_mcp/practice-verifier-runtime.json",
106
110
  "unaltraweb_mcp/scaffolds/common/AGENTS.md.tmpl",
107
111
  "unaltraweb_mcp/scaffolds/common/Makefile.tmpl",
108
112
  "unaltraweb_mcp/scaffolds/common/README.md.tmpl",
@@ -117,6 +121,7 @@ def main() -> int:
117
121
  "unaltraweb_mcp/scaffolds/common/.github/dependabot.yml.tmpl",
118
122
  "unaltraweb_mcp/scaffolds/common/.github/pull_request_template.md",
119
123
  "unaltraweb_mcp/scaffolds/common/.github/workflows/deploy.yml.tmpl",
124
+ "unaltraweb_mcp/scaffolds/common/.github/workflows/source-checks.yml.tmpl",
120
125
  ]
121
126
  missing = [name for name in required if name not in names]
122
127
  if missing:
@@ -129,6 +134,10 @@ def main() -> int:
129
134
  python = environment / "bin/python"
130
135
  cli = environment / "bin/unaltraweb-mcp"
131
136
  run([str(python), "-m", "pip", "install", "--no-deps", str(wheels[0])], cwd=temp, env=env)
137
+ source_cli = environment / "bin/unaltraweb-source-checks"
138
+ source_help = run([str(source_cli), "--help"], cwd=temp).stdout
139
+ if "--base-sha" not in source_help or "--reports" not in source_help:
140
+ raise RuntimeError("Installed source-CI entrypoint is missing its bounded interface")
132
141
  docker_cli = environment / "bin/unaltraweb-mcp-docker"
133
142
  launcher = Path(run([str(docker_cli), "path"], cwd=temp).stdout.strip())
134
143
  if launcher != environment / "share/unaltraweb-launcher":
@@ -178,6 +187,11 @@ def main() -> int:
178
187
  ], cwd=temp).stdout)
179
188
  if not created["ok"]:
180
189
  raise RuntimeError(f"new-web failed for {profile} from clean wheel: {created}")
190
+ source_policy = json.loads((profile_site / ".unaltraweb/source-checks.json").read_bytes())
191
+ if source_policy["profile"] != profile:
192
+ raise RuntimeError("Source-CI policy did not declare the generated profile")
193
+ if not (profile_site / ".github/workflows/source-checks.yml").is_file():
194
+ raise RuntimeError("Source-CI workflow control is missing")
181
195
  readme = (profile_site / "README.md").read_text(encoding="utf-8")
182
196
  agents = (profile_site / "AGENTS.md").read_text(encoding="utf-8")
183
197
  collaboration = (profile_site / ".github/CONTRIBUTING.md").read_text(encoding="utf-8")
@@ -317,6 +331,7 @@ def main() -> int:
317
331
  "Gemfile.lock",
318
332
  ".github/pull_request_template.md",
319
333
  ".github/workflows/deploy.yml",
334
+ ".github/workflows/source-checks.yml",
320
335
  }
321
336
  if set(manifest["files"]) != expected_managed:
322
337
  raise RuntimeError(f"generated {profile} scaffold baseline is incomplete: {manifest['files']}")
@@ -15,7 +15,7 @@ Managed mode requires an immutable reference or an expected full image ID.
15
15
  USAGE
16
16
  }
17
17
 
18
- image="${UNALTRAWEB_MCP_IMAGE:-ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:b84cdb404ba7bab5ff0b14fa8a7ee93d7293e829cf32e28ff0dacaa8afff4677}"
18
+ image="${UNALTRAWEB_MCP_IMAGE:-ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:6aa20cff86c3891a876e37ba0548fa36f3c5633c9f22ba672e9f50123d677cf6}"
19
19
  project="${MCP_CONSUMER_WORKSPACE:-${UNALTRAWEB_PROJECT:-}}"
20
20
  operation=serve
21
21
  expected_image="${UNALTRAWEB_EXPECTED_IMAGE_ID:-}"
@@ -41,6 +41,7 @@ from unaltraweb_mcp.distribution import ( # noqa: E402
41
41
  validate_component_contract,
42
42
  )
43
43
  from scripts.validate_workflows import DEPLOY_PDF_PROVENANCE_POLICY_SHA256, load_workflow_text, reusable_deploy_input_errors # noqa: E402
44
+ from scripts import check_practice_verifier_runtime as practice_verifier # noqa: E402
44
45
 
45
46
 
46
47
  def load_yaml(path: Path) -> dict[str, Any]:
@@ -111,6 +112,9 @@ def validate(root: Path = ROOT) -> list[str]:
111
112
  required_components = {
112
113
  "gem", "wheel", "runtime", "mcp", "compute_python", "compute_r", "web_capture", "manual_pdf", "diavisuals", "vegavisuals"
113
114
  }
115
+ if practice_verifier.COMPONENT in contract.get("components", {}):
116
+ required_components.add(practice_verifier.COMPONENT)
117
+ errors.extend(practice_verifier.publication_errors(contract))
114
118
  if contract.get("schema_version") != 1:
115
119
  errors.append("component contract schema_version must be 1")
116
120
  if set(contract.get("components", {})) != required_components:
@@ -121,6 +125,10 @@ def validate(root: Path = ROOT) -> list[str]:
121
125
  if __version__ != version:
122
126
  errors.append(f"wheel version {__version__} != contract version {version}")
123
127
  errors.extend(component_version_errors(contract))
128
+ if practice_verifier.required(contract):
129
+ dockerfile = (root / "Dockerfile.mcp").read_text()
130
+ if "install_practice_verifier" in dockerfile or re.search(r"(?im)^\s*(?:RUN|COPY|ADD).*unaltracaptura", dockerfile):
131
+ errors.append("Public MCP image must not download or embed the private QGIS package")
124
132
  included = {name for name, item in contract["components"].items() if item["included_in_wheel"]}
125
133
  if included != {"wheel"}:
126
134
  errors.append(f"wheel must not bundle external components: {sorted(included)}")
@@ -212,6 +220,9 @@ def validate(root: Path = ROOT) -> list[str]:
212
220
  if make_value(root / "packaging/launcher/Makefile", "MCP_RELEASE_IMAGE") != component_reference("mcp"):
213
221
  errors.append("installed launcher must select its own package release image")
214
222
  project = pyproject.get("project", {})
223
+ dependencies = [*project.get("dependencies", []), *(entry for values in project.get("optional-dependencies", {}).values() for entry in values)]
224
+ if any(re.search(r"unaltracaptura[-_]qgis", str(entry), re.I) for entry in dependencies):
225
+ errors.append("Public Web wheel must not install the private QGIS package as a dependency")
215
226
  if "version" in project or project.get("dynamic") != ["version"]:
216
227
  errors.append("pyproject version must be derived from the package component contract")
217
228
  if pyproject.get("tool", {}).get("setuptools", {}).get("dynamic", {}).get("version", {}).get("attr") != "unaltraweb_mcp.__version__":
@@ -515,7 +526,7 @@ def publish_ref_errors(
515
526
  default_branch: str,
516
527
  component_ids: list[str],
517
528
  ) -> list[str]:
518
- errors: list[str] = []
529
+ errors: list[str] = practice_verifier.publication_errors(contract)
519
530
  release = contract.get("release", {})
520
531
  version = str(release.get("version") or "")
521
532
  release_tag = str(release.get("tag") or "")
@@ -590,6 +601,10 @@ def main(argv: list[str] | None = None) -> int:
590
601
  component_id for component_id, selected in contract["components"].items()
591
602
  if selected["release_status"] == "pending"
592
603
  )
604
+ verifier_errors = practice_verifier.publication_errors(contract)
605
+ if verifier_errors and practice_verifier.COMPONENT not in pending:
606
+ pending.append(practice_verifier.COMPONENT)
607
+ pending.sort()
593
608
  unavailable = sorted(
594
609
  component_id for component_id, selected in contract["components"].items()
595
610
  if selected["release_status"] == "unavailable"
@@ -612,6 +627,8 @@ def main(argv: list[str] | None = None) -> int:
612
627
  "errors": errors,
613
628
  "pending_releases": pending,
614
629
  "unavailable_releases": unavailable,
630
+ "practice_verifier": {"selection_ready": not verifier_errors, "errors": verifier_errors, "origin": "prepared-image",
631
+ "private_checks": {"state": "not_run", "reason": "Public CI uses metadata/unit fixtures; native private acceptance and review are separate required gates"}},
615
632
  }, indent=2, sort_keys=True))
616
633
  readiness_errors = release_tag_errors + candidate_receipt_errors
617
634
  structural_errors = [error for error in errors if error not in readiness_errors]
@@ -2,8 +2,8 @@
2
2
  "$schema": "component-contract.schema.json",
3
3
  "schema_version": 1,
4
4
  "release": {
5
- "version": "0.7.1",
6
- "tag": "v0.7.1"
5
+ "version": "0.7.2",
6
+ "tag": "v0.7.2"
7
7
  },
8
8
  "consumer_integration": {
9
9
  "schema_version": 1,
@@ -55,17 +55,17 @@
55
55
  "package_only_commands": ["doctor", "import-calibre", "new-web", "version"],
56
56
  "factory_required_commands": ["factory-dir"],
57
57
  "package_only_mcp": ["artifact-import-check", "import-artifact-bundle", "bibliography-add-entry", "bibliography-inventory", "build-health", "content-approval-inventory", "content-freshness-check", "content-inventory", "detect-site", "editorial-policy", "editorial-publication-check", "editorial-review-prepare", "editorial-review-record", "editorial-review-resolve", "editorial-status", "html-audit", "http-check", "image-background-check", "initialize-site", "language-policy", "list-tools", "manual-authoring-capabilities", "manual-editorial-quality-check", "manual-pdf-preview-clean", "manual-source-quality-check", "new-web", "preview-start", "preview-status", "preview-stop", "profile-check", "profile-prune", "profile-prune-plan", "prose-check", "scaffold-sync", "site-context", "site-doctor", "site-source-delete", "site-source-read", "site-source-write", "starter-templates", "translation-plan"],
58
- "factory_required_mcp": ["bibliometrics-check", "bibliometrics-fetch-scimago", "bibliometrics-update", "build-site", "manual-computation-check", "manual-computation-render", "manual-computation-render-figures", "manual-computation-status", "manual-pdf-build", "manual-pdf-preview-prepare", "manual-pdf-publish", "manual-pdf-status", "manual-release-check", "manual-release-prepare", "manual-release-status", "prompts", "serve", "site-check", "web-capture-check", "web-capture-render", "web-capture-status"],
59
- "not_bundled": ["gem", "runtime", "mcp", "compute_python", "compute_r", "web_capture", "manual_pdf", "diavisuals", "vegavisuals"]
58
+ "factory_required_mcp": ["bibliometrics-check", "bibliometrics-fetch-scimago", "bibliometrics-update", "build-site", "manual-computation-check", "manual-computation-render", "manual-computation-render-figures", "manual-computation-status", "manual-pdf-build", "manual-pdf-preview-prepare", "manual-pdf-publish", "manual-pdf-status", "manual-practice-compose", "manual-practice-pdf-build", "manual-release-check", "manual-release-prepare", "manual-release-status", "prompts", "serve", "site-check", "web-capture-check", "web-capture-render", "web-capture-status"],
59
+ "not_bundled": ["gem", "runtime", "mcp", "compute_python", "compute_r", "web_capture", "manual_pdf", "diavisuals", "vegavisuals", "qgis_practice_verifier"]
60
60
  },
61
61
  "components": {
62
62
  "gem": {
63
63
  "kind": "gem",
64
64
  "name": "unaltraweb",
65
- "version": "0.7.1",
66
- "release": "v0.7.1",
65
+ "version": "0.7.2",
66
+ "release": "v0.7.2",
67
67
  "release_status": "ready",
68
- "reference": "unaltraweb (= 0.7.1)",
68
+ "reference": "unaltraweb (= 0.7.2)",
69
69
  "repository": "https://github.com/dosquartsdedocs/unaltraweb",
70
70
  "included_in_wheel": false,
71
71
  "features": ["jekyll-core", "site-build"]
@@ -73,21 +73,21 @@
73
73
  "wheel": {
74
74
  "kind": "python-wheel",
75
75
  "name": "unaltraweb-mcp",
76
- "version": "0.7.1",
77
- "release": "v0.7.1",
76
+ "version": "0.7.2",
77
+ "release": "v0.7.2",
78
78
  "release_status": "ready",
79
- "reference": "unaltraweb-mcp==0.7.1",
79
+ "reference": "unaltraweb-mcp==0.7.2",
80
80
  "repository": "https://github.com/dosquartsdedocs/unaltraweb",
81
81
  "included_in_wheel": true,
82
- "features": ["calibre-import", "doctor", "new-web", "offline-inspection", "mcp-control-plane"]
82
+ "features": ["calibre-import", "doctor", "new-web", "offline-inspection", "mcp-control-plane", "source-ci"]
83
83
  },
84
84
  "runtime": {
85
85
  "kind": "container",
86
86
  "name": "unaltraweb runtime",
87
- "version": "0.7.1",
88
- "release": "v0.7.1",
87
+ "version": "0.7.2",
88
+ "release": "v0.7.2",
89
89
  "release_status": "ready",
90
- "reference": "ghcr.io/dosquartsdedocs/unaltraweb:0.7.1",
90
+ "reference": "ghcr.io/dosquartsdedocs/unaltraweb:0.7.2",
91
91
  "image_repository": "ghcr.io/dosquartsdedocs/unaltraweb",
92
92
  "repository": "https://github.com/dosquartsdedocs/unaltraweb",
93
93
  "included_in_wheel": false,
@@ -96,10 +96,10 @@
96
96
  "mcp": {
97
97
  "kind": "container",
98
98
  "name": "unaltraweb MCP runtime",
99
- "version": "0.7.1",
100
- "release": "v0.7.1",
99
+ "version": "0.7.2",
100
+ "release": "v0.7.2",
101
101
  "release_status": "ready",
102
- "reference": "ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.7.1",
102
+ "reference": "ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.7.2",
103
103
  "image_repository": "ghcr.io/dosquartsdedocs/unaltraweb-mcp",
104
104
  "repository": "https://github.com/dosquartsdedocs/unaltraweb",
105
105
  "included_in_wheel": false,
@@ -153,6 +153,17 @@
153
153
  "included_in_wheel": false,
154
154
  "features": ["manual-pdf"]
155
155
  },
156
+ "qgis_practice_verifier": {
157
+ "kind": "companion",
158
+ "name": "unaltracaptura-qgis",
159
+ "version": "0.6.0",
160
+ "release": "v0.6.0",
161
+ "release_status": "released",
162
+ "reference": "https://github.com/dosquartsdedocs/unaltracaptura-qgis.git@v0.6.0",
163
+ "repository": "https://github.com/dosquartsdedocs/unaltracaptura-qgis",
164
+ "included_in_wheel": false,
165
+ "features": ["qgis-practice-v1", "qgis-practice-v2", "private-prepared-runtime"]
166
+ },
156
167
  "diavisuals": {
157
168
  "kind": "companion",
158
169
  "name": "diavisuals",
@@ -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("published") is False or (profiles and profile not in profiles):
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": "Markdown/HTML prose and public metadata fields; code, math and quotations require semantic/rendered review."}
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.1
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-07 00:00:00.000000000 Z
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,6 +801,7 @@ 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
804
806
  - docs/agents/deploy-provenance-86.md
805
807
  - docs/agents/deployment-integration-0.7.1.md
@@ -808,10 +810,16 @@ files:
808
810
  - docs/agents/owner-closeout-76.md
809
811
  - docs/agents/owner-closeout-80.md
810
812
  - docs/agents/owner-closeout-85.md
813
+ - docs/agents/owner-closeout-93.md
811
814
  - docs/agents/owner-followup-76.md
812
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
813
820
  - docs/agents/quarto-computation-adoption.md
814
821
  - docs/agents/retained-letter-import.md
822
+ - docs/agents/source-ci-98.md
815
823
  - docs/agents/visual-companions-0.4.0.md
816
824
  - docs/assets/img/caption-credits-demo.svg
817
825
  - docs/assets/img/unaltraweb-docs-hero.svg
@@ -822,12 +830,14 @@ files:
822
830
  - scripts/biblio/metrics_common.py
823
831
  - scripts/biblio/metrics_merge_meta.py
824
832
  - scripts/biblio/metrics_update.py
833
+ - scripts/check_practice_verifier_runtime.py
825
834
  - scripts/computations/python/Dockerfile
826
835
  - scripts/computations/r/Dockerfile
827
836
  - scripts/computations/render.py
828
837
  - scripts/cv/render_pdf_preview.sh
829
838
  - scripts/deploy/publish_branch.sh
830
839
  - scripts/editorial_check.py
840
+ - scripts/export_practice_runtime_archives.py
831
841
  - scripts/generate_logo_variants.py
832
842
  - scripts/image_background_check.py
833
843
  - scripts/manual/Dockerfile
@@ -835,9 +845,16 @@ files:
835
845
  - scripts/manual/filters/bibliography.lua
836
846
  - scripts/manual/filters/code-blocks.lua
837
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
838
851
  - scripts/manual/publish_release.sh
852
+ - scripts/manual/render_practice.py
839
853
  - scripts/manual/templates/manual.tex
854
+ - scripts/manual/templates/practice.tex
840
855
  - scripts/manual/verify_release_assets.py
856
+ - scripts/prepare_practice_candidate.py
857
+ - scripts/prepare_practice_editions_candidate.py
841
858
  - scripts/test_gem_build.py
842
859
  - scripts/test_reproducible_jekyll_build.py
843
860
  - scripts/test_wheel_install.py