unaltraweb 0.5.0 → 0.6.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/Makefile +18 -11
  3. data/README.md +4 -4
  4. data/_plugins/retained_documents.rb +62 -0
  5. data/docs/_documentation/en/02-tools.md +3 -3
  6. data/docs/_documentation/en/32-development.md +1 -1
  7. data/docs/_documentation/en/40-distribution.md +157 -12
  8. data/docs/_documentation/en/42-docker-image.md +7 -7
  9. data/docs/_documentation/en/45-retained-documents.md +138 -0
  10. data/docs/agents/mcp-contract.md +64 -2
  11. data/docs/agents/owner-closeout-76.md +205 -0
  12. data/docs/agents/owner-followup-76.md +144 -0
  13. data/docs/agents/owner-preparation-2026-09-29.md +254 -0
  14. data/docs/agents/retained-letter-import.md +231 -0
  15. data/docs/agents/visual-companions-0.4.0.md +5 -0
  16. data/lib/unaltraweb.rb +1 -0
  17. data/requirements.txt +2 -0
  18. data/scripts/manual/Dockerfile +7 -4
  19. data/scripts/manual/build_pdf.py +28 -0
  20. data/scripts/manual/templates/manual.tex +1 -0
  21. data/scripts/test_gem_build.py +28 -1
  22. data/scripts/test_wheel_install.py +51 -0
  23. data/scripts/unaltraweb-mcp-bootstrap.sh +33 -6
  24. data/scripts/validate_distribution.py +21 -2
  25. data/src/unaltraweb_mcp/__init__.py +4 -0
  26. data/src/unaltraweb_mcp/artifact-handoff-v1.schema.json +102 -0
  27. data/src/unaltraweb_mcp/artifact_handoff_v1.py +435 -0
  28. data/src/unaltraweb_mcp/artifact_imports.py +368 -0
  29. data/src/unaltraweb_mcp/bundler_runtime.py +214 -0
  30. data/src/unaltraweb_mcp/component-contract.json +20 -20
  31. data/src/unaltraweb_mcp/distribution.py +887 -0
  32. data/src/unaltraweb_mcp/letter_bundle.py +223 -0
  33. data/src/unaltraweb_mcp/pdf_probe.py +63 -0
  34. metadata +16 -2
@@ -62,8 +62,11 @@ def build(output: Path, *, source: Path = ROOT) -> str:
62
62
 
63
63
  def inspect_gem(path: Path) -> None:
64
64
  editorial_files = ["scripts/editorial_check.py", "src/unaltraweb_mcp/editorial.py", "src/unaltraweb_mcp/editorial_sources.py"]
65
- inspection_files = editorial_files + ["scripts/image_background_check.py", "src/unaltraweb_mcp/image_backgrounds.py",
65
+ inspection_files = editorial_files + ["src/unaltraweb_mcp/bundler_runtime.py", "scripts/image_background_check.py", "src/unaltraweb_mcp/image_backgrounds.py",
66
66
  "src/unaltraweb_mcp/image_probe.py", "src/unaltraweb_mcp/processes.py"]
67
+ inspection_files += [f"src/unaltraweb_mcp/{name}" for name in (
68
+ "__init__.py", "distribution.py", "component-contract.json", "component-contract.schema.json",
69
+ "artifact_imports.py", "artifact_handoff_v1.py", "artifact-handoff-v1.schema.json", "letter_bundle.py", "pdf_probe.py")]
67
70
  with tarfile.open(path, mode="r") as package:
68
71
  data_member = package.extractfile("data.tar.gz")
69
72
  if data_member is None:
@@ -83,6 +86,7 @@ def inspect_gem(path: Path) -> None:
83
86
  "_plugins/figure_captions.rb",
84
87
  "_plugins/manual_release_metadata.rb",
85
88
  "_plugins/reproducible_build_time.rb",
89
+ "_plugins/retained_documents.rb",
86
90
  "assets/js/content-search-match.js",
87
91
  "assets/js/content-search.js",
88
92
  "_config.yml",
@@ -134,6 +138,29 @@ def inspect_gem(path: Path) -> None:
134
138
  inspection = json.loads(run(image_command, cwd=site).stdout)
135
139
  if not inspection["ok"] or inspection["images"][0]["state"] != "transparent" or not inspection["warnings"]:
136
140
  raise RuntimeError(f"Gem-native SVG background inspection failed: {inspection}")
141
+ # This is the synthetic domain fixture, not published-Carta acceptance.
142
+ # Only its data crosses into the isolated extracted-gem subprocess.
143
+ sys.path.insert(0, str(ROOT / "test"))
144
+ from test_artifact_imports import fixture
145
+
146
+ bundle_sha, template_sha = fixture(site / "incoming/letter")
147
+ (site / ".gitignore").write_text("tmp/\n", encoding="utf-8")
148
+ run(["git", "init", "--quiet"], cwd=site)
149
+ isolated = """
150
+ import json, sys
151
+ from pathlib import Path
152
+ sys.path.insert(0, sys.argv[1])
153
+ from unaltraweb_mcp import artifact_imports, letter_bundle, artifact_handoff_v1, distribution, processes
154
+ for module in (artifact_imports, letter_bundle, artifact_handoff_v1, distribution, processes):
155
+ assert Path(module.__file__).is_relative_to(Path(sys.argv[1])), module.__file__
156
+ letter_bundle.TEMPLATES['default-letter.latex'] = sys.argv[4]
157
+ project = Path(sys.argv[2])
158
+ result = artifact_imports.import_bundle(project, 'incoming/letter/bundle.json', sys.argv[3],
159
+ 'letter', '_pages/en/letter.md', dry_run=False, confirm_import=True)
160
+ print(json.dumps(result))
161
+ raise SystemExit(0 if result['ok'] else 1)
162
+ """
163
+ run([sys.executable, "-I", "-c", isolated, str(core / "src"), str(site), bundle_sha, template_sha], cwd=site)
137
164
 
138
165
 
139
166
  def main() -> int:
@@ -35,6 +35,9 @@ def main() -> int:
35
35
  source,
36
36
  ignore=shutil.ignore_patterns(
37
37
  ".git",
38
+ ".venv",
39
+ ".cache",
40
+ ".jekyll-cache",
38
41
  "build",
39
42
  "dist",
40
43
  "*.egg-info",
@@ -58,6 +61,21 @@ def main() -> int:
58
61
  raise RuntimeError(f"Expected one wheel, found: {wheels}")
59
62
  with zipfile.ZipFile(wheels[0]) as archive:
60
63
  names = archive.namelist()
64
+ launcher_prefix = f"{wheels[0].name.split('-')[0]}-{wheels[0].name.split('-')[1]}.data/data/share/unaltraweb-launcher/"
65
+ launcher_files = {
66
+ "Makefile": "packaging/launcher/Makefile",
67
+ "mcp-factory.yml": "mcp-factory.yml",
68
+ **{f"scripts/{name}.sh": f"scripts/{name}.sh" for name in (
69
+ "unaltraweb-mcp-bootstrap", "unaltraweb-mcp-project-id",
70
+ "unaltraweb-mcp-cleanup", "unaltraweb-docker-mount",
71
+ )},
72
+ }
73
+ actual_launcher = {name.removeprefix(launcher_prefix) for name in names if name.startswith(launcher_prefix)}
74
+ if actual_launcher != set(launcher_files):
75
+ raise RuntimeError(f"Wheel launcher closure mismatch: {actual_launcher}")
76
+ for installed_path, source_path in launcher_files.items():
77
+ if archive.read(launcher_prefix + installed_path) != (ROOT / source_path).read_bytes():
78
+ raise RuntimeError(f"Wheel launcher asset differs from source: {installed_path}")
61
79
  forbidden_roots = {"docs", "scripts", "_layouts", "_includes", "_sass"}
62
80
  leaked = []
63
81
  for name in names:
@@ -71,6 +89,12 @@ def main() -> int:
71
89
  raise RuntimeError(f"Factory assets leaked into wheel: {leaked}")
72
90
  required = [
73
91
  "unaltraweb_mcp/calibre_import.py",
92
+ "unaltraweb_mcp/bundler_runtime.py",
93
+ "unaltraweb_mcp/artifact_imports.py",
94
+ "unaltraweb_mcp/artifact_handoff_v1.py",
95
+ "unaltraweb_mcp/artifact-handoff-v1.schema.json",
96
+ "unaltraweb_mcp/letter_bundle.py",
97
+ "unaltraweb_mcp/pdf_probe.py",
74
98
  "unaltraweb_mcp/component-contract.json",
75
99
  "unaltraweb_mcp/component-contract.schema.json",
76
100
  "unaltraweb_mcp/manual_pdf_preview.py",
@@ -105,6 +129,33 @@ def main() -> int:
105
129
  python = environment / "bin/python"
106
130
  cli = environment / "bin/unaltraweb-mcp"
107
131
  run([str(python), "-m", "pip", "install", "--no-deps", str(wheels[0])], cwd=temp, env=env)
132
+ docker_cli = environment / "bin/unaltraweb-mcp-docker"
133
+ launcher = Path(run([str(docker_cli), "path"], cwd=temp).stdout.strip())
134
+ if launcher != environment / "share/unaltraweb-launcher":
135
+ raise RuntimeError(f"Launcher did not resolve its installed wheel data: {launcher}")
136
+ if run([str(docker_cli), "manifest"], cwd=temp).stdout != (ROOT / "mcp-factory.yml").read_text(encoding="utf-8"):
137
+ raise RuntimeError("Installed native manifest drifted from discovery contract")
138
+ no_consumer_env = {key: value for key, value in env.items() if key not in {"MCP_CONSUMER_WORKSPACE", "UNALTRAWEB_PROJECT"}}
139
+ refused = run([str(docker_cli), "serve"], cwd=temp, env=no_consumer_env, expected=2)
140
+ if "requires --project" not in refused.stderr:
141
+ raise RuntimeError("Installed launcher accepted an implicit consumer")
142
+ fake_bin = temp / "fake-bin"
143
+ fake_bin.mkdir()
144
+ fake_docker = fake_bin / "docker"
145
+ fake_docker.write_text("#!/bin/sh\n[ \"$1 $2\" = 'image inspect' ] || exit 99\n[ \"$5\" = \"$EXPECTED_IMAGE\" ] || exit 98\nprintf 'sha256:fixture\\n'\n", encoding="utf-8")
146
+ fake_docker.chmod(0o755)
147
+ expected_image = json.loads((ROOT / "src/unaltraweb_mcp/component-contract.json").read_text())["components"]["mcp"]["reference"]
148
+ launcher_env = {**no_consumer_env, "PATH": f"{fake_bin}:{env['PATH']}", "EXPECTED_IMAGE": expected_image}
149
+ launcher_env.pop("UNALTRAWEB_MCP_IMAGE", None)
150
+ launcher_env.pop("MCP_RELEASE_IMAGE", None)
151
+ prepared_image = run([str(docker_cli), "prepare"], cwd=temp, env=launcher_env).stdout.strip()
152
+ if prepared_image != "sha256:fixture":
153
+ raise RuntimeError("Installed preparation did not use the packaged bootstrap")
154
+ run(["make", "--silent", "--no-print-directory", "-C", str(launcher), "mcp-build"], cwd=temp, env=launcher_env)
155
+ run([str(docker_cli), "prepare", "--image", "explicit:override"], cwd=temp,
156
+ env={**launcher_env, "EXPECTED_IMAGE": "explicit:override", "UNALTRAWEB_MCP_IMAGE": "environment:override"})
157
+ run([str(docker_cli), "prepare"], cwd=temp,
158
+ env={**launcher_env, "EXPECTED_IMAGE": "environment:override", "UNALTRAWEB_MCP_IMAGE": "environment:override"})
108
159
  version = run([str(cli), "version"], cwd=temp).stdout.strip()
109
160
  doctor = json.loads(run([str(cli), "doctor"], cwd=temp).stdout)
110
161
  if not doctor["ok"] or doctor["mode"] != "wheel" or not doctor["limited"]:
@@ -3,15 +3,18 @@ set -eu
3
3
 
4
4
  usage() {
5
5
  cat <<'USAGE'
6
- Usage: unaltraweb-mcp-bootstrap [--project PATH] [--image IMAGE]
6
+ Usage: unaltraweb-mcp-bootstrap [--project PATH] [--image IMAGE] [--prepare|--check|--smoke]
7
7
 
8
8
  Start one Dockerized unaltraweb MCP stdio server for the selected workspace.
9
9
  If --project is omitted, MCP_CONSUMER_WORKSPACE is used, then the current directory.
10
+ --prepare inspects or pulls the selected image without mounting a consumer.
11
+ --check and --smoke require that image locally; neither builds nor pulls.
10
12
  USAGE
11
13
  }
12
14
 
13
- image="${UNALTRAWEB_MCP_IMAGE:-ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.5.0}"
15
+ image="${UNALTRAWEB_MCP_IMAGE:-ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:908b4ce54c7bdf355e14ed55b31ed4b9baae319e211af90004a680d1d1cb8692}"
14
16
  project="${MCP_CONSUMER_WORKSPACE:-${UNALTRAWEB_PROJECT:-}}"
17
+ operation=serve
15
18
 
16
19
  while [ "$#" -gt 0 ]; do
17
20
  case "$1" in
@@ -25,6 +28,11 @@ while [ "$#" -gt 0 ]; do
25
28
  image="$2"
26
29
  shift 2
27
30
  ;;
31
+ --prepare|--check|--smoke)
32
+ [ "$operation" = serve ] || { printf '%s\n' 'Select only one operation.' >&2; exit 2; }
33
+ operation="${1#--}"
34
+ shift
35
+ ;;
28
36
  -h|--help)
29
37
  usage
30
38
  exit 0
@@ -37,6 +45,28 @@ while [ "$#" -gt 0 ]; do
37
45
  esac
38
46
  done
39
47
 
48
+ resolve_image() {
49
+ if ! resolved_image="$(docker image inspect --format '{{.Id}}' "$image" 2>/dev/null)"; then
50
+ case "$operation" in
51
+ check|smoke)
52
+ printf 'Image is not prepared: %s. Run --prepare first.\n' "$image" >&2
53
+ exit 1
54
+ ;;
55
+ esac
56
+ docker pull "$image" >&2
57
+ resolved_image="$(docker image inspect --format '{{.Id}}' "$image")"
58
+ fi
59
+ }
60
+
61
+ if [ "$operation" != serve ]; then
62
+ resolve_image
63
+ case "$operation" in
64
+ prepare) printf '%s\n' "$resolved_image"; exit 0 ;;
65
+ check) exec docker run --rm --network none --user "$(id -u):$(id -g)" -e HOME=/tmp --entrypoint unaltraweb-mcp "$resolved_image" version ;;
66
+ smoke) exec docker run --rm --network none --user "$(id -u):$(id -g)" -e HOME=/tmp --entrypoint python3 "$resolved_image" /opt/unaltraweb/test/mcp_smoke.py ;;
67
+ esac
68
+ fi
69
+
40
70
  if [ -z "$project" ]; then
41
71
  project="$PWD"
42
72
  fi
@@ -80,10 +110,7 @@ if [ -S "$docker_socket" ]; then
80
110
  esac
81
111
  fi
82
112
 
83
- if ! resolved_image="$(docker image inspect --format '{{.Id}}' "$image" 2>/dev/null)"; then
84
- docker pull "$image" >/dev/null
85
- resolved_image="$(docker image inspect --format '{{.Id}}' "$image")"
86
- fi
113
+ resolve_image
87
114
  image_reference="$image"
88
115
  case "$image_reference" in
89
116
  ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:*) ;;
@@ -75,7 +75,7 @@ def component_version_errors(contract: dict[str, Any]) -> list[str]:
75
75
  """Retain the real version of already-published, digest-pinned workers."""
76
76
  version = str(contract["release"]["version"])
77
77
  errors = []
78
- reusable_workers = {"compute_python", "compute_r", "web_capture"}
78
+ reusable_workers = {"compute_python", "compute_r", "web_capture", "manual_pdf"}
79
79
  for component_id in ["gem", "wheel", "runtime", "mcp", "compute_python", "compute_r", "web_capture", "manual_pdf"]:
80
80
  selected = contract["components"][component_id]
81
81
  if (component_id in reusable_workers and selected["kind"] == "container"
@@ -168,6 +168,20 @@ def validate(root: Path = ROOT) -> list[str]:
168
168
  errors.append("component contract schema does not select version 1")
169
169
 
170
170
  pyproject = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))
171
+ launcher_files = {
172
+ "share/unaltraweb-launcher": ["packaging/launcher/Makefile", "mcp-factory.yml"],
173
+ "share/unaltraweb-launcher/scripts": [
174
+ "scripts/unaltraweb-mcp-bootstrap.sh", "scripts/unaltraweb-mcp-project-id.sh",
175
+ "scripts/unaltraweb-mcp-cleanup.sh", "scripts/unaltraweb-docker-mount.sh",
176
+ ],
177
+ }
178
+ if pyproject.get("tool", {}).get("setuptools", {}).get("data-files") != launcher_files:
179
+ errors.append("wheel must distribute exactly the native Docker launcher and its helper closure")
180
+ launcher_image = make_value(root / "Makefile", "MCP_RELEASE_IMAGE")
181
+ if not re.fullmatch(r"ghcr\.io/dosquartsdedocs/unaltraweb-mcp@sha256:[0-9a-f]{64}", launcher_image):
182
+ errors.append("MCP_RELEASE_IMAGE must select a full GHCR MCP digest")
183
+ if make_value(root / "packaging/launcher/Makefile", "MCP_RELEASE_IMAGE") != component_reference("mcp"):
184
+ errors.append("installed launcher must select its own package release image")
171
185
  project = pyproject.get("project", {})
172
186
  if "version" in project or project.get("dynamic") != ["version"]:
173
187
  errors.append("pyproject version must be derived from the package component contract")
@@ -242,7 +256,7 @@ def validate(root: Path = ROOT) -> list[str]:
242
256
  pinned_text = {
243
257
  "scripts/computations/render.py": [component_reference("compute_python"), component_reference("compute_r")],
244
258
  "scripts/web_captures/render.py": [component_reference("web_capture")],
245
- "scripts/unaltraweb-mcp-bootstrap.sh": [component_reference("mcp")],
259
+ "scripts/unaltraweb-mcp-bootstrap.sh": [launcher_image],
246
260
  "Dockerfile.mcp": [component_reference("runtime")],
247
261
  }
248
262
  for relative, references in pinned_text.items():
@@ -485,6 +499,11 @@ def publish_ref_errors(
485
499
  if not isinstance(component, dict):
486
500
  errors.append(f"unknown publish component: {component_id}")
487
501
  continue
502
+ if (component_id == "manual_pdf" and component.get("release_status") == "released"
503
+ and re.fullmatch(r"ghcr\.io/dosquartsdedocs/unaltraweb-manual-pdf@sha256:[0-9a-f]{64}", str(component.get("reference", "")))):
504
+ # Already-published, unchanged worker. It is not a new candidate for
505
+ # this coordinated core release and receives no new semver identity.
506
+ continue
488
507
  if str(component.get("version") or "") != version:
489
508
  errors.append(f"{component_id} publish version does not match distribution release {version}")
490
509
  if str(component.get("release") or "") != release_tag:
@@ -0,0 +1,4 @@
1
+ from .distribution import distribution_version
2
+
3
+
4
+ __version__ = distribution_version()
@@ -0,0 +1,102 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "MCP artifact handoff v1 (review candidate)",
4
+ "oneOf": [
5
+ {"$ref": "#/$defs/bundle"},
6
+ {"$ref": "#/$defs/integration"}
7
+ ],
8
+ "$defs": {
9
+ "id": {"type": "string", "pattern": "^[a-z][a-z0-9._-]{0,79}$"},
10
+ "hash": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
11
+ "revision": {"type": "string", "pattern": "^(git:([0-9a-f]{40}|[0-9a-f]{64})|sha256:[0-9a-f]{64})$"},
12
+ "path": {"type": "string", "minLength": 1, "maxLength": 1024},
13
+ "runtime": {
14
+ "type": "object", "additionalProperties": false,
15
+ "required": ["name", "revision"],
16
+ "properties": {
17
+ "name": {"$ref": "#/$defs/id"},
18
+ "revision": {"$ref": "#/$defs/revision"}
19
+ }
20
+ },
21
+ "actor": {
22
+ "type": "object", "additionalProperties": false,
23
+ "required": ["name", "version", "revision", "runtimes"],
24
+ "properties": {
25
+ "name": {"$ref": "#/$defs/id"},
26
+ "version": {"type": "string", "minLength": 1, "maxLength": 80, "pattern": "^[A-Za-z0-9][A-Za-z0-9._+-]*$"},
27
+ "revision": {"$ref": "#/$defs/revision"},
28
+ "runtimes": {"type": "array", "maxItems": 32, "items": {"$ref": "#/$defs/runtime"}}
29
+ }
30
+ },
31
+ "dependency": {
32
+ "type": "object", "additionalProperties": false,
33
+ "required": ["id", "sha256"],
34
+ "properties": {
35
+ "id": {"$ref": "#/$defs/id"},
36
+ "sha256": {"$ref": "#/$defs/hash"}
37
+ }
38
+ },
39
+ "upstream": {
40
+ "type": "object", "additionalProperties": false,
41
+ "required": ["dependency", "file"],
42
+ "properties": {
43
+ "dependency": {"$ref": "#/$defs/id"},
44
+ "file": {"$ref": "#/$defs/id"}
45
+ }
46
+ },
47
+ "file": {
48
+ "type": "object", "additionalProperties": false,
49
+ "required": ["id", "path", "kind", "role", "ownership", "sha256", "bytes"],
50
+ "properties": {
51
+ "id": {"$ref": "#/$defs/id"},
52
+ "path": {"$ref": "#/$defs/path"},
53
+ "kind": {"enum": ["input", "output", "evidence"]},
54
+ "role": {"$ref": "#/$defs/id"},
55
+ "ownership": {"enum": ["author", "producer"]},
56
+ "sha256": {"$ref": "#/$defs/hash"},
57
+ "bytes": {"type": "integer", "minimum": 0, "maximum": 536870912},
58
+ "variant_of": {"$ref": "#/$defs/id"},
59
+ "from": {"$ref": "#/$defs/upstream"}
60
+ }
61
+ },
62
+ "bundle": {
63
+ "type": "object", "additionalProperties": false,
64
+ "required": ["schema_version", "kind", "producer", "request", "files", "dependencies"],
65
+ "properties": {
66
+ "schema_version": {"type": "integer", "const": 1},
67
+ "kind": {"const": "mcp-artifact-bundle"},
68
+ "producer": {"$ref": "#/$defs/actor"},
69
+ "request": {"$ref": "#/$defs/id"},
70
+ "files": {"type": "array", "minItems": 2, "maxItems": 10000, "items": {"$ref": "#/$defs/file"}},
71
+ "dependencies": {"type": "array", "maxItems": 63, "items": {"$ref": "#/$defs/dependency"}}
72
+ }
73
+ },
74
+ "retained_bundle": {
75
+ "type": "object", "additionalProperties": false,
76
+ "required": ["path", "sha256"],
77
+ "properties": {
78
+ "path": {"$ref": "#/$defs/path"},
79
+ "sha256": {"$ref": "#/$defs/hash"}
80
+ }
81
+ },
82
+ "mapping": {
83
+ "type": "object", "additionalProperties": false,
84
+ "required": ["source", "destination"],
85
+ "properties": {
86
+ "source": {"$ref": "#/$defs/path"},
87
+ "destination": {"$ref": "#/$defs/path"}
88
+ }
89
+ },
90
+ "integration": {
91
+ "type": "object", "additionalProperties": false,
92
+ "required": ["schema_version", "kind", "integrator", "bundle", "mappings"],
93
+ "properties": {
94
+ "schema_version": {"type": "integer", "const": 1},
95
+ "kind": {"const": "mcp-artifact-integration"},
96
+ "integrator": {"$ref": "#/$defs/actor"},
97
+ "bundle": {"$ref": "#/$defs/retained_bundle"},
98
+ "mappings": {"type": "array", "minItems": 1, "maxItems": 10000, "items": {"$ref": "#/$defs/mapping"}}
99
+ }
100
+ }
101
+ }
102
+ }