unaltraweb 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Makefile +27 -5
- data/docs/_documentation/en/33-reusable-workflows.md +28 -3
- data/docs/_documentation/en/40-distribution.md +57 -19
- data/docs/_documentation/en/43-workspace-path-policies.md +13 -0
- data/docs/_documentation/en/46-live-runtime.md +6 -3
- data/docs/_documentation/en/47-private-practice-pdfs.md +130 -0
- data/docs/agents/consolidation-0.7.2.md +97 -0
- data/docs/agents/d0-runtime-identity.md +22 -5
- data/docs/agents/deploy-provenance-86.md +86 -0
- data/docs/agents/deployment-integration-0.7.1.md +142 -0
- data/docs/agents/manual-authoring-components.md +23 -0
- data/docs/agents/mcp-contract.md +112 -6
- data/docs/agents/owner-closeout-85.md +269 -0
- data/docs/agents/owner-closeout-93.md +185 -0
- data/docs/agents/practice-editions-98.md +816 -0
- data/docs/agents/practice-pdf-installable-98.md +250 -0
- data/docs/agents/practice-runtime-archives-98.md +147 -0
- data/docs/agents/private-practice-verifier-98.md +296 -0
- data/docs/agents/source-ci-98.md +148 -0
- data/scripts/check_practice_verifier_runtime.py +69 -0
- data/scripts/export_practice_runtime_archives.py +138 -0
- data/scripts/manual/filters/practice-finish.lua +36 -0
- data/scripts/manual/filters/practice-layout.lua +84 -0
- data/scripts/manual/practice_pdf.py +31 -0
- data/scripts/manual/render_practice.py +125 -0
- data/scripts/manual/templates/practice.tex +95 -0
- data/scripts/prepare_practice_candidate.py +159 -0
- data/scripts/prepare_practice_editions_candidate.py +119 -0
- data/scripts/test_wheel_install.py +16 -1
- data/scripts/unaltraweb-mcp-bootstrap.sh +1 -1
- data/scripts/validate_distribution.py +51 -5
- data/scripts/validate_workflows.py +33 -4
- data/src/unaltraweb_mcp/component-contract.json +59 -22
- data/src/unaltraweb_mcp/component-contract.schema.json +35 -2
- data/src/unaltraweb_mcp/distribution.py +42 -2
- data/src/unaltraweb_mcp/editorial_sources.py +62 -4
- metadata +22 -2
|
@@ -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")
|
|
@@ -290,7 +304,7 @@ def main() -> int:
|
|
|
290
304
|
raise RuntimeError(f"generated {profile} Gemfile does not use the reviewed core revision")
|
|
291
305
|
if f'revision: {integration["core_sha"]}' not in lockfile:
|
|
292
306
|
raise RuntimeError(f"generated {profile} lockfile does not use the reviewed core revision")
|
|
293
|
-
if f'uses: {integration["site_deploy_workflow"]}@{integration["
|
|
307
|
+
if f'uses: {integration["site_deploy_workflow"]}@{integration["site_deploy_workflow_sha"]}' not in deploy:
|
|
294
308
|
raise RuntimeError(f"generated {profile} deploy caller does not use the reviewed core revision")
|
|
295
309
|
if f'manual-pdf-image: "{integration["manual_pdf_image"]}"' not in deploy:
|
|
296
310
|
raise RuntimeError(f"generated {profile} deploy caller does not use the reviewed PDF image")
|
|
@@ -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:
|
|
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:-}"
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
|
+
import hashlib
|
|
7
8
|
import json
|
|
8
9
|
import os
|
|
9
10
|
import re
|
|
@@ -39,7 +40,8 @@ from unaltraweb_mcp.distribution import ( # noqa: E402
|
|
|
39
40
|
is_mutable_reference,
|
|
40
41
|
validate_component_contract,
|
|
41
42
|
)
|
|
42
|
-
from scripts.validate_workflows import load_workflow_text, reusable_deploy_input_errors # noqa: E402
|
|
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
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
def load_yaml(path: Path) -> dict[str, Any]:
|
|
@@ -88,6 +90,21 @@ def component_version_errors(contract: dict[str, Any]) -> list[str]:
|
|
|
88
90
|
return errors
|
|
89
91
|
|
|
90
92
|
|
|
93
|
+
def selected_deployment_errors(contract: dict[str, Any], workflow: dict[str, Any], provider_contract: dict[str, Any]) -> list[str]:
|
|
94
|
+
"""Release readiness binds the called workflow's code and data, not local YAML."""
|
|
95
|
+
errors = []
|
|
96
|
+
steps = workflow.get("jobs", {}).get("build", {}).get("steps", [])
|
|
97
|
+
step = next((row for row in steps if row.get("name") == "Verify manual PDF image provenance"), {})
|
|
98
|
+
if hashlib.sha256(json.dumps(step, sort_keys=True, separators=(",", ":")).encode()).hexdigest() != DEPLOY_PDF_PROVENANCE_POLICY_SHA256:
|
|
99
|
+
errors.append("selected deployment workflow does not contain the reviewed contract-driven provenance gate")
|
|
100
|
+
reference = contract["consumer_integration"]["manual_pdf_image"]
|
|
101
|
+
expected = [row for row in contract.get("deployment_contract", {}).get("manual_pdf_workers", []) if row.get("reference") == reference]
|
|
102
|
+
actual = [row for row in provider_contract.get("deployment_contract", {}).get("manual_pdf_workers", []) if row.get("reference") == reference]
|
|
103
|
+
if len(expected) != 1 or actual != expected:
|
|
104
|
+
errors.append("selected deployment workflow's producer record differs from the package's PDF identity")
|
|
105
|
+
return errors
|
|
106
|
+
|
|
107
|
+
|
|
91
108
|
def validate(root: Path = ROOT) -> list[str]:
|
|
92
109
|
errors: list[str] = []
|
|
93
110
|
contract = distribution_contract()
|
|
@@ -95,6 +112,9 @@ def validate(root: Path = ROOT) -> list[str]:
|
|
|
95
112
|
required_components = {
|
|
96
113
|
"gem", "wheel", "runtime", "mcp", "compute_python", "compute_r", "web_capture", "manual_pdf", "diavisuals", "vegavisuals"
|
|
97
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))
|
|
98
118
|
if contract.get("schema_version") != 1:
|
|
99
119
|
errors.append("component contract schema_version must be 1")
|
|
100
120
|
if set(contract.get("components", {})) != required_components:
|
|
@@ -105,6 +125,10 @@ def validate(root: Path = ROOT) -> list[str]:
|
|
|
105
125
|
if __version__ != version:
|
|
106
126
|
errors.append(f"wheel version {__version__} != contract version {version}")
|
|
107
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")
|
|
108
132
|
included = {name for name, item in contract["components"].items() if item["included_in_wheel"]}
|
|
109
133
|
if included != {"wheel"}:
|
|
110
134
|
errors.append(f"wheel must not bundle external components: {sorted(included)}")
|
|
@@ -125,6 +149,11 @@ def validate(root: Path = ROOT) -> list[str]:
|
|
|
125
149
|
errors.append(f"component contract schema validation failed: {exc}")
|
|
126
150
|
errors.extend(f"component contract semantic validation failed: {error}" for error in component_contract_semantic_errors(contract))
|
|
127
151
|
core_sha = str(contract["consumer_integration"]["core_sha"])
|
|
152
|
+
workflow_sha = str(contract["consumer_integration"].get("site_deploy_workflow_sha", core_sha))
|
|
153
|
+
workflow_ancestor = subprocess.run(["git", "merge-base", "--is-ancestor", workflow_sha, "HEAD"], cwd=root,
|
|
154
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
|
|
155
|
+
if workflow_ancestor.returncode != 0:
|
|
156
|
+
errors.append("consumer integration workflow SHA must be an ancestor of the current source")
|
|
128
157
|
core_object = subprocess.run(
|
|
129
158
|
["git", "cat-file", "-e", f"{core_sha}^{{commit}}"],
|
|
130
159
|
cwd=root,
|
|
@@ -145,7 +174,7 @@ def validate(root: Path = ROOT) -> list[str]:
|
|
|
145
174
|
if core_ancestor.returncode != 0:
|
|
146
175
|
errors.append("consumer integration core SHA must be an ancestor of the current source")
|
|
147
176
|
pinned_workflow = subprocess.run(
|
|
148
|
-
["git", "show", f"{
|
|
177
|
+
["git", "show", f"{workflow_sha}:.github/workflows/site-deploy.yml"],
|
|
149
178
|
cwd=root,
|
|
150
179
|
text=True,
|
|
151
180
|
stdout=subprocess.PIPE,
|
|
@@ -153,7 +182,7 @@ def validate(root: Path = ROOT) -> list[str]:
|
|
|
153
182
|
check=False,
|
|
154
183
|
)
|
|
155
184
|
if pinned_workflow.returncode != 0:
|
|
156
|
-
errors.append("consumer integration
|
|
185
|
+
errors.append("consumer integration workflow SHA does not contain the reusable deploy workflow")
|
|
157
186
|
else:
|
|
158
187
|
try:
|
|
159
188
|
pinned_deploy = load_workflow_text(pinned_workflow.stdout)
|
|
@@ -164,6 +193,14 @@ def validate(root: Path = ROOT) -> list[str]:
|
|
|
164
193
|
f"consumer integration workflow: {error}"
|
|
165
194
|
for error in reusable_deploy_input_errors(pinned_deploy)
|
|
166
195
|
)
|
|
196
|
+
if "deployment_contract" in contract and contract["components"]["mcp"]["release_status"] == "ready":
|
|
197
|
+
recorded = subprocess.run(["git", "show", f"{workflow_sha}:src/unaltraweb_mcp/component-contract.json"],
|
|
198
|
+
cwd=root, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
|
|
199
|
+
try:
|
|
200
|
+
provider_contract = json.loads(recorded.stdout) if recorded.returncode == 0 else {}
|
|
201
|
+
errors.extend(selected_deployment_errors(contract, pinned_deploy, provider_contract))
|
|
202
|
+
except (ValueError, KeyError, TypeError):
|
|
203
|
+
errors.append("selected deployment workflow has no valid provider-owned component contract")
|
|
167
204
|
if schema.get("properties", {}).get("schema_version", {}).get("const") != 1:
|
|
168
205
|
errors.append("component contract schema does not select version 1")
|
|
169
206
|
|
|
@@ -183,6 +220,9 @@ def validate(root: Path = ROOT) -> list[str]:
|
|
|
183
220
|
if make_value(root / "packaging/launcher/Makefile", "MCP_RELEASE_IMAGE") != component_reference("mcp"):
|
|
184
221
|
errors.append("installed launcher must select its own package release image")
|
|
185
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")
|
|
186
226
|
if "version" in project or project.get("dynamic") != ["version"]:
|
|
187
227
|
errors.append("pyproject version must be derived from the package component contract")
|
|
188
228
|
if pyproject.get("tool", {}).get("setuptools", {}).get("dynamic", {}).get("version", {}).get("attr") != "unaltraweb_mcp.__version__":
|
|
@@ -273,7 +313,7 @@ def validate(root: Path = ROOT) -> list[str]:
|
|
|
273
313
|
"Makefile.tmpl": ["__MCP_IMAGE__", "__MANUAL_PDF_IMAGE__"],
|
|
274
314
|
".github/workflows/deploy.yml.tmpl": [
|
|
275
315
|
"__SITE_DEPLOY_WORKFLOW__",
|
|
276
|
-
"
|
|
316
|
+
"__SITE_DEPLOY_WORKFLOW_SHA__",
|
|
277
317
|
"__MANUAL_PDF_IMAGE__",
|
|
278
318
|
"__VEGAVISUALS_SHA__",
|
|
279
319
|
],
|
|
@@ -486,7 +526,7 @@ def publish_ref_errors(
|
|
|
486
526
|
default_branch: str,
|
|
487
527
|
component_ids: list[str],
|
|
488
528
|
) -> list[str]:
|
|
489
|
-
errors: list[str] =
|
|
529
|
+
errors: list[str] = practice_verifier.publication_errors(contract)
|
|
490
530
|
release = contract.get("release", {})
|
|
491
531
|
version = str(release.get("version") or "")
|
|
492
532
|
release_tag = str(release.get("tag") or "")
|
|
@@ -561,6 +601,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
561
601
|
component_id for component_id, selected in contract["components"].items()
|
|
562
602
|
if selected["release_status"] == "pending"
|
|
563
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()
|
|
564
608
|
unavailable = sorted(
|
|
565
609
|
component_id for component_id, selected in contract["components"].items()
|
|
566
610
|
if selected["release_status"] == "unavailable"
|
|
@@ -583,6 +627,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
583
627
|
"errors": errors,
|
|
584
628
|
"pending_releases": pending,
|
|
585
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"}},
|
|
586
632
|
}, indent=2, sort_keys=True))
|
|
587
633
|
readiness_errors = release_tag_errors + candidate_receipt_errors
|
|
588
634
|
structural_errors = [error for error in errors if error not in readiness_errors]
|
|
@@ -52,6 +52,9 @@ PYPI_PUBLISH_IMAGE = (
|
|
|
52
52
|
)
|
|
53
53
|
# Canonical JSON digest of the complete unprivileged verification job.
|
|
54
54
|
PACKAGE_PUBLISH_VERIFY_POLICY_SHA256 = "e8d99c35040770a2b00d3c62497da637739e794310b253d1893078d048d51c48"
|
|
55
|
+
# Canonical JSON of the complete provider-owned PDF digest/producer gate.
|
|
56
|
+
# Extending its reviewed release records also requires published-worker tests.
|
|
57
|
+
DEPLOY_PDF_PROVENANCE_POLICY_SHA256 = "da18b92db4069655e9979308dd90279ea8bfce4b77e47158efd5ac4421e06417"
|
|
55
58
|
IMAGE_WORKFLOWS = {
|
|
56
59
|
"compute-images.yml",
|
|
57
60
|
"docker-image.yml",
|
|
@@ -119,14 +122,17 @@ def load_consumer_integration(root: Path) -> dict[str, Any]:
|
|
|
119
122
|
"manual_pdf_image",
|
|
120
123
|
"vegavisuals_sha",
|
|
121
124
|
}
|
|
122
|
-
if not isinstance(integration, dict) or set(integration)
|
|
125
|
+
if not isinstance(integration, dict) or not required <= set(integration) or set(integration) - required - {"site_deploy_workflow_sha"}:
|
|
123
126
|
raise ValueError("component contract has no exact consumer_integration object")
|
|
127
|
+
integration = dict(integration)
|
|
128
|
+
integration.setdefault("site_deploy_workflow_sha", integration["core_sha"])
|
|
124
129
|
return integration
|
|
125
130
|
|
|
126
131
|
|
|
127
132
|
def load_scaffold_deploy(path: Path, integration: dict[str, Any]) -> dict[str, Any]:
|
|
128
133
|
replacements = {
|
|
129
134
|
"CORE_SHA": str(integration["core_sha"]),
|
|
135
|
+
"SITE_DEPLOY_WORKFLOW_SHA": str(integration["site_deploy_workflow_sha"]),
|
|
130
136
|
"SITE_DEPLOY_WORKFLOW": str(integration["site_deploy_workflow"]),
|
|
131
137
|
"MANUAL_PDF_IMAGE": str(integration["manual_pdf_image"]),
|
|
132
138
|
"VEGAVISUALS_SHA": str(integration["vegavisuals_sha"]),
|
|
@@ -213,6 +219,7 @@ def validate_workflows(root: Path = WORKFLOW_ROOT) -> list[str]:
|
|
|
213
219
|
integration = {
|
|
214
220
|
"core_sha": "invalid",
|
|
215
221
|
"site_deploy_workflow": "invalid",
|
|
222
|
+
"site_deploy_workflow_sha": "invalid",
|
|
216
223
|
"manual_pdf_image": "invalid",
|
|
217
224
|
"vegavisuals_sha": "invalid",
|
|
218
225
|
}
|
|
@@ -1047,6 +1054,16 @@ def validate_workflows(root: Path = WORKFLOW_ROOT) -> list[str]:
|
|
|
1047
1054
|
if marker not in ci_text:
|
|
1048
1055
|
errors.append(f"ci.yml: missing required check {marker}")
|
|
1049
1056
|
ci_docker = ci.get("jobs", {}).get("docker", {})
|
|
1057
|
+
published_pdf_gate = _named_step(ci.get("jobs", {}).get("distribution", {}), "Test published PDF deployment provenance")
|
|
1058
|
+
if published_pdf_gate != {
|
|
1059
|
+
"name": "Test published PDF deployment provenance",
|
|
1060
|
+
"env": {
|
|
1061
|
+
"UNALTRAWEB_DEPLOY_PROVENANCE_DOCKER": "1",
|
|
1062
|
+
"UNALTRAWEB_DEPLOY_PROVENANCE_SOURCE_SHA": "${{ github.event.pull_request.head.sha || github.sha }}",
|
|
1063
|
+
},
|
|
1064
|
+
"run": "PYTHONPATH=test python -m unittest test_deploy_provenance.PublishedDeployProvenanceTests",
|
|
1065
|
+
}:
|
|
1066
|
+
errors.append("ci.yml: published PDF deployment provenance must run unconditionally with real Docker")
|
|
1050
1067
|
ruby_tests = str(_named_step(ci_docker, "Test all Ruby files").get("run") or "")
|
|
1051
1068
|
if not all(
|
|
1052
1069
|
marker in ruby_tests
|
|
@@ -1362,8 +1379,20 @@ def validate_workflows(root: Path = WORKFLOW_ROOT) -> list[str]:
|
|
|
1362
1379
|
for marker in ["MANUAL_PDF_IMAGE", "WORKFLOW_REPOSITORY", "WORKFLOW_SHA", "docker pull", "docker image inspect", "org.opencontainers.image.revision"]:
|
|
1363
1380
|
if marker not in provenance_text:
|
|
1364
1381
|
errors.append(f"site-deploy.yml: PDF worker provenance check missing {marker}")
|
|
1365
|
-
if '[[ "$image_revision" != "$
|
|
1366
|
-
errors.append("site-deploy.yml: PDF worker revision label must be compared with
|
|
1382
|
+
if '[[ "$image_revision" != "$expected_pdf_revision" ]]' not in provenance_text:
|
|
1383
|
+
errors.append("site-deploy.yml: PDF worker revision label must be compared with its recorded producer SHA")
|
|
1384
|
+
provenance_sha256 = hashlib.sha256(
|
|
1385
|
+
json.dumps(provenance_step, sort_keys=True, separators=(",", ":")).encode()
|
|
1386
|
+
).hexdigest()
|
|
1387
|
+
if provenance_sha256 != DEPLOY_PDF_PROVENANCE_POLICY_SHA256:
|
|
1388
|
+
errors.append("site-deploy.yml: PDF digest/producer gate differs from the exact reviewed structure")
|
|
1389
|
+
try:
|
|
1390
|
+
contract = json.loads((ROOT / "src/unaltraweb_mcp/component-contract.json").read_text())
|
|
1391
|
+
records = contract["deployment_contract"]["manual_pdf_workers"]
|
|
1392
|
+
if sum(row.get("reference") == integration["manual_pdf_image"] for row in records) != 1:
|
|
1393
|
+
errors.append("site-deploy.yml: selected PDF worker has no reviewed digest/producer record")
|
|
1394
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
1395
|
+
errors.append("site-deploy.yml: missing or invalid provider deployment records")
|
|
1367
1396
|
if "docker pull" in provenance_text and "docker image inspect" in provenance_text and provenance_text.index("docker pull") > provenance_text.index("docker image inspect"):
|
|
1368
1397
|
errors.append("site-deploy.yml: PDF worker digest must be pulled before its revision label is inspected")
|
|
1369
1398
|
if "${{ github.workflow_sha }}" in deploy_text or "${{ github.workflow_repository }}" in deploy_text:
|
|
@@ -1542,7 +1571,7 @@ def validate_workflows(root: Path = WORKFLOW_ROOT) -> list[str]:
|
|
|
1542
1571
|
action, separator, revision = uses.rpartition("@")
|
|
1543
1572
|
if not separator or action != integration["site_deploy_workflow"] or not FULL_SHA.fullmatch(revision):
|
|
1544
1573
|
errors.append(f"scaffold deploy.yml:{job_name}: reusable workflow must use an immutable unaltraweb SHA")
|
|
1545
|
-
elif revision != integration["
|
|
1574
|
+
elif revision != integration["site_deploy_workflow_sha"]:
|
|
1546
1575
|
errors.append(f"scaffold deploy.yml:{job_name}: reusable workflow SHA has not been reviewed")
|
|
1547
1576
|
reusable_job = scaffold.get("jobs", {}).get(job_name, {})
|
|
1548
1577
|
if reusable_job.get("needs") != "validate":
|
|
@@ -2,17 +2,43 @@
|
|
|
2
2
|
"$schema": "component-contract.schema.json",
|
|
3
3
|
"schema_version": 1,
|
|
4
4
|
"release": {
|
|
5
|
-
"version": "0.7.
|
|
6
|
-
"tag": "v0.7.
|
|
5
|
+
"version": "0.7.2",
|
|
6
|
+
"tag": "v0.7.2"
|
|
7
7
|
},
|
|
8
8
|
"consumer_integration": {
|
|
9
9
|
"schema_version": 1,
|
|
10
10
|
"core_repository": "https://github.com/dosquartsdedocs/unaltraweb.git",
|
|
11
|
-
"core_sha": "
|
|
11
|
+
"core_sha": "3332e0b93933f12317e05f1ce8d38ce80046feab",
|
|
12
12
|
"site_deploy_workflow": "dosquartsdedocs/unaltraweb/.github/workflows/site-deploy.yml",
|
|
13
|
-
"
|
|
13
|
+
"site_deploy_workflow_sha": "3332e0b93933f12317e05f1ce8d38ce80046feab",
|
|
14
|
+
"manual_pdf_image": "ghcr.io/dosquartsdedocs/unaltraweb-manual-pdf@sha256:1bb3f2dafd741e96c28639ec1cc8ccaeb81cbaa73e649f787c0b4ef59997bd4b",
|
|
14
15
|
"vegavisuals_sha": "a448cc50a5f511743a6fcfc213baeb9be80f4a27"
|
|
15
16
|
},
|
|
17
|
+
"deployment_contract": {
|
|
18
|
+
"schema_version": 1,
|
|
19
|
+
"manual_pdf_workers": [
|
|
20
|
+
{
|
|
21
|
+
"reference": "ghcr.io/dosquartsdedocs/unaltraweb-manual-pdf@sha256:9e0b3a45753c170b795e9a9d6df61580085c113436beac5bf6c8de69b6562097",
|
|
22
|
+
"producer_sha": "d857f8c9f5fea90cf450c0b30b4e77a37b541275"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"reference": "ghcr.io/dosquartsdedocs/unaltraweb-manual-pdf@sha256:0ba267cb87f53ebaca4e31805fe00610cd61fdf97a8d2c3692f4655700dceaed",
|
|
26
|
+
"producer_sha": "5cf9817489c8dc47cee726bfe42fe3071cd32b85"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"reference": "ghcr.io/dosquartsdedocs/unaltraweb-manual-pdf@sha256:1bb3f2dafd741e96c28639ec1cc8ccaeb81cbaa73e649f787c0b4ef59997bd4b",
|
|
30
|
+
"producer_sha": "b7ec1d7a139c65b3b0b88699cdbeb19080f13be1"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"caller_migrations": [
|
|
34
|
+
{
|
|
35
|
+
"id": "deploy-provenance-86",
|
|
36
|
+
"path": ".github/workflows/deploy.yml",
|
|
37
|
+
"baseline_sha256": "a789870542614d5817d30ea4328c7d1ad38d27443c009c68a48ad5aaa3106b8e",
|
|
38
|
+
"current_sha256": "c632a548d0782867be20a2a52fe03469cf63066b7364bace77fd54a226cb3b18"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
16
42
|
"receipt_contract": {
|
|
17
43
|
"schema_version": 1,
|
|
18
44
|
"path_template": ".unaltraweb/receipts/{provider}.json",
|
|
@@ -29,17 +55,17 @@
|
|
|
29
55
|
"package_only_commands": ["doctor", "import-calibre", "new-web", "version"],
|
|
30
56
|
"factory_required_commands": ["factory-dir"],
|
|
31
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"],
|
|
32
|
-
"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"],
|
|
33
|
-
"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"]
|
|
34
60
|
},
|
|
35
61
|
"components": {
|
|
36
62
|
"gem": {
|
|
37
63
|
"kind": "gem",
|
|
38
64
|
"name": "unaltraweb",
|
|
39
|
-
"version": "0.7.
|
|
40
|
-
"release": "v0.7.
|
|
65
|
+
"version": "0.7.2",
|
|
66
|
+
"release": "v0.7.2",
|
|
41
67
|
"release_status": "ready",
|
|
42
|
-
"reference": "unaltraweb (= 0.7.
|
|
68
|
+
"reference": "unaltraweb (= 0.7.2)",
|
|
43
69
|
"repository": "https://github.com/dosquartsdedocs/unaltraweb",
|
|
44
70
|
"included_in_wheel": false,
|
|
45
71
|
"features": ["jekyll-core", "site-build"]
|
|
@@ -47,21 +73,21 @@
|
|
|
47
73
|
"wheel": {
|
|
48
74
|
"kind": "python-wheel",
|
|
49
75
|
"name": "unaltraweb-mcp",
|
|
50
|
-
"version": "0.7.
|
|
51
|
-
"release": "v0.7.
|
|
76
|
+
"version": "0.7.2",
|
|
77
|
+
"release": "v0.7.2",
|
|
52
78
|
"release_status": "ready",
|
|
53
|
-
"reference": "unaltraweb-mcp==0.7.
|
|
79
|
+
"reference": "unaltraweb-mcp==0.7.2",
|
|
54
80
|
"repository": "https://github.com/dosquartsdedocs/unaltraweb",
|
|
55
81
|
"included_in_wheel": true,
|
|
56
|
-
"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"]
|
|
57
83
|
},
|
|
58
84
|
"runtime": {
|
|
59
85
|
"kind": "container",
|
|
60
86
|
"name": "unaltraweb runtime",
|
|
61
|
-
"version": "0.7.
|
|
62
|
-
"release": "v0.7.
|
|
87
|
+
"version": "0.7.2",
|
|
88
|
+
"release": "v0.7.2",
|
|
63
89
|
"release_status": "ready",
|
|
64
|
-
"reference": "ghcr.io/dosquartsdedocs/unaltraweb:0.7.
|
|
90
|
+
"reference": "ghcr.io/dosquartsdedocs/unaltraweb:0.7.2",
|
|
65
91
|
"image_repository": "ghcr.io/dosquartsdedocs/unaltraweb",
|
|
66
92
|
"repository": "https://github.com/dosquartsdedocs/unaltraweb",
|
|
67
93
|
"included_in_wheel": false,
|
|
@@ -70,10 +96,10 @@
|
|
|
70
96
|
"mcp": {
|
|
71
97
|
"kind": "container",
|
|
72
98
|
"name": "unaltraweb MCP runtime",
|
|
73
|
-
"version": "0.7.
|
|
74
|
-
"release": "v0.7.
|
|
99
|
+
"version": "0.7.2",
|
|
100
|
+
"release": "v0.7.2",
|
|
75
101
|
"release_status": "ready",
|
|
76
|
-
"reference": "ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.7.
|
|
102
|
+
"reference": "ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.7.2",
|
|
77
103
|
"image_repository": "ghcr.io/dosquartsdedocs/unaltraweb-mcp",
|
|
78
104
|
"repository": "https://github.com/dosquartsdedocs/unaltraweb",
|
|
79
105
|
"included_in_wheel": false,
|
|
@@ -118,15 +144,26 @@
|
|
|
118
144
|
"manual_pdf": {
|
|
119
145
|
"kind": "container",
|
|
120
146
|
"name": "unaltraweb manual PDF worker",
|
|
121
|
-
"version": "0.
|
|
122
|
-
"release": "v0.
|
|
147
|
+
"version": "0.7.1",
|
|
148
|
+
"release": "v0.7.1",
|
|
123
149
|
"release_status": "released",
|
|
124
|
-
"reference": "ghcr.io/dosquartsdedocs/unaltraweb-manual-pdf@sha256:
|
|
150
|
+
"reference": "ghcr.io/dosquartsdedocs/unaltraweb-manual-pdf@sha256:1bb3f2dafd741e96c28639ec1cc8ccaeb81cbaa73e649f787c0b4ef59997bd4b",
|
|
125
151
|
"image_repository": "ghcr.io/dosquartsdedocs/unaltraweb-manual-pdf",
|
|
126
152
|
"repository": "https://github.com/dosquartsdedocs/unaltraweb",
|
|
127
153
|
"included_in_wheel": false,
|
|
128
154
|
"features": ["manual-pdf"]
|
|
129
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
|
+
},
|
|
130
167
|
"diavisuals": {
|
|
131
168
|
"kind": "companion",
|
|
132
169
|
"name": "diavisuals",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"required": ["schema_version", "release", "consumer_integration", "receipt_contract", "wheel_contract", "components"],
|
|
8
8
|
"properties": {
|
|
9
9
|
"$schema": {"type": "string"},
|
|
10
|
-
"schema_version": {"const": 1},
|
|
10
|
+
"schema_version": {"type": "integer", "const": 1},
|
|
11
11
|
"release": {
|
|
12
12
|
"type": "object",
|
|
13
13
|
"additionalProperties": false,
|
|
@@ -22,14 +22,47 @@
|
|
|
22
22
|
"additionalProperties": false,
|
|
23
23
|
"required": ["schema_version", "core_repository", "core_sha", "site_deploy_workflow", "manual_pdf_image", "vegavisuals_sha"],
|
|
24
24
|
"properties": {
|
|
25
|
-
"schema_version": {"const": 1},
|
|
25
|
+
"schema_version": {"type": "integer", "const": 1},
|
|
26
26
|
"core_repository": {"const": "https://github.com/dosquartsdedocs/unaltraweb.git"},
|
|
27
27
|
"core_sha": {"type": "string", "minLength": 40, "maxLength": 40, "pattern": "^[0-9a-f]{40}$"},
|
|
28
28
|
"site_deploy_workflow": {"const": "dosquartsdedocs/unaltraweb/.github/workflows/site-deploy.yml"},
|
|
29
|
+
"site_deploy_workflow_sha": {"type": "string", "minLength": 40, "maxLength": 40, "pattern": "^[0-9a-f]{40}$"},
|
|
29
30
|
"manual_pdf_image": {"type": "string", "minLength": 117, "maxLength": 117, "pattern": "^ghcr\\.io/dosquartsdedocs/unaltraweb-manual-pdf@sha256:[0-9a-f]{64}$"},
|
|
30
31
|
"vegavisuals_sha": {"type": "string", "minLength": 40, "maxLength": 40, "pattern": "^[0-9a-f]{40}$"}
|
|
31
32
|
}
|
|
32
33
|
},
|
|
34
|
+
"deployment_contract": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"required": ["schema_version", "manual_pdf_workers", "caller_migrations"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"schema_version": {"type": "integer", "const": 1},
|
|
40
|
+
"manual_pdf_workers": {
|
|
41
|
+
"type": "array", "minItems": 1, "uniqueItems": true,
|
|
42
|
+
"items": {
|
|
43
|
+
"type": "object", "additionalProperties": false,
|
|
44
|
+
"required": ["reference", "producer_sha"],
|
|
45
|
+
"properties": {
|
|
46
|
+
"reference": {"type": "string", "minLength": 117, "maxLength": 117, "pattern": "^ghcr\\.io/dosquartsdedocs/unaltraweb-manual-pdf@sha256:[0-9a-f]{64}$"},
|
|
47
|
+
"producer_sha": {"type": "string", "minLength": 40, "maxLength": 40, "pattern": "^[0-9a-f]{40}$"}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"caller_migrations": {
|
|
52
|
+
"type": "array", "uniqueItems": true,
|
|
53
|
+
"items": {
|
|
54
|
+
"type": "object", "additionalProperties": false,
|
|
55
|
+
"required": ["id", "path", "baseline_sha256", "current_sha256"],
|
|
56
|
+
"properties": {
|
|
57
|
+
"id": {"type": "string", "pattern": "^[a-z][a-z0-9-]{0,63}$"},
|
|
58
|
+
"path": {"const": ".github/workflows/deploy.yml"},
|
|
59
|
+
"baseline_sha256": {"type": "string", "minLength": 64, "maxLength": 64, "pattern": "^[0-9a-f]{64}$"},
|
|
60
|
+
"current_sha256": {"type": "string", "minLength": 64, "maxLength": 64, "pattern": "^[0-9a-f]{64}$"}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
33
66
|
"receipt_contract": {
|
|
34
67
|
"type": "object",
|
|
35
68
|
"additionalProperties": false,
|
|
@@ -20,7 +20,15 @@ def _strict_json(path: Path) -> Any:
|
|
|
20
20
|
def reject(value: str) -> None:
|
|
21
21
|
raise ValueError(f"non-finite JSON number is not allowed: {value}")
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
def unique(pairs):
|
|
24
|
+
result = {}
|
|
25
|
+
for key, value in pairs:
|
|
26
|
+
if key in result:
|
|
27
|
+
raise ValueError(f"duplicate component contract key: {key}")
|
|
28
|
+
result[key] = value
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
return json.loads(path.read_text(encoding="utf-8"), parse_constant=reject, object_pairs_hook=unique)
|
|
24
32
|
|
|
25
33
|
|
|
26
34
|
def _schema_type_matches(value: Any, expected: str) -> bool:
|
|
@@ -155,6 +163,9 @@ def component_contract_semantic_errors(value: dict[str, Any]) -> list[str]:
|
|
|
155
163
|
expected_workflow = f"{expected_core_repository.removeprefix('https://github.com/').removesuffix('.git')}/.github/workflows/site-deploy.yml"
|
|
156
164
|
if integration["site_deploy_workflow"] != expected_workflow:
|
|
157
165
|
errors.append("consumer integration deploy workflow must belong to the core provider")
|
|
166
|
+
workflow_sha = str(integration.get("site_deploy_workflow_sha", core_sha))
|
|
167
|
+
if re.fullmatch(r"[0-9a-f]{40}", workflow_sha) is None or workflow_sha == "0" * 40:
|
|
168
|
+
errors.append("consumer integration workflow SHA must be a nonzero full lowercase commit SHA")
|
|
158
169
|
expected_pdf_prefix = f"{value['components']['manual_pdf']['image_repository']}@sha256:"
|
|
159
170
|
manual_pdf_image = str(integration["manual_pdf_image"])
|
|
160
171
|
if re.fullmatch(rf"{re.escape(expected_pdf_prefix)}[0-9a-f]{{64}}", manual_pdf_image) is None or manual_pdf_image.endswith("0" * 64):
|
|
@@ -162,6 +173,26 @@ def component_contract_semantic_errors(value: dict[str, Any]) -> list[str]:
|
|
|
162
173
|
vegavisuals_sha = str(integration["vegavisuals_sha"])
|
|
163
174
|
if re.fullmatch(r"[0-9a-f]{40}", vegavisuals_sha) is None or vegavisuals_sha == "0" * 40:
|
|
164
175
|
errors.append("consumer integration Vega revision must be a nonzero full lowercase commit SHA")
|
|
176
|
+
deployment = value.get("deployment_contract")
|
|
177
|
+
if deployment is not None:
|
|
178
|
+
references = [row["reference"] for row in deployment["manual_pdf_workers"]]
|
|
179
|
+
if not 1 <= len(references) <= 64:
|
|
180
|
+
errors.append("deployment worker inventory must contain between 1 and 64 records")
|
|
181
|
+
if len(set(references)) != len(references):
|
|
182
|
+
errors.append("deployment worker references must be unique")
|
|
183
|
+
if manual_pdf_image not in references or value["components"]["manual_pdf"]["reference"] != manual_pdf_image:
|
|
184
|
+
errors.append("selected PDF worker must match a reviewed deployment record and the component reference")
|
|
185
|
+
for row in deployment["manual_pdf_workers"]:
|
|
186
|
+
if row["producer_sha"] == "0" * 40 or row["reference"].endswith("0" * 64):
|
|
187
|
+
errors.append("deployment worker identities must be nonzero")
|
|
188
|
+
migrations = deployment["caller_migrations"]
|
|
189
|
+
if len(migrations) > 32:
|
|
190
|
+
errors.append("deployment caller migration inventory exceeds 32 records")
|
|
191
|
+
if len({row["id"] for row in migrations}) != len(migrations) or len({(row["path"], row["baseline_sha256"], row["current_sha256"]) for row in migrations}) != len(migrations):
|
|
192
|
+
errors.append("deployment caller migrations must be unique")
|
|
193
|
+
for row in migrations:
|
|
194
|
+
if row["baseline_sha256"] == row["current_sha256"] or "0" * 64 in (row["baseline_sha256"], row["current_sha256"]):
|
|
195
|
+
errors.append("deployment caller migration requires distinct nonzero baseline and current hashes")
|
|
165
196
|
return errors
|
|
166
197
|
|
|
167
198
|
|
|
@@ -194,7 +225,16 @@ def distribution_version() -> str:
|
|
|
194
225
|
|
|
195
226
|
def consumer_integration() -> dict[str, Any]:
|
|
196
227
|
"""Return an isolated copy of the reviewed consumer runtime tuple."""
|
|
197
|
-
|
|
228
|
+
result = dict(_contract()["consumer_integration"])
|
|
229
|
+
result.setdefault("site_deploy_workflow_sha", result["core_sha"])
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def deployment_contract() -> dict[str, Any]:
|
|
234
|
+
"""Provider-owned identities and exact reviewed caller migrations."""
|
|
235
|
+
return json.loads(json.dumps(_contract().get("deployment_contract", {
|
|
236
|
+
"schema_version": 1, "manual_pdf_workers": [], "caller_migrations": [],
|
|
237
|
+
})))
|
|
198
238
|
|
|
199
239
|
|
|
200
240
|
def component(component_id: str) -> dict[str, Any]:
|