unaltraweb 0.3.0 → 0.4.0

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
- title: Use The Docker Image
3
- description: Runtime image used by local unaltraweb workflows.
2
+ title: Use The Docker Images
3
+ description: Site, MCP, and worker images used by local unaltraweb workflows.
4
4
  lang: en
5
5
  ref: docker_image
6
6
  profiles:
@@ -11,17 +11,25 @@ documentation_profiles:
11
11
  section: Work Locally
12
12
  weight: 140
13
13
  permalink: "/docker-image/"
14
- nav_title: Docker Image
14
+ nav_title: Docker Images
15
15
  ---
16
- The pending distribution contract selects this eventual shared image, which is published manually from the core repository and may not exist remotely yet:
16
+ Release `v0.3.0` currently publishes two core images. Published generated sites select the self-contained MCP/site image for normal local commands:
17
+
18
+ ```text
19
+ ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.3.0
20
+ ```
21
+
22
+ It contains the Python control plane and the reviewed factory at `/opt/unaltraweb`. Its lower-level base is:
17
23
 
18
24
  ```text
19
25
  ghcr.io/dosquartsdedocs/unaltraweb:0.3.0
20
26
  ```
21
27
 
22
- It provides the runtime environment: Ruby, Bundler, Jekyll system dependencies, ImageMagick, Node for ExecJS and Python tooling.
28
+ The base provides Ruby, Bundler, Jekyll system dependencies, ImageMagick, Node for ExecJS and Python tooling. Generated Make targets run in the MCP image and load layouts, styles and plugins as a path gem from `/opt/unaltraweb`. RubyGems remains an optional native Bundler channel rather than a download required by the local Docker path.
29
+
30
+ The `v0.4.0` candidate source selects matching `unaltraweb:0.4.0` and `unaltraweb-mcp:0.4.0` references. Those tags remain candidate intent, not public installation instructions, until the coordinated receipt, tag, and promotion sequence completes.
23
31
 
24
- The image is not the source of layouts and styles. Those come from the `unaltraweb` gem in the child site's `Gemfile`.
32
+ ContExt prepares and launches the MCP image through the full digest in `MCP_RELEASE_IMAGE`. That post-release pin is separate from the semver scaffold reference and cannot be shadowed by checkout builds, which use local `:dev` names. A new release advances the pin only after its receipt records the published digest.
25
33
 
26
34
  The GHCR package is kept because it makes the local Docker workflow cheap and repeatable. Publishing is manual. Its unprivileged `preflight` job performs package and source checks without a registry login or Docker build. A default-branch run then crosses three separate credential boundaries:
27
35
 
@@ -46,9 +54,11 @@ During local core development, use the locally built image:
46
54
  ```bash
47
55
  docker build -t unaltraweb:dev .
48
56
  make docs-serve DOCKER_IMAGE=unaltraweb:dev
57
+ make mcp-image
58
+ make mcp-smoke-prebuilt MCP_IMAGE=unaltraweb-mcp:dev
49
59
  ```
50
60
 
51
- After the first GHCR publish, make the package public and confirm unauthenticated pulls work.
61
+ Both core GHCR packages are public, and unauthenticated `v0.3.0` pulls have been verified.
52
62
 
53
63
  ## Computation Images
54
64
 
@@ -175,11 +185,11 @@ The current publication workflows build `linux/amd64` images. ARM authors need D
175
185
  Selector-based screenshot authoring uses a separate Playwright image rather than adding Chromium to the Jekyll runtime:
176
186
 
177
187
  ```text
178
- ghcr.io/dosquartsdedocs/unaltraweb-web-capture:0.3.0
188
+ ghcr.io/dosquartsdedocs/unaltraweb-web-capture@sha256:0bf1bc67fe63e1440bffe708a168beefa11c54441650a871ab99380d362f7c1e
179
189
  ```
180
190
 
181
- The image contains pinned Playwright/Chromium, the capture worker, the Python status controller, and the core visual sources used in fingerprints. `make web-capture-image` builds the explicitly named `unaltraweb-web-capture:dev` maintainer image; set `WEB_CAPTURE_IMAGE` to that name when testing it. The manual `Web capture image` workflow publishes default-branch, commit, and semver/release tags to GHCR.
191
+ The `v0.4.0` contract reuses this already-published worker by immutable digest. The image contains pinned Playwright/Chromium, the capture worker, the Python status controller, and the core visual sources used in fingerprints. `make web-capture-image` builds the explicitly named `unaltraweb-web-capture:dev` maintainer image; set `WEB_CAPTURE_IMAGE` to that name when testing it. The manual `Web capture image` workflow publishes default-branch, commit, and semver/release tags to GHCR.
182
192
 
183
- Manual PDF commands similarly consume `ghcr.io/dosquartsdedocs/unaltraweb-manual-pdf:0.3.0` by default. `manual-pdf-image` reuses or pulls that selected image instead of rebuilding it locally. A pending release may therefore fail to pull until it is actually published. Maintainers use `make manual-pdf-image-dev` and then pass `MANUAL_PDF_IMAGE=unaltraweb-manual-pdf:dev` for local PDF runtime changes.
193
+ Published `v0.3.0` sites consume `ghcr.io/dosquartsdedocs/unaltraweb-manual-pdf:0.3.0`. The candidate source selects `ghcr.io/dosquartsdedocs/unaltraweb-manual-pdf:0.4.0`, which must not be adopted until coordinated publication. `manual-pdf-image` reuses or pulls the selected image instead of rebuilding it locally. Maintainers use `make manual-pdf-image-dev` and then pass `MANUAL_PDF_IMAGE=unaltraweb-manual-pdf:dev` for local PDF runtime changes.
184
194
 
185
195
  Rendering creates an ephemeral Docker `--internal` network shared only by Jekyll and Chromium, keeps browser requests on the preview origin, blocks service workers, popups, and WebSockets, drops Linux capabilities, uses a read-only container root and bounded resources, and writes only the declared PNG/SVG outputs under the mounted project. Ordinary checks run without browser execution or network access.
@@ -1,10 +1,12 @@
1
1
  Start or resume work in an `unaltraweb` website workspace.
2
2
 
3
3
  1. Read local `AGENTS.md` if present.
4
- 2. If the workspace has no `_config.yml`, inspect `web://new-web-scaffolds`, then use `new_web` only after the target profile, title, baseurl, url, default language, and maintained languages are known or intentionally left blank.
5
- 3. Inspect `web://site-context` or call `site_context`.
6
- 4. Confirm the configured `unaltraweb.site_profile`, `language_policy`, languages, and enabled features before editing.
7
- 5. Run or inspect `profile_check`, `content_inventory`, `content_approval_inventory`, `translation_plan`, `bibliography_inventory`, and `content_freshness_check`.
8
- 6. Keep edits inside the website workspace. Do not treat chat history as durable project memory.
9
- 7. For visible content changes, run `build_site` or the local Make equivalent before handoff when feasible.
10
- 8. Do not commit or publish visible content changes until the human author has reviewed the served site in a browser and explicitly approved the rendered result.
4
+ 2. Before editing, require the configured MCP control plane's read-only checkout preflight to confirm the primary mutable checkout and the repository's only active editing session. A session that needs a process-held cooperative lease must be launched through the control plane's `exec` wrapper.
5
+ 3. Request `unaltraweb` as the one top-level MCP and let the control plane select its declared dependency closure. Pass the consumer root through `MCP_CONSUMER_WORKSPACE`; never create, switch to, move, prune, repair, or remove linked worktrees implicitly.
6
+ 4. If the workspace has no `_config.yml`, inspect `web://new-web-scaffolds`, then use `new_web` only after the target profile, title, baseurl, url, default language, and maintained languages are known or intentionally left blank.
7
+ 5. Inspect `web://site-context` or call `site_context`.
8
+ 6. Confirm the configured `unaltraweb.site_profile`, `language_policy`, languages, and enabled features before editing.
9
+ 7. Run or inspect `profile_check`, `content_inventory`, `content_approval_inventory`, `translation_plan`, `bibliography_inventory`, and `content_freshness_check`.
10
+ 8. Keep edits inside the website workspace. Do not treat chat history as durable project memory.
11
+ 9. For visible content changes, run `build_site` or the local Make equivalent before handoff when feasible.
12
+ 10. Do not commit or publish visible content changes until the human author has reviewed the served site in a browser and explicitly approved the rendered result.
@@ -8,7 +8,7 @@ The client registration should launch:
8
8
  make --silent --no-print-directory -C ${factoryRoot} mcp-stdio
9
9
  ```
10
10
 
11
- The opened workspace is the consumer website repository. The pending distribution contract selects `ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.3.0` for the factory logic, without asserting that the remote image exists yet. The launcher reuses it locally or attempts to pull it; an unavailable pending release fails visibly rather than triggering a local fallback build. This is the canonical ContExt command from `mcp-factory.yml`; the transport also sets `MCP_CONSUMER_WORKSPACE=${workspaceFolder}` in the child environment:
11
+ The opened workspace is the consumer website repository. ContExt preparation inspects or pulls the full public digest selected by `MCP_RELEASE_IMAGE`, and the launcher executes that exact image; it never falls back to an implicit source build. The pin advances in a separate post-release change only after a receipt records the new digest. This is the canonical ContExt command from `mcp-factory.yml`; the transport also sets `MCP_CONSUMER_WORKSPACE=${workspaceFolder}` in the child environment:
12
12
 
13
13
  ```bash
14
14
  MCP_CONSUMER_WORKSPACE="$PWD" make --silent --no-print-directory -C /path/to/unaltraweb mcp-stdio
@@ -16,7 +16,9 @@ MCP_CONSUMER_WORKSPACE="$PWD" make --silent --no-print-directory -C /path/to/una
16
16
 
17
17
  Replace `/path/to/unaltraweb` with the checkout's absolute path. The bootstrap canonicalizes the inherited environment value after process launch; neither Make nor generated shell source evaluates consumer path text. The declared launcher remains `make`, which ContExt permits for a container runtime without a `runtime.allowed_host_launchers` exception. Restart clients such as OpenCode after changing their MCP registration.
18
18
 
19
- ContExt dependency preparation builds the runtime and required companions but does not initialize consumer content. The transport passes `${workspaceFolder}` only through `MCP_CONSUMER_WORKSPACE`; it never sets `transport.cwd` or embeds the consumer path in a Make assignment. The factory command may therefore use `make -C` without changing or reparsing the selected consumer root. Companion-aware checks and smoke tests include both required providers, while provider updates remain explicit. The manifest does not advertise an `init` command, and both companion dependencies set `init: false`. Use `new_web` explicitly when a new consumer site should be created. Restart long-lived MCP clients after registration, rebuilds, or provider upgrades so their stdio processes use the selected releases.
19
+ ContExt dependency preparation ensures the selected MCP release image and prepares required companions but does not initialize consumer content. The transport passes `${workspaceFolder}` only through `MCP_CONSUMER_WORKSPACE`; it never sets `transport.cwd` or embeds the consumer path in a Make assignment. The factory command may therefore use `make -C` without changing or reparsing the selected consumer root. Companion-aware checks and smoke tests include both required providers, while provider updates remain explicit. The manifest does not advertise an `init` command, and both companion dependencies set `init: false`. Use `new_web` explicitly when a new consumer site should be created. Restart long-lived MCP clients after registration, release-pin changes, or provider upgrades so their stdio processes use the selected releases.
20
+
21
+ Repository editing coordination remains a control-plane responsibility rather than an `unaltraweb` runtime feature. Request one top-level MCP and let the control plane select its declared dependency closure; unrelated user registrations remain configured until explicitly removed and clients reconnect. Before editing, the control plane runs its read-only checkout preflight against the one primary mutable checkout and, when required, holds a process-scoped cooperative lease through its `exec` wrapper. Only one editing session may be active per repository. The control plane must never create, switch to, move, prune, repair, or remove Git worktrees implicitly.
20
22
 
21
23
  For a new agent-driven workspace, create or select the empty Git repository first, open that directory in the IDE, register the factory, restart the client, and then call `new_web`. The tool remains confined to the configured project root and does not accept an arbitrary destination. Generated sites include user-owned `README.md` and `AGENTS.md`, profile-specific source directories, and a managed runtime baseline; `scaffold_sync` never rewrites those user-owned guidance or content files.
22
24
 
@@ -83,6 +85,8 @@ The request digest is SHA-256 over `unaltraweb-companion-receipt-v1\0OWNER\0`, f
83
85
  | `web_capture_render` | Start Jekyll and Chromium on an ephemeral internal Docker network, then publish original PNG plus editable annotated SVG from declared CSS selectors. |
84
86
  | `manual_pdf_status` | Inspect PDF configuration, sources, generated artefacts, published paths, selector, and freshness without Docker, network, or writes. |
85
87
  | `manual_pdf_build` | Build one or all configured language PDFs and first-page cover previews under `tmp/`; `release_selector` defaults to `latest` and is part of the PDF fingerprint. |
88
+ | `manual_pdf_preview_prepare` | Build only stale `latest` PDF languages, then atomically stage ignored, untracked PDF and cover copies for local Jekyll review under a cleanup receipt. Always reports `publishes: false`; disabled PDF configuration is a no-op unless receipt-owned preview files still require cleanup. |
89
+ | `manual_pdf_preview_clean` | Dry-run or remove only preview files whose path, Git state, content hash, and file identity still match the staging receipt. Real cleanup requires explicit confirmation plus the dry-run's exact `receipt_sha256` as `expected_receipt_sha256`, and never publishes. |
86
90
  | `manual_pdf_publish` | Copy built PDFs and covers to configured public assets. Defaults to dry-run; real publication requires explicit confirmation, and the selector must match the build. |
87
91
  | `manual_release_status` | Offline, read-only inspection of selector-bound build receipt, HTML audit, PDF evidence, stable policy, and local candidate state. |
88
92
  | `manual_release_check` | Fail unless the selector-bound candidate exactly matches the current verified source, site, PDF, cover, manifest, and checksums. |
@@ -107,11 +111,13 @@ The request digest is SHA-256 over `unaltraweb-companion-receipt-v1\0OWNER\0`, f
107
111
  | `preview_stop` | Remove only that project's labelled preview container. |
108
112
  | `http_check` | Probe bounded safe paths on the current project's owned labelled preview. The origin is derived internally; redirects and arbitrary origins are rejected. |
109
113
 
110
- Advanced computation, capture, PDF, and bibliometrics tools delegate to factory-owned Make targets against the consumer project. Fresh package scaffolds therefore do not need to copy those implementation targets into each website. A new `unaltremanual` does include a consumer-owned `.unaltraweb/computations.yml` that selects the release's R and Python workers; the first render reuses a local image or pulls that selected image automatically. The worker layers remain external distribution components and are not copied into the wheel or site. The tool names use `bibliometrics_*` even though factory Make targets retain `metrics-*` for backwards compatibility.
114
+ Advanced computation, capture, PDF, and bibliometrics tools delegate to factory-owned Make targets against the consumer project. Fresh package scaffolds therefore do not copy worker implementation into each website. Their managed outer `build`, `test`, and `serve` targets call `manual_pdf_preview_prepare` in a short-lived controller before Jekyll starts. That controller mounts a locally available Docker socket when a stale PDF may need a nested worker, but can still execute a disabled or already-fresh operation when no socket is available; `build-native`, `test-native`, `serve-native`, and the persistent preview container remain socket-free. A new `unaltremanual` does include a consumer-owned `.unaltraweb/computations.yml` that selects the release's R and Python workers; the first render reuses a local image or pulls that selected image automatically. The worker layers remain external distribution components and are not copied into the wheel or site. The tool names use `bibliometrics_*` even though factory Make targets retain `metrics-*` for backwards compatibility.
111
115
 
112
116
  `distribution_doctor` findings always include `code`, `severity`, `expected`, `actual`, and `remediation`. Missing factory assets in a direct wheel install produce healthy limited wheel mode, not a false failure. When Docker checks are requested, doctor uses only `docker version` and `docker image inspect`; it does not pull, build, start, or remove anything.
113
117
 
114
- Manual PDF publication is a local workspace operation: it copies reviewed artefacts from `tmp/manual-pdf/` to configured paths such as `assets/pdf/` and `assets/img/`. It never commits, pushes, creates releases, or writes outside the consumer workspace. Use one selector consistently across `manual_pdf_build`, `manual_pdf_publish`, `build_site`, and `manual_release_prepare`. `latest` is the default; stable selectors use `vYYYY.MM(.N)` and require the consumer repository root to be an exact clean Git checkout without nested repositories, submodules, or clean/smudge filters. Stable Jekyll builds run in an MCP image selected by immutable digest, derive `SOURCE_DATE_EPOCH` from the consumer commit, and record both identities in their version-2 candidate manifest. Run `manual_source_quality_check`, `manual_editorial_quality_check`, `manual_pdf_status`, `manual_pdf_build`, and a `manual_pdf_publish` dry-run before calling `manual_pdf_publish(dry_run=false, confirm_publish=true)`. A stable caller additionally submits the SHA-256 of its checked local `tmp/manual-release/<selector>/release-manifest.json`; only the GitHub workflow has tag and release authority.
118
+ Manual PDF preview staging is distinct from publication. `manual_pdf_preview_prepare` is fixed to `latest`, builds stale languages, requires the repository root plus ignored and untracked public destinations, writes its ignored receipt under `.cache/unaltraweb/`, and never commits, pushes, tags, releases, or deploys. It refuses unmanaged existing files and leaves each current generated-equivalent PDF or cover unowned and unchanged. Confirmed publication first writes expected hashes to `.cache/unaltraweb/manual-pdf-publication-intent.json`, then records non-owning file identity at `.cache/unaltraweb/manual-pdf-publication.json`; an interrupted or later preview cycle can replace only matching deployment products without granting cleanup authority over them. `manual_pdf_preview_clean` preserves any staged file that was edited, replaced, mode-changed, tracked, or made non-ignored and retains the receipt when cleanup conflicts. Its dry-run returns `receipt_sha256`; confirmed cleanup must submit that exact digest so a newly prepared generation cannot be deleted under an older review. Receipt-owned files must be cleaned before PDF output is disabled or real publication. The direct MCP sequence is prepare, `build_site`, `preview_start`, human browser/PDF review, cleanup dry-run, and digest-bound confirmed cleanup.
119
+
120
+ Manual PDF publication is a local workspace operation: it copies reviewed artefacts from `tmp/manual-pdf/` to configured paths such as `assets/pdf/` and `assets/img/`. It never commits, pushes, creates releases, or writes outside the consumer workspace. Use one selector consistently across `manual_pdf_build`, `manual_pdf_publish`, `build_site`, and `manual_release_prepare`. `latest` is the default; stable selectors use `vYYYY.MM(.N)` and require the consumer repository root to be an exact clean Git checkout without nested repositories, submodules, or clean/smudge filters. Stable Jekyll builds run in an MCP image selected by immutable digest, derive `SOURCE_DATE_EPOCH` from the consumer commit, and record both identities in their version-2 candidate manifest. Run `manual_source_quality_check`, `manual_editorial_quality_check`, `manual_pdf_status`, `manual_pdf_preview_prepare`, browser/PDF review, confirmed preview cleanup, and a `manual_pdf_publish` dry-run before calling `manual_pdf_publish(dry_run=false, confirm_publish=true)`. A stable caller additionally submits the SHA-256 of its checked local `tmp/manual-release/<selector>/release-manifest.json`; only the GitHub workflow has tag and release authority.
115
121
 
116
122
  ## New Site Initialization
117
123
 
@@ -145,13 +151,13 @@ Translations are a pre-publication task. They should preserve `ref`, citations,
145
151
 
146
152
  ## Docker Runtime And Preview
147
153
 
148
- `make mcp-build` builds `ghcr.io/dosquartsdedocs/unaltraweb:0.3.0` and then `ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.3.0` locally. `make mcp-smoke` runs a real MCP client/server stdio exchange, compiles a temporary minimal site, and exercises preview start/status/stop. `mcp-stdio` remains dormant until a client launches it; dependency preparation never invokes it.
154
+ `make mcp-build` prepares only the immutable public image selected by `MCP_RELEASE_IMAGE`; it does not build source. `make mcp-image`, `make mcp-check` and `make mcp-smoke` build checkout source under local `unaltraweb:dev` and `unaltraweb-mcp:dev` names. The smoke target also builds `unaltraweb-manual-pdf:dev`, runs a real MCP client/server stdio exchange, builds a stale manual PDF through the short-lived socket-enabled controller, and verifies preview start/status/stop against a persistent container that has no Docker socket. `mcp-stdio` remains dormant until a client launches it; dependency preparation never invokes it.
149
155
 
150
156
  Run `site_doctor` and `site_check`, then resolve any blocking validation result before compiling. `build_site` reuses the active MCP container and the consumer's `build-native` target, and runs the local HTML audit after a successful Jekyll process. The generated `test-native` target also runs `html_audit`. This is intentionally different from the consumer's normal host-side `make build`, which starts a Jekyll container and would create a nested runtime when called from MCP.
151
157
 
152
158
  Make delegation and feasible Docker control calls use one bounded subprocess runner. Status and control commands have short deadlines, builds/renders have target-specific longer deadlines, timeout terminates the process group and returns code `124`, and retained stdout/stderr is capped with explicit truncation fields. Factory commands that promise JSON fail closed when output is empty, malformed, non-object, non-finite, or truncated. Every bind source and target is encoded as a quoted Docker CSV field, so commas or quotes in host paths cannot introduce duplicate mount fields; carriage-return and newline path characters are rejected before canonicalization or mount construction. Computation, capture, and PDF containers carry factory, worker-role, project, and invocation-token labels plus cidfiles; after timeout cleanup selects all four labels and cannot remove unrelated containers.
153
159
 
154
- A preview must outlive one MCP tool invocation, so it runs in a separate container made from the same MCP/Jekyll image. Its deterministic name is derived from the canonical host project path and it carries the factory, role, and project labels. Its isolated container always listens on port `4000`; the default host port is allocated atomically by Docker on loopback and is reported as `preview_status.port`, preventing the old cross-project collision on host port `4000`. Starting an already-running preview probes it again instead of creating a duplicate. A preview created under the former fixed-port default is accepted as compatible with the new automatic default until it is stopped; its next start uses dynamic allocation. Changing an explicit requested port or profile still requires stopping first. Stdio session containers intentionally have Docker-generated names so independent clients can run simultaneously, but carry the same stable project ID and labels as previews and capture resources. Stop and cleanup operations select ownership labels before removing anything.
160
+ A preview must outlive one MCP tool invocation, so it runs in a separate container made from the same MCP/Jekyll image. Its deterministic name is derived from the canonical host project path and it carries the factory, role, and project labels. Its isolated container always listens on port `4000`; the default host port is allocated atomically by Docker on loopback and is reported as `preview_status.port`, preventing the old cross-project collision on host port `4000`. Starting an already-running preview probes it again instead of creating a duplicate. A preview created under the former fixed-port default is accepted as compatible with the new automatic default until it is stopped; its next start uses dynamic allocation. Changing an explicit requested port or profile still requires stopping first. Stdio session containers intentionally have Docker-generated names so independent clients can run simultaneously, but carry the same stable project ID and labels as previews and capture resources. That process-level capability does not authorize concurrent editing sessions in one repository. Stop and cleanup operations select ownership labels before removing anything.
155
161
 
156
162
  `preview_status.url` is the browser URL published on host loopback. `preview_status.internal_url` is informational; callers do not pass it to `http_check`. That tool verifies preview ownership, derives the exact container-internal HTTP origin, disables environment proxies, accepts at most 20 local paths within a bounded timeout and response-read budget, and rejects absolute URLs, protocol-relative forms, traversal, fragments, hostile characters, non-2xx results, and redirects. Preview readiness checks the configured home permalink and generated root candidates rather than guessing a language route. `MCP_CONSUMER_WORKSPACE=/canonical/consumer/path make mcp-down` removes containers and networks selected by both `io.context.mcp-factory=unaltraweb` and the stable project label. A simultaneous `MCP_PROJECT_ID` must match that canonical live path. If the original path has moved or disappeared, pass the absent path with the retained ID or explicitly clear `MCP_CONSUMER_WORKSPACE`; only then is the retained ID accepted without path access. `make mcp-down-all` is the explicit maintainer cleanup for every resource carrying the factory label. Neither target deletes images or touches unlabelled resources.
157
163
 
@@ -44,6 +44,10 @@
44
44
  \displaywidowpenalty=10000
45
45
  \setcounter{secnumdepth}{3}
46
46
  \setcounter{tocdepth}{2}
47
+ \makeatletter
48
+ \renewcommand*{\l@section}{\@dottedtocline{1}{1.5em}{3em}}
49
+ \renewcommand*{\l@subsection}{\@dottedtocline{2}{4.5em}{4em}}
50
+ \makeatother
47
51
  \pretocmd{\section}{\Needspace{6\baselineskip}}{}{}
48
52
  \pretocmd{\subsection}{\Needspace{5\baselineskip}}{}{}
49
53
  \pretocmd{\subsubsection}{\Needspace{5\baselineskip}}{}{}
@@ -13,7 +13,7 @@ from pathlib import Path
13
13
 
14
14
 
15
15
  ROOT = Path(__file__).resolve().parents[1]
16
- IMAGE = os.environ.get("DOCKER_IMAGE", "ghcr.io/dosquartsdedocs/unaltraweb:0.3.0")
16
+ IMAGE = os.environ.get("DOCKER_IMAGE", "ghcr.io/dosquartsdedocs/unaltraweb:0.4.0")
17
17
  SELECTOR = "v2026.09"
18
18
  EPOCH = "946684800"
19
19
 
@@ -73,6 +73,7 @@ def main() -> int:
73
73
  "unaltraweb_mcp/calibre_import.py",
74
74
  "unaltraweb_mcp/component-contract.json",
75
75
  "unaltraweb_mcp/component-contract.schema.json",
76
+ "unaltraweb_mcp/manual_pdf_preview.py",
76
77
  "unaltraweb_mcp/manual_release.py",
77
78
  "unaltraweb_mcp/scaffolds/common/AGENTS.md.tmpl",
78
79
  "unaltraweb_mcp/scaffolds/common/Makefile.tmpl",
@@ -123,6 +124,7 @@ def main() -> int:
123
124
  if not created["ok"]:
124
125
  raise RuntimeError(f"new-web failed for {profile} from clean wheel: {created}")
125
126
  readme = (profile_site / "README.md").read_text(encoding="utf-8")
127
+ agents = (profile_site / "AGENTS.md").read_text(encoding="utf-8")
126
128
  collaboration = (profile_site / ".github/CONTRIBUTING.md").read_text(encoding="utf-8")
127
129
  dependabot = (profile_site / ".github/dependabot.yml").read_text(encoding="utf-8")
128
130
  pr_template = (profile_site / ".github/pull_request_template.md").read_text(encoding="utf-8")
@@ -132,7 +134,7 @@ def main() -> int:
132
134
  f"**`{profile}`** profile: {description}",
133
135
  f"## Editable Content For `{profile}`",
134
136
  profile_path,
135
- "only one active editor per file",
137
+ "only one active editing session per repository",
136
138
  "Never edit or commit directly to `main`",
137
139
  "Draft pull request",
138
140
  "Starts the deploy workflow manually",
@@ -146,6 +148,18 @@ def main() -> int:
146
148
  raise RuntimeError(f"generated {profile} README does not explain all profiles")
147
149
  if "correctly named short-lived branch" not in pr_template or "Required local checks and renders pass" not in pr_template:
148
150
  raise RuntimeError(f"generated {profile} pull request template lacks coordination checks")
151
+ agent_markers = [
152
+ "read-only checkout preflight",
153
+ "primary mutable checkout",
154
+ "process-held cooperative lease",
155
+ "`exec` wrapper",
156
+ "one active editing session per repository",
157
+ "one top-level MCP",
158
+ "declared dependency closure",
159
+ "`MCP_CONSUMER_WORKSPACE`",
160
+ ]
161
+ if any(value not in agents for value in agent_markers):
162
+ raise RuntimeError(f"generated {profile} agent contract lacks single-checkout guidance")
149
163
  collaboration_markers = [
150
164
  "content/<issue>-<slug>",
151
165
  "reference/<issue>-<slug>",
@@ -158,20 +172,37 @@ def main() -> int:
158
172
  "integration/<provider>/<issue>-<slug>",
159
173
  "A maintainer must accept an exact reservation",
160
174
  "not broad directories or globs",
161
- "dedicated Git worktree",
162
- "Never share a mutable checkout",
175
+ "one active editing session per repository",
176
+ "primary mutable checkout",
177
+ "read-only checkout preflight",
178
+ "process-held cooperative lease",
179
+ "`exec` wrapper",
180
+ "Never create, switch to, move, prune, repair, or remove Git worktrees implicitly",
181
+ "one top-level MCP",
182
+ "declared dependency closure",
183
+ "`MCP_CONSUMER_WORKSPACE`",
184
+ "factory `build`, `check`, and `smoke` operations",
163
185
  "one atomic bundle",
164
186
  "source, every local data/input file, output, caption-bearing content reference",
165
187
  "immutable release, full commit SHA, or image digest",
166
188
  "Draft pull request after the first coherent change",
167
189
  "delete the local and remote task branch",
168
- "remove the worktree",
190
+ "retain the primary checkout",
169
191
  ]
170
192
  if any(value not in collaboration for value in collaboration_markers):
171
193
  raise RuntimeError(f"generated {profile} collaboration contract is incomplete")
172
194
  if "dependency-name: unaltraweb" not in dependabot or f"dependency-name: {integration['site_deploy_workflow']}" not in dependabot:
173
195
  raise RuntimeError(f"generated {profile} Dependabot policy permits partial integration updates")
174
- combined_guidance = readme + collaboration + pr_template
196
+ combined_guidance = readme + collaboration + pr_template + agents
197
+ obsolete_guidance = [
198
+ "dedicated Git worktree",
199
+ "Never share a mutable checkout",
200
+ "remove the worktree",
201
+ "one active editor per file",
202
+ "no other active editor",
203
+ ]
204
+ if any(value in combined_guidance for value in obsolete_guidance):
205
+ raise RuntimeError(f"generated {profile} guidance retains obsolete worktree coordination")
175
206
  forbidden = ["${{", "secrets.", "GITHUB_TOKEN", "id-token:", "contents: write", "on:\n push:"]
176
207
  if any(value in combined_guidance for value in forbidden):
177
208
  raise RuntimeError(f"generated {profile} editor guidance exposes workflow or secret internals")
@@ -198,6 +229,7 @@ def main() -> int:
198
229
  raise RuntimeError(f"generated {profile} .gitignore ignores arbitrary publication assets")
199
230
  gemfile = (profile_site / "Gemfile").read_text(encoding="utf-8")
200
231
  lockfile = (profile_site / "Gemfile.lock").read_text(encoding="utf-8")
232
+ makefile = (profile_site / "Makefile").read_text(encoding="utf-8")
201
233
  deploy = (profile_site / ".github/workflows/deploy.yml").read_text(encoding="utf-8")
202
234
  if f'git: "{integration["core_repository"]}"' not in gemfile or f'ref: "{integration["core_sha"]}"' not in gemfile:
203
235
  raise RuntimeError(f"generated {profile} Gemfile does not use the reviewed core revision")
@@ -207,6 +239,16 @@ def main() -> int:
207
239
  raise RuntimeError(f"generated {profile} deploy caller does not use the reviewed core revision")
208
240
  if f'manual-pdf-image: "{integration["manual_pdf_image"]}"' not in deploy:
209
241
  raise RuntimeError(f"generated {profile} deploy caller does not use the reviewed PDF image")
242
+ makefile_markers = [
243
+ f"MANUAL_PDF_IMAGE ?= {integration['manual_pdf_image']}",
244
+ "manual-pdf-preview-prepare: runtime-image",
245
+ "manual-pdf-preview-clean: runtime-image",
246
+ "build: manual-pdf-preview-prepare",
247
+ "serve: manual-pdf-preview-prepare",
248
+ "test: manual-pdf-preview-prepare",
249
+ ]
250
+ if any(value not in makefile for value in makefile_markers):
251
+ raise RuntimeError(f"generated {profile} Makefile lacks managed manual PDF preview staging")
210
252
  if f'vegavisuals-sha: "{integration["vegavisuals_sha"]}"' not in deploy:
211
253
  raise RuntimeError(f"generated {profile} deploy caller does not use the reviewed Vega renderer")
212
254
  manifest = json.loads((profile_site / ".unaltraweb/scaffold.json").read_text(encoding="utf-8"))
@@ -10,7 +10,7 @@ If --project is omitted, MCP_CONSUMER_WORKSPACE is used, then the current direct
10
10
  USAGE
11
11
  }
12
12
 
13
- image="${UNALTRAWEB_MCP_IMAGE:-ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.3.0}"
13
+ image="${UNALTRAWEB_MCP_IMAGE:-ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.4.0}"
14
14
  project="${MCP_CONSUMER_WORKSPACE:-${UNALTRAWEB_PROJECT:-}}"
15
15
 
16
16
  while [ "$#" -gt 0 ]; do
@@ -62,6 +62,24 @@ project_id="$(/bin/sh "$script_dir/unaltraweb-mcp-project-id.sh" "$project")"
62
62
  workspace_mount="$(/bin/sh "$script_dir/unaltraweb-docker-mount.sh" "$project" /workspace)"
63
63
  mirror_mount="$(/bin/sh "$script_dir/unaltraweb-docker-mount.sh" "$project" "$project")"
64
64
 
65
+ if [ -S "$docker_socket" ]; then
66
+ unresolved_socket="$docker_socket"
67
+ if ! docker_socket="$(realpath -e -- "$unresolved_socket")"; then
68
+ printf '%s\n' "Cannot resolve Docker socket: $unresolved_socket" >&2
69
+ exit 1
70
+ fi
71
+ if [ "$(stat -c '%h' "$docker_socket")" != 1 ]; then
72
+ printf '%s\n' 'UNALTRAWEB_DOCKER_SOCKET must not have hard-link aliases that could enter the consumer project.' >&2
73
+ exit 1
74
+ fi
75
+ case "$docker_socket" in
76
+ "$project"|"$project"/*)
77
+ printf '%s\n' 'UNALTRAWEB_DOCKER_SOCKET must be outside the consumer project so the persistent preview cannot inherit it.' >&2
78
+ exit 1
79
+ ;;
80
+ esac
81
+ fi
82
+
65
83
  if ! resolved_image="$(docker image inspect --format '{{.Id}}' "$image" 2>/dev/null)"; then
66
84
  docker pull "$image" >/dev/null
67
85
  resolved_image="$(docker image inspect --format '{{.Id}}' "$image")"
@@ -241,7 +241,7 @@ def validate(root: Path = ROOT) -> list[str]:
241
241
  scaffold_templates = {
242
242
  "Gemfile.tmpl": ["__GEM_VERSION__", "__CORE_REPOSITORY__", "__CORE_SHA__"],
243
243
  "Gemfile.lock.tmpl": ["__GEM_VERSION__", "__CORE_REPOSITORY__", "__CORE_SHA__"],
244
- "Makefile.tmpl": ["__MCP_IMAGE__"],
244
+ "Makefile.tmpl": ["__MCP_IMAGE__", "__MANUAL_PDF_IMAGE__"],
245
245
  ".github/workflows/deploy.yml.tmpl": [
246
246
  "__SITE_DEPLOY_WORKFLOW__",
247
247
  "__CORE_SHA__",