From 9822c4fae677fffcb821468347f8b4c8b96df396 Mon Sep 17 00:00:00 2001 From: Threepwood-7 Date: Tue, 10 Nov 2026 13:48:25 +0100 Subject: [PATCH 1/5] docs(web): allow approved Rust identity assets (RUST-CI-046) --- AGENTS.md | 16 +++++++------ docs/SITE-HANDBOOK.md | 55 +++++++++++++++++++++++++------------------ 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d71c5f7..984ecf3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,11 +13,12 @@ edition. Everything below is this repo's local deltas only. ## Hard Rules -- Keep every HTML page text-only. Do not publish in-page logos, mascots, - screenshots, copied application artwork, or other raster assets. The browser - favicon at `assets/brand/emulebb-favicon.ico` is the only image exception. -- Use the text wordmark `eMuleBB` in page headers. Do not add logo or image - fields to metadata or structured data unless this policy changes first. +- Use the `eMuleBB` text wordmark with only the operator-approved proposal-1 + Rust donkey assets recorded in the public-presence contract. Do not publish + screenshots, unrelated artwork, or unapproved image variants. +- Give approved in-page images explicit dimensions and appropriate alt text. + The approved social card may appear in Open Graph/Twitter metadata. Keep the + favicon at `assets/brand/emulebb-favicon.ico`; do not regenerate brand assets. - Treat `emulebb-tooling` active/reference docs as the source of truth. Pages summarizes and links; it does not invent feature or release facts. - Treat `..\emulebb-tooling\manifests\public-presence.v1.json` as the source @@ -56,8 +57,9 @@ edition. Everything below is this repo's local deltas only. pages and `python tools\render_pages.py --lastmod YYYY-MM-DD --check` to confirm committed HTML is up to date with the templates and public-presence contract. -- Use `python tools\render_pages.py --lastmod YYYY-MM-DD --output-root - .local\site` to stage the bounded deployment artifact. +- The supported manager stages deployment artifacts below inherited + `EMULEBB_WORKSPACE_OUTPUT_ROOT`; the renderer refuses source-local staging. + `--capabilities` advertises the supported output protocol without writing. - Use `python ..\emulebb-tooling\helpers\pages-site-tools.py --pages-root . validate` before publishing locale, metadata, sitemap, asset-policy, or navigation changes. diff --git a/docs/SITE-HANDBOOK.md b/docs/SITE-HANDBOOK.md index cb898d3..3efe5d6 100644 --- a/docs/SITE-HANDBOOK.md +++ b/docs/SITE-HANDBOOK.md @@ -6,7 +6,7 @@ site at `https://emulebb.github.io/`. The site is the curated public front door for the eMuleBB organization. It presents CI-gated `emulebb-rust` nightly builds as the active beta testing channel and eMuleBB MFC `0.7.x` as the maintained Windows lane. Beta 3 is -expected after a few more weeks of implementation and stabilization. Rust +the next development target, without an ETA. Rust development is best-effort and spare-time, and contributors are welcome. The site is not the canonical engineering ledger, release ledger, or feature backlog. Those live in the public `emulebb-tooling` docs and the owning release @@ -37,9 +37,9 @@ repositories. `emulebb-rust` nightly prereleases from `main`. Formal beta tags remain immutable release evidence, not the public front-door download target. - The public Rust status is Active beta. Tell users to choose the newest dated - nightly prerelease from GitHub Releases. Beta 3 is expected after a few more - weeks of implementation and stabilization. The approved testing summary is: - “Repeatedly tested on live eD2K/Kad networks.” + nightly prerelease from GitHub Releases. Beta 3 is the next development + target, without a date. Keep historical live eD2K/Kad evidence separate from + exact-source native package smoke and current-head qualification. - Rust development is best-effort and spare-time. Public copy should welcome contributors and link to the repository's `CONTRIBUTING.md`. - The historical MFC `0.7.3` suite installer and aMuTorrent package remain @@ -65,21 +65,24 @@ repositories. homepage copy must remain focused and precise. - Avoid marketing fog. Prefer concrete behavior: upload slots, Kad, REST, shared libraries, validation, release gates. -- Keep every HTML page text-only. Use the `eMuleBB` text wordmark in headers. -- Do not publish logos, screenshots, copied application artwork, mascots, or - arbitrary raster assets from this repository. -- Do not add copied application resource images such as `Logo.jpg`. -- The browser favicon at `assets/brand/emulebb-favicon.ico` is the only image - exception. -- Structured data must not include logo or image fields unless this policy is - explicitly changed first. +- Use the `eMuleBB` text wordmark and only the explicitly approved proposal-1 + Rust donkey derivatives supplied by the operator. The shared public-presence + contract records their exact paths, dimensions and SHA-256 identities. +- Preserve the original artwork and supplied derivatives. Do not regenerate + them or publish screenshots, copied application resources or unrelated art. +- Approved images need explicit dimensions and appropriate alt text. A header + identity image beside the wordmark is decorative and uses empty alt text; + a standalone README image has descriptive alt text. +- The approved `1280x640` social card may appear in Open Graph/Twitter metadata. + Do not invent social accounts or add unrelated image metadata. +- Keep the browser favicon at `assets/brand/emulebb-favicon.ico`. ## Homepage Section Contract The English homepage is canonical. Keep this focused order: -1. Hero that defines eMuleBB Rust, with nightlies as the primary action and MFC - as a secondary link +1. Hero that defines eMuleBB Rust, with equal Use and Develop routes and MFC + available through a secondary maintenance link 2. Concise eMuleBB Rust capability summary 3. Essential downloads and documentation 4. Focused repositories @@ -190,7 +193,9 @@ Search/publishing rules: - `sitemap.xml` must include only complete, indexable pages. - Locale stubs and non-indexable compatibility chooser pages stay out of `sitemap.xml`. -- Metadata and structured data must not mention or point to logos or images. +- Image metadata may reference only the exact approved public social card, + with its dimensions, MIME type and descriptive alt text. Preserve canonical + URLs, reciprocal locale alternates and the text-only no-index Rust redirect. ## Layout And CSS Policy @@ -202,8 +207,8 @@ Search/publishing rules: - Mobile nav should remain usable without horizontal scrolling; the current pattern is a wrapped grid on small screens and a horizontal nav on desktop. - Use CSS variables in `:root` for shared colors. -- Do not introduce a one-hue palette; retain restrained contrast across teal, - blue, green, red, and gold accents. +- Use a neutral readable palette with restrained cyan identity accents. Keep + contrast and visible keyboard focus; avoid decorative animation. ## Update Workflow @@ -260,15 +265,19 @@ python -m pip install -r requirements.txt python ..\emulebb-tooling\helpers\public-presence.py validate $lastmod = ([regex]::Match((Get-Content -Raw sitemap.xml), '([^<]+)')).Groups[1].Value python tools\render_pages.py --lastmod $lastmod --check -python tools\render_pages.py --lastmod (Get-Date -Format yyyy-MM-dd) --output-root .local\site -python ..\emulebb-tooling\helpers\pages-site-tools.py --pages-root .local\site validate -python ..\emulebb-tooling\helpers\check-tooling-doclinks.py --docs-root ..\emulebb-tooling\docs .local\site -$expected = (((Get-Content .local\site\install.ps1.sha256) -split '\s+')[0]).ToUpperInvariant() -$actual = (Get-FileHash -Algorithm SHA256 .local\site\install.ps1).Hash -if ($actual -ne $expected) { throw 'Stable installer checksum mismatch' } git diff --check ``` +Run the normal push gate from the selected Build checkout through +`python -B -m emule_workspace local-check --repo +--stage push`. The manager checks renderer capabilities, performs source drift +checks without writes, and stages into a unique inherited +`EMULEBB_WORKSPACE_OUTPUT_ROOT/artifacts/public-presence//site` child. +It validates the staged site, documentation links and stable installer checksum. +Do not precreate that `site` child or use source-local `.local` output. Explicit +developer regeneration of tracked HTML remains supported by the renderer's +default output mode; it is separate from validation and deployment staging. + `.github/workflows/pages.yml` performs the same contract, generated-output, link, static-site, and installer checks. It uploads only the clean staged site artifact. Pushes to `main` and manual dispatches may deploy; scheduled runs are From 72566500bedc447b934afed6bb49f00867af3211 Mon Sep 17 00:00:00 2001 From: Threepwood-7 Date: Tue, 10 Nov 2026 13:54:32 +0100 Subject: [PATCH 2/5] feat(web): stage renderer output in owned external paths (RUST-CI-046) --- tests/test_renderer_staging.py | 82 ++++++++++++++++++++++++++++++++++ tools/render_pages.py | 75 +++++++++++++++++++++++-------- 2 files changed, 139 insertions(+), 18 deletions(-) create mode 100644 tests/test_renderer_staging.py diff --git a/tests/test_renderer_staging.py b/tests/test_renderer_staging.py new file mode 100644 index 0000000..33e5533 --- /dev/null +++ b/tests/test_renderer_staging.py @@ -0,0 +1,82 @@ +"""The manager can validate and stage Pages without modifying source or old evidence.""" +import contextlib +import importlib.util +import io +import json +import os +from pathlib import Path +import sys +import tempfile +import unittest +from unittest.mock import patch + +SPEC = importlib.util.spec_from_file_location("pages_renderer", Path(__file__).parents[1] / "tools/render_pages.py") +renderer = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = renderer +SPEC.loader.exec_module(renderer) + + +class RendererStagingTests(unittest.TestCase): + def setUp(self): + self.scratch = tempfile.TemporaryDirectory(dir=os.environ.get("EMULEBB_WORKSPACE_OUTPUT_ROOT")) + self.addCleanup(self.scratch.cleanup) + self.base = Path(self.scratch.name) + self.workspace = self.base / "workspace" + self.source = self.workspace / "pages" + self.output = self.base / "output" + self.source.mkdir(parents=True) + self.output.mkdir() + self.site = self.output / "artifacts/public-presence/test-run/site" + environment = patch.dict(os.environ, {"EMULEBB_WORKSPACE_ROOT": str(self.workspace), + "EMULEBB_WORKSPACE_OUTPUT_ROOT": str(self.output)}) + environment.start() + self.addCleanup(environment.stop) + + def test_capability_probe_requires_no_environment_or_source_reads(self): + with patch.dict(os.environ, {}, clear=True), patch.object(renderer, "load_public_presence_contract", side_effect=AssertionError("source read")): + captured = io.StringIO() + with contextlib.redirect_stdout(captured): + self.assertEqual(renderer.main(["--capabilities"]), 0) + self.assertEqual(json.loads(captured.getvalue()), { + "schema": "emulebb.pages.renderer-capabilities/1", + "outputStaging": {"protocol": 1, "boundary": "workspace-output", "sourceWrites": False}, "check": True}) + self.assertFalse(self.site.exists()) + + def test_new_external_site_is_created_without_replacing_existing_evidence(self): + self.site.parent.mkdir(parents=True) + evidence = self.site.parent / "source-receipt.json" + evidence.write_text("retained", encoding="utf-8") + renderer.prepare_staging_root(self.source, self.site, self.output) + self.assertTrue(self.site.is_dir()) + with self.assertRaisesRegex(ValueError, "already exists"): + renderer.prepare_staging_root(self.source, self.site, self.output) + self.assertEqual(evidence.read_text(encoding="utf-8"), "retained") + + def test_root_source_relative_and_wrong_boundary_paths_are_rejected(self): + for destination, boundary in ((self.output, self.output), (self.source / ".local/site", self.output), + (Path(".local/site"), self.output), (self.site, self.base)): + with self.subTest(destination=destination), self.assertRaises(ValueError): + renderer.prepare_staging_root(self.source, destination, boundary) + self.assertFalse(self.site.exists()) + self.assertFalse((self.source / ".local").exists()) + + def test_redirected_run_parent_is_refused_before_creating_any_site(self): + redirected = self.base / "redirected" + redirected.mkdir() + self.site.parent.parent.mkdir(parents=True) + try: + self.site.parent.symlink_to(redirected, target_is_directory=True) + except OSError as error: + self.skipTest(f"Host cannot create this isolated directory symlink: {error}") + with self.assertRaisesRegex(ValueError, "redirect"): + renderer.prepare_staging_root(self.source, self.site, self.output) + self.assertFalse((redirected / "site").exists()) + + def test_source_drift_check_never_writes_rendered_content(self): + page = self.source / "index.html" + page.write_text("original", encoding="utf-8") + with patch.object(renderer, "load_public_presence_contract", return_value={}), patch.object(renderer, "render_outputs", return_value={Path("index.html"): "different"}): + with contextlib.redirect_stderr(io.StringIO()): + self.assertEqual(renderer.main(["--root", str(self.source), "--check"]), 1) + self.assertEqual(page.read_text(encoding="utf-8"), "original") + self.assertFalse(self.site.exists()) diff --git a/tools/render_pages.py b/tools/render_pages.py index 264bc81..255a70f 100644 --- a/tools/render_pages.py +++ b/tools/render_pages.py @@ -7,6 +7,7 @@ import datetime as dt import difflib import json +import os import posixpath import re import shutil @@ -1237,16 +1238,46 @@ def write_outputs(root: Path, outputs: dict[Path, str], check: bool) -> int: return 1 if failures else 0 -def prepare_staging_root(source_root: Path, output_root: Path) -> None: - """Create a clean staging directory below the repository-local .local tree.""" - - allowed_root = (source_root / ".local").resolve() - resolved_output = output_root.resolve() - if resolved_output == allowed_root or not resolved_output.is_relative_to(allowed_root): - raise ValueError(f"--output-root must be a child of {allowed_root}") - if resolved_output.exists(): - shutil.rmtree(resolved_output) - resolved_output.mkdir(parents=True) +def prepare_staging_root(source_root: Path, output_root: Path, workspace_output_root: Path | None) -> None: + """Create one new site child under the inherited external output boundary. + + Existing destinations and redirected descendants are refused before writes; + staging never removes a caller-supplied directory. + """ + inherited_output = os.environ.get("EMULEBB_WORKSPACE_OUTPUT_ROOT", "") + inherited_workspace = os.environ.get("EMULEBB_WORKSPACE_ROOT", "") + if not inherited_output or not inherited_workspace: + raise ValueError("External staging requires inherited workspace and output roots") + boundary, workspace = Path(inherited_output), Path(inherited_workspace) + if (not boundary.is_absolute() or not workspace.is_absolute() or not boundary.is_dir() + or not workspace.is_dir() or workspace_output_root is None or not workspace_output_root.is_absolute() + or workspace_output_root.resolve() != boundary.resolve()): + raise ValueError("--workspace-output-root must identify the existing inherited output root") + if boundary.resolve().is_relative_to(workspace.resolve()): + raise ValueError("Workspace output must be outside the source workspace") + if not source_root.resolve().is_relative_to(workspace.resolve()): + raise ValueError("Selected Pages source must belong to the inherited source workspace") + if not output_root.is_absolute(): + raise ValueError("--output-root requires an absolute external site child") + try: + parts = output_root.relative_to(boundary).parts + except ValueError as error: + raise ValueError("--output-root must stay below the inherited output root") from error + if (len(parts) != 4 or parts[:2] != ("artifacts", "public-presence") or parts[-1] != "site" + or re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,79}", parts[2]) is None): + raise ValueError("Staging requires OUTPUT/artifacts/public-presence//site") + candidate = boundary + for part in parts: + candidate /= part + if candidate.is_symlink() or candidate.is_junction(): + raise ValueError("Staging descendants must not redirect through links or junctions") + if output_root.exists(): + raise ValueError("Staging destination already exists; select a new owned run") + if not output_root.resolve().is_relative_to(boundary.resolve()): + raise ValueError("Staging destination escapes the inherited output boundary") + # WHY: validation must never erase prior evidence or a source checkout. + # Exclusive creation of a fresh bounded site replaces recursive cleanup. + output_root.mkdir(parents=True, exist_ok=False) def copy_static_site_files(source_root: Path, output_root: Path) -> None: @@ -1262,22 +1293,29 @@ def copy_static_site_files(source_root: Path, output_root: Path) -> None: shutil.copy2(source, destination) -def parse_args() -> argparse.Namespace: +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: """Parse render command arguments.""" parser = argparse.ArgumentParser(description="Render the eMuleBB static pages.") parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) parser.add_argument("--contract", type=Path, default=DEFAULT_PUBLIC_PRESENCE_CONTRACT) - parser.add_argument("--output-root", type=Path, help="Stage a clean deployable site below ROOT/.local.") + parser.add_argument("--capabilities", action="store_true", help="Print supported protocols without rendering or writing.") + parser.add_argument("--workspace-output-root", type=Path, help="Explicit inherited external output boundary.") + parser.add_argument("--output-root", type=Path, help="Stage a new OUTPUT/artifacts/public-presence//site child.") parser.add_argument("--lastmod", default=dt.date.today().isoformat()) parser.add_argument("--check", action="store_true", help="Fail if generated files differ.") - return parser.parse_args() + return parser.parse_args(argv) -def main() -> int: +def main(argv: list[str] | None = None) -> int: """Render or check the generated static pages.""" - args = parse_args() + args = parse_args(argv) + if args.capabilities: + print(json.dumps({"schema": "emulebb.pages.renderer-capabilities/1", + "outputStaging": {"protocol": 1, "boundary": "workspace-output", "sourceWrites": False}, + "check": True}, sort_keys=True)) + return 0 try: dt.date.fromisoformat(args.lastmod) except ValueError as exc: @@ -1287,11 +1325,12 @@ def main() -> int: contract = load_public_presence_contract(args.contract.resolve()) if args.output_root and args.check: raise ValueError("--check and --output-root cannot be combined") + if args.workspace_output_root and not args.output_root: + raise ValueError("--workspace-output-root requires --output-root") output_root = root if args.output_root: - output_root = args.output_root if args.output_root.is_absolute() else root / args.output_root - output_root = output_root.resolve() - prepare_staging_root(root, output_root) + output_root = args.output_root + prepare_staging_root(root, output_root, args.workspace_output_root) copy_static_site_files(root, output_root) except (OSError, ValueError, json.JSONDecodeError) as exc: print(f"render error: {exc}", file=sys.stderr) From c61d3b683e7313f17e202a14262972ba0a74735f Mon Sep 17 00:00:00 2001 From: Threepwood-7 Date: Tue, 10 Nov 2026 14:40:45 +0100 Subject: [PATCH 3/5] feat(web): publish current reader routes and approved identity (RUST-CI-046) --- README.md | 48 +++++++---- ar-ae/faq/index.html | 5 +- ar-ae/index.html | 6 +- assets/brand/emulebb-donkey-180.png | Bin 0 -> 46087 bytes assets/brand/emulebb-favicon.ico | Bin 25214 -> 5707 bytes assets/brand/emulebb-logo-256.png | Bin 0 -> 106278 bytes assets/brand/emulebb-logo-384.png | Bin 0 -> 238240 bytes assets/brand/emulebb-logo-512.png | Bin 0 -> 409923 bytes assets/brand/rust-social-preview.png | Bin 0 -> 108889 bytes ast/index.html | 6 +- bg/index.html | 6 +- br/index.html | 6 +- ca-valencia-racv/index.html | 6 +- ca-valencia/index.html | 6 +- ca/index.html | 6 +- cs/index.html | 6 +- da/index.html | 6 +- de/faq/index.html | 5 +- de/index.html | 6 +- el/index.html | 6 +- es/faq/index.html | 5 +- es/index.html | 6 +- et/index.html | 6 +- eu/index.html | 6 +- fa/index.html | 6 +- faq/index.html | 118 ++++++++++++++------------ fi/index.html | 6 +- fr/faq/index.html | 5 +- fr/index.html | 6 +- gl/index.html | 6 +- he/index.html | 6 +- hu/index.html | 6 +- index.html | 96 ++++++++++++--------- it/faq/index.html | 5 +- it/index.html | 6 +- ja/faq/index.html | 5 +- ja/index.html | 6 +- ko/faq/index.html | 5 +- ko/index.html | 6 +- languages/index.html | 20 ++++- lt/index.html | 6 +- lv/index.html | 6 +- mt/index.html | 6 +- nb/index.html | 6 +- nl/faq/index.html | 5 +- nl/index.html | 6 +- nn/index.html | 6 +- pl/faq/index.html | 5 +- pl/index.html | 6 +- pt-br/faq/index.html | 5 +- pt-br/index.html | 6 +- pt-pt/faq/index.html | 5 +- pt-pt/index.html | 6 +- ro/index.html | 6 +- ru/faq/index.html | 5 +- ru/index.html | 6 +- sitemap.xml | 122 +++++++++++++-------------- sl/index.html | 6 +- sq/index.html | 6 +- sv/index.html | 6 +- templates/faq.html.j2 | 16 +++- templates/home.html.j2 | 21 +++-- templates/languages.html.j2 | 20 ++++- tests/test_renderer_staging.py | 17 ++++ tools/render_pages.py | 118 ++++++++++++++++++-------- tr/faq/index.html | 5 +- tr/index.html | 6 +- ug-cn/index.html | 6 +- uk/faq/index.html | 5 +- uk/index.html | 6 +- vi/index.html | 6 +- zh-cn/faq/index.html | 5 +- zh-cn/index.html | 6 +- zh-tw/index.html | 6 +- 74 files changed, 592 insertions(+), 337 deletions(-) create mode 100644 assets/brand/emulebb-donkey-180.png create mode 100644 assets/brand/emulebb-logo-256.png create mode 100644 assets/brand/emulebb-logo-384.png create mode 100644 assets/brand/emulebb-logo-512.png create mode 100644 assets/brand/rust-social-preview.png diff --git a/README.md b/README.md index 2cbb3aa..3fafeb1 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,17 @@ # eMuleBB organization website -This repository publishes the organization GitHub Pages site. The public front -door presents CI-gated `emulebb-rust` nightly builds as the primary active-beta -testing channel and eMuleBB MFC `0.7.x` as the maintained Windows line. Beta 3 -is expected after a few more weeks of implementation and stabilization. Rust -development is best-effort, and contributors are welcome. +The public front door for eMuleBB Rust gives equal space to using the client +and developing with its REST API. CI-gated nightly packages are the active-beta +testing channel; Beta 3 is the next development target. + +**Contents:** [Read the site](#read-the-site) · [Maintain the source](#static-page-production) · [Validate and publish](#validation-and-publication) + +## Read the site + +- [Use eMuleBB Rust](https://emulebb.github.io/emulebb-tooling/products/emulebb-rust/reference/GUIDE-GETTING-STARTED/) +- [Choose a platform and package](https://emulebb.github.io/emulebb-tooling/products/emulebb-rust/reference/GUIDE-DOWNLOADS-PLATFORMS/) +- [Develop with the client](https://emulebb.github.io/emulebb-tooling/products/emulebb-rust/reference/GUIDE-CONTRIBUTING/) +- [Build against the REST API](https://emulebb.github.io/emulebb-tooling/products/emulebb-rust/api/GUIDE-API-CONSUMERS/) The homepage is the single canonical product page. `/rust/` is retained only as a no-index compatibility redirect. Only the homepage and FAQ have localized @@ -22,25 +29,34 @@ fallback, but source edits go through the Jinja2 renderer and the shared `emulebb-tooling\manifests\public-presence.v1.json` contract: ```powershell -python -m pip install -r requirements.txt -python ..\emulebb-tooling\helpers\public-presence.py validate +python -B ..\emulebb-tooling\helpers\public-presence.py validate $lastmod = Get-Date -Format yyyy-MM-dd -python tools\render_pages.py --lastmod $lastmod -python tools\render_pages.py --lastmod $lastmod --check -python tools\render_pages.py --lastmod $lastmod --output-root .local\site +python -B tools\render_pages.py --lastmod $lastmod +python -B tools\render_pages.py --lastmod $lastmod --check ``` Run the validation helper before publishing: ```powershell -python ..\emulebb-tooling\helpers\pages-site-tools.py --pages-root .local\site validate -python ..\emulebb-tooling\helpers\check-tooling-doclinks.py --docs-root ..\emulebb-tooling\docs .local\site +python -B ..\emulebb-tooling\helpers\pages-site-tools.py --pages-root . validate +python -B ..\emulebb-tooling\helpers\check-tooling-doclinks.py --docs-root ..\emulebb-tooling\docs . ``` -`.github/workflows/pages.yml` repeats these checks, verifies -`install.ps1.sha256`, uploads only `.local/site`, and deploys through GitHub -Pages Actions on `main`. Scheduled runs validate contract drift without -publishing. +## Validation and publication + +The workspace manager probes `tools/render_pages.py --capabilities` and stages +the site in a fresh owned `OUTPUT/artifacts/public-presence//site` +directory. Explicit staging requires both `--output-root` and +`--workspace-output-root`; the latter must match the inherited output boundary. +Existing site directories and redirected paths are refused. Keep generated +scratch outside the checkout and leave inherited workspace variables unchanged. + +The proposal-1 identity derivatives are supplied assets. Preserve their bytes, +dimensions and recorded SHA-256 values; do not regenerate them. + +`.github/workflows/pages.yml` repeats the checks, verifies +`install.ps1.sha256`, uploads only the staged site, and deploys through GitHub +Pages Actions on `main`. Scheduled runs validate without publishing. ## Markdown Documentation Publishing diff --git a/ar-ae/faq/index.html b/ar-ae/faq/index.html index 11cff40..ea33bb4 100644 --- a/ar-ae/faq/index.html +++ b/ar-ae/faq/index.html @@ -1,5 +1,5 @@ - + @@ -110,8 +110,9 @@ +