unaltraweb 0.8.0 → 0.8.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4da9160082bd9c84936be79042fb12dcd6b08de75353196d34079d77e2abc55c
4
- data.tar.gz: fb9d3fb95a69e51d6c430f612baf9cf426a694c4b9c986de30e60ff3e652b765
3
+ metadata.gz: bc478612d85e4f4f43eb203c048bce25f105fa320a56e66e41922627c1f4b4b3
4
+ data.tar.gz: 0d55b9e3743df94ae89dda1cf008d604873d6938ae3732a2b41edd8f03d64ea0
5
5
  SHA512:
6
- metadata.gz: 8949f58e89b5ade9255639ff6d2b43df2bce76401b989662059922c948bca7be1e63f6da6b9e623ef9587e5f8820d82492c06c3c7e226c6ee1055d17f783f243
7
- data.tar.gz: 2999667915dfbfb58da97468780057a1a0a78d29054894fd70bc4d7d8e4ab096f662cee9f40043fc92111df3e9171b50b4884a7dde1916a1bd8fcc798493295a
6
+ metadata.gz: 2ee71521b81afc077773b375120d39ca2e00fbaa926f287ee6df2e2c6356c10524f4b67346298f0dd647ee182f2c0fb2e45940faadaf9a0e4c336eab697aa85a
7
+ data.tar.gz: fc0f4072ce20853d839c46b0cab1c305f5dfc43bf61d83d58ebb13e552f708ad2d16e4473f4f454829c9206857b66b3279d38ba7f7e110845053fa0942d8497a
data/Makefile CHANGED
@@ -1,9 +1,9 @@
1
1
  PYTHON ?= python3
2
2
  override PROJECT := $${MCP_CONSUMER_WORKSPACE:?MCP_CONSUMER_WORKSPACE is required}
3
3
  override PROJECT_ROOT := $(PROJECT)
4
- MCP_RUNTIME_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb:0.8.0
5
- MCP_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.8.0
6
- MCP_RELEASE_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:10bcaf1411f27a518969283a77e23dcc99119014b216270b3089ba85ba0b6c7e
4
+ MCP_RUNTIME_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb:0.8.1
5
+ MCP_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.8.1
6
+ MCP_RELEASE_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:8b41abacad9ca2894755db7f3587a6b47c8fa8f536d88a151bdfc4ae53ba7f08
7
7
  MCP_DOCKER_BUILD_NETWORK ?= default
8
8
  INIT_SITE_PROFILE ?= unaltreselfie
9
9
  NEW_WEB_PROFILE ?= unaltreselfie
@@ -40,7 +40,7 @@ WEB_CAPTURE_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-web-capture@sha256:0bf1b
40
40
  WEB_CAPTURE_DEV_IMAGE ?= unaltraweb-web-capture:dev
41
41
  WEB_CAPTURE_DOCKER_BUILD_NETWORK ?= default
42
42
  VEGAVISUALS_CLI ?=
43
- DOCKER_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb:0.8.0
43
+ DOCKER_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb:0.8.1
44
44
  MANUAL_PDF_IMAGE ?= ghcr.io/dosquartsdedocs/unaltraweb-manual-pdf@sha256:1bb3f2dafd741e96c28639ec1cc8ccaeb81cbaa73e649f787c0b4ef59997bd4b
45
45
  MANUAL_PDF_DEV_IMAGE ?= unaltraweb-manual-pdf:dev
46
46
  MCP_SMOKE_MANUAL_PDF_IMAGE ?= $(MANUAL_PDF_IMAGE)
@@ -82,6 +82,25 @@ are refused with a diagnostic, not relabelled as static. Its profile/receipt
82
82
  integration and receiver support for raster-bearing SVG require coordinated
83
83
  follow-up. Edited captures are never silently substituted with original rasters.
84
84
 
85
+ ### Candidate 0.8.1: check-only computation dependencies
86
+
87
+ The 0.8.1 source candidate separates explicitly declared native computation
88
+ inputs/environment fingerprints from public content selection. Durable literal
89
+ project-relative inputs, including `_practices/<project>/data/...` and root-level
90
+ `Dockerfile.compute-*` / `requirements-compute.*`, are checked through the same
91
+ bounded no-follow reader and native freshness checks. They do not become public
92
+ image/include/output sources. Hidden paths, broad private roots and transient or
93
+ dependency-installation roots (`tmp`, `sandbox`, `dist`, `node_modules`, `vendor`)
94
+ remain refused. Reader and transport byte limits are unchanged.
95
+
96
+ Only their hashes participate in the source fingerprint and external producer
97
+ receipt. Private input bytes and paths do not enter the transport. Global
98
+ completeness, stale-input checks and final rechecks still apply when selecting a
99
+ single chapter. The candidate also restores inline-code tokens in HTML image
100
+ alt/title/credits before recording metadata and figure identity. Published 0.8.0
101
+ does not contain these repairs; use the newly qualified release after publication
102
+ and explicit adoption. The v1 schema and SVG/PDF/QGIS restrictions remain unchanged.
103
+
85
104
  Diagrams use the provider's exact effective-artifact receipt. Vega uses exactly
86
105
  the output declared in `.vegavisuals.yml` (including an edited output only when
87
106
  that is the declared, verified output). Computation SVG overrides must postdate
@@ -1,9 +1,10 @@
1
1
  # Manual context producer — #121
2
2
 
3
3
  Workspace: `/home/benizar/git/unaltraweb`; branch `feat/121-manual-context-export`.
4
- [Draft PR122](https://github.com/dosquartsdedocs/unaltraweb/pull/122) contains the
4
+ The initial [PR122](https://github.com/dosquartsdedocs/unaltraweb/pull/122) contained the
5
5
  post-release pin (`59bd62b`), first bound exporter (`cbf7260`) and resolver/check
6
- continuation. No merge or public release is part of this delivery.
6
+ continuation. That initial delivery preceded the authorized integration and
7
+ publication recorded below.
7
8
 
8
9
  ## Interface and contract
9
10
 
@@ -105,8 +106,8 @@ increment; recognizable QGIS captures get an explicit unsupported diagnostic.
105
106
  Interactive/arbitrary Liquid and undeclared outputs also fail closed. These
106
107
  limitations are recorded on #121 for producer/receiver coordination.
107
108
 
108
- The source version is 0.8.0 with publication status **pending**. The checkout
109
- launcher now selects published 0.7.4; observation of this conversation's existing
109
+ At initial source delivery, version 0.8.0 had publication status **pending**. The checkout
110
+ launcher then selected published 0.7.4; observation of this conversation's existing
110
111
  MCP still showed installed **0.7.3**, instance `ecac06618bd3436794e89c31746b8914`,
111
112
  bound to this checkout. It was not hot-reloaded or rebound. Hub admission belongs
112
113
  to gaContExt #52, and real-manual selection/author review remain separate.
@@ -123,7 +124,7 @@ with no manual checkout: all four conformance checks passed, receiver receipt
123
124
  `15b8561e1e55899b57d8e45b8d19b7a8eeaa669808cf729d29eac3589a3231fb`
124
125
  ([receiver return](https://github.com/dosquartsdedocs/unaltraweb/issues/121#issuecomment-6597815786)).
125
126
 
126
- The four new Web components are marked release-ready for the normal signed
127
+ The four new Web components were marked release-ready for the normal signed
127
128
  candidate, receipt-only merge/tag and trusted publication workflow. Independent
128
129
  core/gem, workflow, PDF and QGIS selections retain their previously verified
129
130
  identities. This source readiness is not a claim of published bytes or admission.
@@ -132,3 +133,59 @@ GaContExt #52 specifically concerns admitting newer **Itemarium** native builds.
132
133
  The new Web tuple must also be delivered to the hub owner after publication;
133
134
  neither publication alone nor the existing consumer conformance receipt proves
134
135
  hub admission, selected-client activation or an export of a real teaching manual.
136
+
137
+ ## Published 0.8.0 and verified intake
138
+
139
+ [Release v0.8.0](https://github.com/dosquartsdedocs/unaltraweb/releases/tag/v0.8.0)
140
+ is published. PR122 supplies source `54afe7588c36e959a7eb75d5f52763a7456108d7`;
141
+ receipt-only PR123 supplies tag target `f71aeea75e11f4c2b15df4451f47ddd8a161bd8b`.
142
+ Annotated tag object: `eca74d75783bd6630a47376bd1a3076912a1a087`.
143
+ Receipt SHA256: `a2bf883ea62a4f164c925fa477e756378d0590f8dc6bbcdc54c17b645a7896da`.
144
+
145
+ - Wheel: `4dcec737596b4e4742e04b50a70b81d04ffadf993139e6ba1d1bce665ece4816`.
146
+ - Gem: `bbc3dbbc106b69d7a677608c46bf1d4f64d7016a48b27301022ec71e5a378f79`.
147
+ - Controller OCI: `ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:8b41abacad9ca2894755db7f3587a6b47c8fa8f536d88a151bdfc4ae53ba7f08`.
148
+ - Controller config ID: `sha256:e0502369c85c59f9e512fbd9fdb17dbc128845d057a1423375425e68ce2e79a2`.
149
+ - Runtime OCI: `ghcr.io/dosquartsdedocs/unaltraweb@sha256:d58d8f68cf08a76e9b9f3ef238a0917b856cf7152aa5cc4a6f54ca109e1ac3fe`.
150
+ - Runtime config ID: `sha256:c0ec72478618c3e7a9444cd7ae9120317a18757019b2fbf8f0438ca451695731`.
151
+
152
+ Complete [release-handoff.json](https://github.com/dosquartsdedocs/unaltraweb/releases/download/v0.8.0/release-handoff.json),
153
+ SHA256 `7a7cab1d48e3302af5f21309e57b540d7ca81eb4d8c0f36c7a5da7099538a53a`,
154
+ lists every asset, untagged Docker archive, independent worker and native profile.
155
+ Core `9d97197` / gem 0.7.3, workflow `3332e0b`, PDF 0.7.1 and classic QGIS 0.6.1 are preserved.
156
+
157
+ Signed candidate run `41641425949`, package preparation `41641430158`, Trusted
158
+ Publishing `41687937608`, OCI promotion `41687938131` and tag CI `41687888720`
159
+ passed. The exact controller's 40 package modules match the CI wheel. Its new
160
+ code fingerprint is `sha256:a678d2b2649e3479d0049ca0ca8a500aba5bb3f0a59b070cd7aaf5c0c6b466d0`;
161
+ the earlier development image/receipts above are retained as their own evidence.
162
+
163
+ The exact installed release controller exported two new fixture packages:
164
+ `9bc3b4506a3f04c66631e7bcc3617b558879af95ee29f6c3b8fead870826afa7` and
165
+ `7225f7c00f0992c922048608b622eec9a9d6d4b6a1c18049b32c2b5ffba7eb40`.
166
+ Itemarium 0.6.2 again passed all four checks on these exact bytes. Release assets
167
+ `producer-acceptance.json` and `consumer-conformance.json` have SHA256
168
+ `4a8ae9db55757b584b24b9b1602ffe94e566cd4531016a073c7afc31e78f4e48` and
169
+ `af73eba0cee0c447072df269572c22164dd8aca2eb0ea73e2416170056cd4e63`.
170
+
171
+ All 18 original assets were downloaded anonymously and hash/size checked,
172
+ including both image archives; PyPI/RubyGems copies and OCI aliases matched.
173
+ [verification.json](https://github.com/dosquartsdedocs/unaltraweb/releases/download/v0.8.0/verification.json)
174
+ has SHA256 `ec04ac1714a1b00c2f8e14f415e0fb0e21a195e42c5848ce53694ce0b85ed403`.
175
+ The downloaded public wheel was installed in a fresh environment: actual MCP
176
+ discovery reports 65 tools, its bound exporter produces a valid package, and
177
+ Itemarium's independent check passes without importing it.
178
+ [public-installation.json](https://github.com/dosquartsdedocs/unaltraweb/releases/download/v0.8.0/public-installation.json)
179
+ has SHA256 `b94920fd618d50ba384207d3e2a126ccd4b1cf98b697760d4d27c09f6b80cf75`.
180
+
181
+ Post-release verification of the updated checkout pin passes distribution/workflow
182
+ checks, the public-image bootstrap check/smoke and the full local suite: 763 cases,
183
+ 736 passed and 27 skips in that prepared environment.
184
+
185
+ The post-release checkout pin now selects the published 0.8.0 controller. The
186
+ configured hub's immutable `cbb753e4…` controls still admit Web only through 0.7.4;
187
+ this is an observed admission boundary, not a missing producer release. The full
188
+ intake is [gaContExt #54](https://github.com/dosquartsdedocs/gacontext/issues/54),
189
+ alongside Itemarium's separate #52. No hub registry, other client registration or
190
+ real manual was changed here. Source selection, hub admission and live client
191
+ activation remain distinct.
@@ -0,0 +1,130 @@
1
+ # Minimal #121 producer repair: P1 + P2
2
+
3
+ Coordination request:
4
+ `gacontext-itemarium-readiness-20261104-unaltraweb-02290003fd784c20a1659f4f803839ef`.
5
+
6
+ Authoritative workspace: `/home/benizar/git/unaltraweb`. Branch
7
+ `fix/121-context-inputs-html` starts from the preserved review125 at
8
+ `b67a51292f680d3bc0058b272c68116c3fd75bc4`; clean preflight was ELIGIBLE.
9
+ The review branch and experimental116 at
10
+ `6a8cd85ff75c8ad41dc67f8fb1c3c5a8f8919e53` remain intact. No other checkout,
11
+ registry, real manual or teacher bank is modified by this unit.
12
+
13
+ ## Implemented repairs
14
+
15
+ **P1:** `Snapshot.computation_input` validates declared check-only dependencies
16
+ separately from public sources. Only native computation input/environment
17
+ declarations use this path. Existing descriptor-relative no-follow reads,
18
+ per-file/total budgets, complete native status checks and final input/inventory
19
+ rechecks are reused. No rendering or native source execution is introduced.
20
+
21
+ The private practice root stays forbidden to images/includes/outputs, even if a
22
+ caller attempts to name it as the manual collection. Opaque inputs remain outside
23
+ the archive; fingerprints and external retained receipts bind their exact bytes.
24
+ The same role correction supports TIGIT's explicitly declared root environment
25
+ files `Dockerfile.compute-python`, `Dockerfile.compute-r`,
26
+ `requirements-compute.txt` and `requirements-compute.R`. This avoids merely moving
27
+ the first private-data error to the next environment-file error.
28
+
29
+ **P2:** HTML `alt`, `title` and `data-caption-source` values are restored before
30
+ `figure()` stores metadata and computes the ID, just as Markdown attributes were.
31
+ Tests assert exact text and identity stability when unrelated inline code is added.
32
+
33
+ No general Liquid extension, SVG raster policy, QGIS provenance adapter, W1
34
+ integration or Geo collector is included. Existing typed refusal envelopes and
35
+ v1 schema remain unchanged.
36
+
37
+ ## Tests and actual producer/receiver execution
38
+
39
+ - 30 targeted context cases, including private input success, no private payload
40
+ or path leakage, global checking for an unrelated selected chapter, missing/
41
+ changed data, root environment fingerprints, symlink/traversal/hidden-root
42
+ refusals, unchanged 10 MiB reader limit and private-collection refusal.
43
+ - Full local suite during implementation: **771 cases, 743 passed, 28 skips**;
44
+ the final declaration-type regression is additionally covered in the 30-case
45
+ targeted pass. Final-head CI retains the repository-wide gate.
46
+ - `distribution-check`, `workflow-check`, `make mcp-check`, `make mcp-smoke`: PASS.
47
+ Base runtime and PDF toolchain layers were cached; only MCP/package layers and
48
+ small metadata were rebuilt for the successive fixes.
49
+ Disk observation: 15 GiB before, 14 GiB after verification. No global cleanup.
50
+ - The acceptance runner has an explicit `--private-inputs-html` fixture mode.
51
+ It uses synthetic native computation records (not a claim of computation
52
+ rendering), a private input sentinel and the formerly broken HTML caption.
53
+ The **real MCP exporter** creates both transport archives. Assertions inspect
54
+ their exact manifests/closure, restored attributes, absence of private bytes/
55
+ paths/tokens and create-only behavior. A real CLI selects one block as well.
56
+
57
+ Installed candidate controller:
58
+ `sha256:b5b6c1e7816623a63c3340b97195253ff612458cf4b96ed443fafad2a1369bac`.
59
+ Producer version **0.8.1 candidate**, fingerprint
60
+ `sha256:2f82f9adb62a7111229e270772f00c1276ec546e9e42e4ad54e32e86e2a8ebdf`.
61
+ The isolated stdio containers have read-only rootfs, disabled network, no Docker
62
+ socket, and only their synthetic manual mounted. Both close and are confirmed absent.
63
+
64
+ Evidence below `tmp/context-p1p2-acceptance-qualified/`:
65
+
66
+ | Artifact | SHA256 |
67
+ | --- | --- |
68
+ | `producer-receipt.json` | `a610e07bc6a369079406ea93edc02581b2ec6dcbe82506ad314ed0920fec46b6` |
69
+ | `manual-fixture/tmp/manual-context/v1/manual-context.tar.gz` | `8a53b2721e24544ba45ee18b8535f298691cb857054cd7ed12189d610007c971` |
70
+ | `manual-fixture/tmp/manual-context/v2/manual-context.tar.gz` | `971d22218e3b407825643f63e524c6438a378e6a90db624afa689e4b1d66a8b9` |
71
+
72
+ Itemarium's independent source runner 0.6.2 (engine
73
+ `1a8983100c4b0c72bac74912fdde7a4d8ad14ce9970e188f13aae611dbeee813`) consumes
74
+ these exact exporter archives and passes all four checks: integrity/closure,
75
+ isolated import, existing-bank preservation and backup/reopening. Receipt:
76
+ `tmp/context-p1p2-conformance-qualified/receipt.json`, SHA256
77
+ `1660f7c576aa52e6f9fbcb39ec7d15a3a6b3c56b3a8dd63b4e2d1017d24bd9b9`.
78
+ The source-mode preliminary run remains separately retained under
79
+ `tmp/context-p1p2-acceptance-01` / `tmp/context-p1p2-conformance-01`; it predates
80
+ the 0.8.1 metadata and is not relabelled as installed-candidate proof. Intermediate
81
+ `-081` and `-081-final` runs are also retained; the qualified run includes the
82
+ HTML entity-decoding and typed-declaration corrections. Protected HTML entities
83
+ are decoded once without double-decoding surrounding attribute text.
84
+
85
+ ## Reproduction and next intake
86
+
87
+ From this checkout with a prepared Python environment and the candidate image:
88
+
89
+ ```bash
90
+ python scripts/test_manual_context_export.py \
91
+ --output /absolute/unaltraweb/tmp/new-p1p2-acceptance \
92
+ --private-inputs-html --image <exact-prepared-controller-ID>
93
+ ```
94
+
95
+ Feed its returned first/next paths and hashes directly to Itemarium's documented
96
+ `manual_package_conformance.py`, with a fresh output below Web's `tmp/`. Never
97
+ manufacture replacement archives. These tests qualify the implementation; they
98
+ do not substitute for the real TIGIT owner's subsequent export and bank workflow.
99
+
100
+ Source metadata proposes **0.8.1** for package/gem artifact/controller/runtime,
101
+ with publication status `ready` after explicit closeout authorization. The existing public checkout pin stays on
102
+ 0.8.0 until a new immutable publication is verified. Independent selections:
103
+
104
+ - Native Git core `9d97197dec5fc9346ab795191a3afd18884c27fb`, gem **0.7.3**.
105
+ - Workflow `3332e0b93933f12317e05f1ce8d38ce80046feab`.
106
+ - PDF **0.7.1**, OCI `sha256:1bb3f2dafd741e96c28639ec1cc8ccaeb81cbaa73e649f787c0b4ef59997bd4b`.
107
+ - Classic QGIS **0.6.1**, descriptor and controller/engine IDs unchanged.
108
+
109
+ Required next gates: review/integration, exact signed candidates and receipt-only
110
+ publication, cold installed proof, then a new reviewed hub intake. The local image
111
+ is not a published or admitted point. TIGIT keeps its current activated0.8.0
112
+ selection until explicit adoption, and must report a real export's path/SHA/receipt
113
+ or the next exact blocker. No real TIGIT package is claimed by this fixture proof.
114
+
115
+ ## Follow-up review: opaque YAML dependencies
116
+
117
+ PR126 discussion `4471006805` was reproduced on `b582c998`: a declared
118
+ `_practices/course/data/parameters.yml` with valid YAML anchors and a current
119
+ synthetic native computation receipt failed in `Snapshot.copy`. Check-only
120
+ dependencies now pass `opaque=True`; Reader confinement, budgets, hashes and
121
+ final source checks remain shared. Configuration reads still scan YAML even
122
+ when a previous opaque read has already staged the file.
123
+
124
+ The regression exports the anchored dataset without transporting its private
125
+ bytes/path, then rejects its changed fingerprint. A separate regression refuses
126
+ anchors on a configuration read after an opaque copy and in `_config.yml`.
127
+ The installed-export harness also includes anchored YAML alongside the CSV
128
+ input. Full local suite: 774 tests, OK (28 skipped). Earlier candidate hashes
129
+ above remain historical; the changed source requires fresh installed-candidate
130
+ and receiver evidence before publication.
@@ -0,0 +1,277 @@
1
+ # #121 — review after real-manual pilots, 2026-11-04
2
+
3
+ ## Findings and implementation order
4
+
5
+ 1. **P1, producer defect:** declared computation inputs are incorrectly subjected
6
+ to the public-content root allowlist. This is the first reported TIGIT blocker
7
+ and is reproduced against the published 0.8.0 image, independently of missing
8
+ data, stale output, receiver behavior or an old client connection.
9
+ 2. **P2, producer defect:** HTML image metadata containing inline code retains
10
+ internal protection tokens in the manifest. This was found by the review's
11
+ synthetic component probe; it is not claimed as a reported TIGIT error.
12
+ 3. **Coordinated compatibility extension:** SVG embedded rasters are refused by
13
+ both released Web 0.8.0 and Itemarium 0.6.2. Widening Web alone cannot deliver
14
+ a consumable package. Agree a bounded profile/version and update both owners.
15
+ 4. **Producer provenance extension:** recognizable QGIS capture provenance has
16
+ no context exporter adapter. This is distinct from SVG raster compatibility;
17
+ even a standalone vector SVG can hit this producer gate.
18
+
19
+ The immediate useful Web increment is **1 + 2**, with the unchanged v1 transport
20
+ and an explicit new release/adoption. Then the TIGIT owner reruns the selected
21
+ chapter. There is no evidence yet that general Liquid execution or QGIS/raster
22
+ support is necessary for that particular chapter to pass its next gate.
23
+
24
+ This is a review with executable reproductions, not a runtime repair or a new
25
+ release. Production modules, schema and pins remain unchanged. The experimental
26
+ #116 checkpoint remains `6a8cd85ff75c8ad41dc67f8fb1c3c5a8f8919e53` on
27
+ `feat/116-common-practice-sdk`; none of it is used here.
28
+
29
+ ## State: publication, admission and the actual caller
30
+
31
+ - Web 0.8.0 is published; artifact source
32
+ `54afe7588c36e959a7eb75d5f52763a7456108d7`, controller config ID
33
+ `sha256:e0502369c85c59f9e512fbd9fdb17dbc128845d057a1423375425e68ce2e79a2`,
34
+ OCI digest `sha256:8b41abacad9ca2894755db7f3587a6b47c8fa8f536d88a151bdfc4ae53ba7f08`.
35
+ - [gaContExt PR55](https://github.com/dosquartsdedocs/gacontext/pull/55) is integrated
36
+ at `ccbe1610f975faee913044c47b567cfe5b856cc0`. Configured controls are
37
+ `c6b5ab73abc399d47489c199e9b91628254b84868674712a21acf0db42e9d768`.
38
+ Web 0.8.0 and Itemarium 0.6.2 are prepared/enrolled. Managed export/import,
39
+ version preservation and backup/reopening pass. See the
40
+ [hub return](https://github.com/dosquartsdedocs/gacontext/issues/54#issuecomment-6615276920).
41
+ - The final fresh readonly TIGIT OpenCode status confirms **revision 5 / Web
42
+ 0.8.0**, with matching configuration, deployment `272e7dce…` and the enrolled
43
+ controller above. Its older live connection `4ac20e8829224276a8765beefc71b559`
44
+ still loads **0.7.4**. The initial observation in this review returned the
45
+ preceding revision-4 state; it is superseded by this reobservation and the
46
+ [TIGIT #27 return](https://github.com/geourv/tigit/issues/27#issuecomment-6625078407).
47
+ - TIGIT's owner explicitly activated the new point and invoked a bounded managed
48
+ connection with loaded/current 0.8.0, no drift, correct binding and 65 tools.
49
+ Its complete-Catalan export hits the same private-input error, creates no
50
+ package/receipt, then drains/exits and leaves its exact controller absent.
51
+ Restarting the older connection does not repair this confirmed 0.8.0 defect.
52
+ - TIGIT's local `_config.yml` already contains `unaltraweb.manual.id: tigit`.
53
+ The old missing-ID diagnosis is obsolete for that inspected working copy.
54
+ - Itemarium's 2026-11-04 report observes native **0.6.2 / 63 tools**, unchanged
55
+ frozen bytes, and a separate Web 0.7.4 connection bound to Itemarium. Its actual
56
+ real-manual CLI probe explicitly uses the published 0.8.0 image in a readonly,
57
+ network-disabled mount. It creates no real manual package.
58
+
59
+ Owner input: `itemarium/docs/handoffs/2026-11-04-manual-adoption-readiness.ca.md`,
60
+ SHA256 `1a9438e6c2f412200006f9769069e98fa42d76c101a3556b154dbc6ca6d4195c`;
61
+ retained pilot `tmp/manual-readiness-20261104/receipt.json`, SHA256
62
+ `551c372cfd0576d1ee71cbb2be0b6f614d4fc783ecc474c06c783822f20d24d2`.
63
+ These were inspected read-only while the receiver owner retained its working
64
+ changes. No teacher-bank contents were needed for this review.
65
+
66
+ ## P1 — separate check-only dependencies from transported public content
67
+
68
+ The owner reports identical exit-1 failures for the whole manual and
69
+ `--chapter manual-territorial-indicators`:
70
+
71
+ ```text
72
+ UW-MANUAL-CONTEXT
73
+ Resource is outside the public manual source roots:
74
+ _practices/tigit-projecte-territorial/data/processed/tarragones-boundaries-icgc-20260120.gpkg
75
+ ```
76
+
77
+ The declaration is present in
78
+ `assets/quarto/cartographic-language/minimum-mapping-unit-tarragones.qmd`, lines
79
+ 13–19. It is a computation input; the declared published output is an SVG under
80
+ `assets/img/`. The call chain is
81
+ `Sources._computations → dependency → Snapshot.public`, at
82
+ [`manual_context_sources.py:40–47`](https://github.com/dosquartsdedocs/unaltraweb/blob/v0.8.0/src/unaltraweb_mcp/manual_context_sources.py#L40-L47).
83
+ `Snapshot.public` is appropriate for content entering the transport but rejects
84
+ this check-only dependency before native freshness verification.
85
+ The TIGIT owner also confirms an existing regular GPKG of **598016 bytes**, below
86
+ the exporter's current 10 MiB per-file reader limit. The observed blocker is its
87
+ root/role policy, not that size limit.
88
+
89
+ Published-image reproductions:
90
+
91
+ - `public-input`: a synthetic declared input under `assets/data/` passes the
92
+ native computation status and actual export. The input itself is absent from
93
+ the transport; only the effective section and SVG are declared.
94
+ - `private-input`: the same opaque bytes and valid synthetic native record under
95
+ `_practices/fixture/data/` pass native `status` (`current_count:1`, `ok:true`),
96
+ then fail in the exporter with the reported root error. The file exists; no
97
+ missing-file or GIS-format failure is inferred. No computation is executed.
98
+ - `private-input-other-chapter`: the same refusal occurs when selecting a chapter
99
+ that does not reference the computation. This confirms the existing global
100
+ completeness gate, rather than an ineffective chapter selector.
101
+ - `changed-public-input` gives `source_or_environment_changed`; `missing-figure`
102
+ gives `Missing editorial input`. These are distinct local-source conditions.
103
+ - `private-image` is correctly refused. Adding `_practices` indiscriminately to
104
+ `public_roots` would weaken this required transport boundary.
105
+
106
+ ### Concrete repair and tests
107
+
108
+ Introduce a separate bounded dependency-read/snapshot path for **explicit native
109
+ input declarations**. Keep public-source/image selection on `Snapshot.public`.
110
+ Use the existing descriptor-relative reader, literal project-relative validation,
111
+ no-follow rules and before/after rechecks. Permit verification of the declared
112
+ private input without discovering/copying an entire private repository or sending
113
+ its content. Keep native global completeness and fingerprint checks intact.
114
+
115
+ The two roles must remain separate throughout the snapshot, input fingerprint,
116
+ resolution evidence and final transport inventory. Required regression cases:
117
+
118
+ 1. An unchanged declared `_practices/...` input passes native checking/export;
119
+ only selected sections and effective figures appear in the tar and `files`.
120
+ 2. Sentinel private data, private paths and executable source bytes do not leak
121
+ into transported Markdown/figures/manifest. A private path may remain in the
122
+ producer's external retained check receipt, not the content package.
123
+ 3. Changed/missing inputs, modified outputs and stale records still fail;
124
+ chapter selection cannot bypass the global check.
125
+ 4. Traversal, symlinks, undeclared/private image or include references and source
126
+ changes during export still fail before a sealed delivery.
127
+ 5. Dependency byte budgets remain explicit and bounded. If a real declared input
128
+ exceeds the current reader budget, assess a bounded streaming snapshot/hash
129
+ path separately; do not silently increase the image/text transport limits.
130
+
131
+ No v1 schema extension is needed for this role correction. Moving the GPKG into
132
+ public assets, removing its declaration, skipping global checks or hand-authoring
133
+ a package in Itemarium are not a verification of the repair.
134
+
135
+ ## P2 — restore HTML figure metadata before registration
136
+
137
+ Input used in `html-caption-code`:
138
+
139
+ ```html
140
+ <img src="assets/img/plain.svg" alt="Alt `code`" title="Caption `literal`">
141
+ ```
142
+
143
+ The real published exporter succeeds but records:
144
+
145
+ ```json
146
+ {"alt":"Alt UNALTRAWEBEXPORTPROTECTED0TOKEN",
147
+ "caption":"Caption UNALTRAWEBEXPORTPROTECTED1TOKEN"}
148
+ ```
149
+
150
+ [`Projection.text`](https://github.com/dosquartsdedocs/unaltraweb/blob/v0.8.0/src/unaltraweb_mcp/manual_context.py#L261-L326)
151
+ restores protected Markdown-image attributes before `figure()`, but
152
+ [`html_images`](https://github.com/dosquartsdedocs/unaltraweb/blob/v0.8.0/src/unaltraweb_mcp/manual_context.py#L328-L351)
153
+ registers HTML attributes while they still contain placeholders. The final text
154
+ restoration does not repair the previously registered manifest or figure ID.
155
+
156
+ The actual produced package SHA is
157
+ `53d065605af60cd90f73553b3c7004a52c6fa6e74ec4a0f88336f879f161296a`.
158
+ Itemarium's independent package checker accepts it: byte integrity is correct,
159
+ but `caption_preserved:false` exposes the semantic error. This is a producer
160
+ fidelity defect, not a malformed source or a receiver checksum failure.
161
+
162
+ Restore `alt`, caption/title and credits before calculating the registration ID
163
+ and storing metadata, or replace placeholder-based attribute handling with a
164
+ token-aware path. Add assertions for exact HTML/Markdown metadata parity, quoted
165
+ and escaped attributes, inline code in credits, absence of internal markers and
166
+ stable figure identity when unrelated code appears earlier in the text.
167
+
168
+ ## Components and Liquid: supported subset versus missing functionality
169
+
170
+ The `components` case combines a literal include, definitions, a localized
171
+ callout, a citation, table, subfigures and `site.baseurl`. Its actual archive is
172
+ accepted independently by Itemarium. These are the same principal source forms
173
+ observed in TIGIT chapter 2; this does not prove the complete real chapter exports.
174
+
175
+ `{{ page.title }}` and a parameterized include are refused with the documented
176
+ Liquid/include diagnostics. No real pilot return inspected here reaches one of
177
+ those errors. They are explicit unsupported producer forms, not evidence that
178
+ all Liquid/components fail or that an old client explains the private-input error.
179
+
180
+ If the subsequent pilot needs another form, record its exact source fragment and
181
+ add a deliberately bounded resolver case with source/resource closure and literal
182
+ code protection. Do not replace export with unrestricted Jekyll/Liquid execution.
183
+ Diagnostics should identify the rejected form and source location distinctly
184
+ from a missing include or a stale resource.
185
+
186
+ ## SVG with raster: coordinate producer and receiver policy
187
+
188
+ `embedded-raster` contains a valid, small PNG embedded as
189
+ `<image href="data:image/png;base64,...">` in a standalone SVG. Web rejects it at
190
+ `manual_context_media.validate_image`; Itemarium's existing `app.media.read_image`
191
+ independently rejects the **same image bytes**, before any package import.
192
+ The external-URL raster control is also refused. No alternative transport archive
193
+ is fabricated for these checks; they are image-member validator probes only.
194
+
195
+ Required coordinated work, if the pilot needs these effective figures:
196
+
197
+ - Agree the compatibility profile/version rather than silently widening v1 or
198
+ adding unrecognized manifest fields. The admitted Itemarium 0.6.2 point rejects
199
+ the current payload even if only Web's allowlist is changed.
200
+ - Permit only agreed raster MIME types, verified base64/decoded bytes, dimensions,
201
+ pixel/byte/element budgets and safe image decoding. Preserve XML/DTD/entity,
202
+ active-content and external-network/resource refusals.
203
+ - Preserve the selected edited SVG and bind the delivered effective bytes to new
204
+ resolution evidence. Never substitute the original screenshot or rasterize as
205
+ an implicit export side effect.
206
+ - Test valid embedded PNG/JPEG, oversized/malformed data, nested SVG data URLs,
207
+ external href/CSS and actual exported-package checking/import/backup in the new
208
+ receiver. New installed bytes need their own release/admission evidence.
209
+
210
+ ## QGIS provenance: a separate gate
211
+
212
+ `qgis-vector` has only safe vector geometry/text plus synthetic QGIS metadata:
213
+ Itemarium's image validator accepts the SVG, but Web refuses missing QGIS capture
214
+ provenance. Thus adding raster support alone does not solve this gate.
215
+
216
+ `qgis-untyped` supplies ordinary static vector bytes with a QGIS-related alt label.
217
+ It exports as `resolution.kind: static`. That is not proof of a fresh QGIS output:
218
+ the current byte-token/path heuristic is a refusal hint, not a provenance adapter.
219
+ Removing metadata to force static treatment is not an acceptable adoption route.
220
+
221
+ Implement an explicit mapping from a declared QGIS output to an independently
222
+ verified producer bundle/receipt, selected original/edited output and complete
223
+ required resource closure. Verify hashes, native producer/profile identity and
224
+ freshness; bind new evidence to the exact effective bytes. Reject missing/tampered
225
+ receipts, stale edits, incomplete resources and mismatched outputs. Exercise native
226
+ QGIS verification through an agreed existing interface; do not embed the private
227
+ QGIS implementation in Web or trust a caller's success boolean.
228
+
229
+ The schema already names `qgis-capture`, but no record dispatch in 0.8.0 produces
230
+ that verified kind. Pin/qualify the actual classic capture profile separately;
231
+ this review does not adopt the #116 shared-job checkpoint or a different QGIS
232
+ practice tuple. After provenance works, the effective SVG must still satisfy the
233
+ negotiated receiver image policy.
234
+
235
+ ## Reproduction and evidence
236
+
237
+ Versioned runner: `scripts/review_manual_context_pilots.py`. It requires the two
238
+ already-prepared published controller IDs (0.8.0 and 0.7.4), never pulls, and uses
239
+ network-disabled/read-only-root containers with no Docker socket and only new
240
+ synthetic fixture inputs. The 0.7.4 control rejects the unknown CLI command with
241
+ exit 2; this is distinct from the published 0.8.0 content error (exit 1).
242
+
243
+ ```bash
244
+ python scripts/review_manual_context_pilots.py \
245
+ --output /absolute/unaltraweb/tmp/context-pilot-review-new \
246
+ --receiver-python /absolute/prepared-python/bin/python \
247
+ --receiver-source /home/benizar/git/itemarium/tools/itemarium
248
+ ```
249
+
250
+ The receiver flags are paired and optional. The receiver source is read-only,
251
+ bytecode writes are disabled, and its before/after engine fingerprint is checked.
252
+ Only archives actually produced by the published exporter reach `check_package`.
253
+ There is no app/bank creation or question generation in this review.
254
+
255
+ Completed run: `tmp/manual-context-pilot-review-20261104-02/`:
256
+
257
+ - 16 producer scenarios, five actual archives independently accepted by the
258
+ receiver; expected refusals remain failures in the retained domain responses.
259
+ - Producer identity `sha256:a678d2b2649e3479d0049ca0ca8a500aba5bb3f0a59b070cd7aaf5c0c6b466d0`.
260
+ - `producer-review.json` SHA256
261
+ `3a33b4b844bbc6e41419f420146d45e96b963bede0f04ae1e690c1a09dc25d69`.
262
+ - `receiver-review.json` SHA256
263
+ `2c6944d9778e317dda5c0f6dfaf74cda5d7a339e7ad61dfb20f3012df071f521`.
264
+ - Receiver source checker: 0.6.2, engine SHA256
265
+ `1a8983100c4b0c72bac74912fdde7a4d8ad14ce9970e188f13aae611dbeee813`.
266
+ - Original fixture sources remain unchanged during export, and exact review
267
+ container absence is checked with a responsive Docker daemon.
268
+ - The initial `-01` attempt failed in the review harness's root-mounted script
269
+ path calculation before exporter execution. It is retained, corrected in the
270
+ harness and not misclassified as a product failure.
271
+ - Full existing suite: 763 cases, 736 passed, 27 skips; `git diff --check` passes.
272
+
273
+ No live consumer activation, real-manual export, publication or runtime/schema
274
+ patch is performed here. TIGIT owns its local sources/adoption and the next real
275
+ chapter export; Web owns the two identified producer repairs and coordinated
276
+ format/provenance extensions; Itemarium owns receiver changes and eventual
277
+ explicit import with bank preservation.
@@ -38,10 +38,11 @@ it to an image ID before execution. The source checkout's digest pin is separate
38
38
  it advances only after a new receipt exists. Explicit `--image` (or the console
39
39
  adapter's `UNALTRAWEB_MCP_IMAGE`) permits a reviewed immutable selection.
40
40
 
41
- The post-release checkout pin selects published 0.7.4 MCP digest
42
- `sha256:10bcaf1411f27a518969283a77e23dcc99119014b216270b3089ba85ba0b6c7e`.
43
- The [0.7.4 delivery report](scaffold-closeout-118.md) records the coherent independent
44
- core/gem tuple, reviewed scaffold migration and verified publication.
41
+ The post-release checkout pin selects published 0.8.0 MCP digest
42
+ `sha256:8b41abacad9ca2894755db7f3587a6b47c8fa8f536d88a151bdfc4ae53ba7f08`.
43
+ The [0.8.0 delivery report](manual-context-121.md) records the verified portable
44
+ manual exporter, public artifact tuple and independent Itemarium conformance.
45
+ The coherent core/gem tuple and reviewed scaffold migration from 0.7.4 remain.
45
46
  Existing processes keep their running image until clients reconnect. Site build
46
47
  defaults and customized Makefiles remain separate, explicitly reviewed selections.
47
48
 
@@ -0,0 +1,287 @@
1
+ """Opt-in, version-bound #121 review against the published 0.8.0 controller.
2
+
3
+ Creates synthetic sources, invokes the REAL installed CLI without network/socket,
4
+ and retains successes AND expected refusals. It does not repair the exporter,
5
+ render figures, manufacture transport archives, or mount a teaching manual.
6
+ Native computation locks below are explicitly synthetic check fixtures, not
7
+ evidence that R/Python rendering took place. Itemarium checking is optional and
8
+ uses its read-only source tree with a separately prepared Python interpreter.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import base64
14
+ import hashlib
15
+ import importlib.util
16
+ import io
17
+ import json
18
+ import os
19
+ from pathlib import Path
20
+ import subprocess
21
+ import sys
22
+ import tarfile
23
+ import uuid
24
+
25
+ ROOT = Path(__file__).resolve().parent.parent
26
+ IMAGE = "sha256:e0502369c85c59f9e512fbd9fdb17dbc128845d057a1423375425e68ce2e79a2"
27
+ OLD_IMAGE = "sha256:ea14b1224382e47031b979d294f970b66773155584ac8f9eb94a33dc6421b0c9"
28
+ SOURCE = "54afe7588c36e959a7eb75d5f52763a7456108d7"
29
+ PRODUCER = "sha256:a678d2b2649e3479d0049ca0ca8a500aba5bb3f0a59b070cd7aaf5c0c6b466d0"
30
+ SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="30" height="20"><rect width="30" height="20" fill="white"/><text x="1" y="12">Fixture</text></svg>'
31
+
32
+
33
+ def sha(data):
34
+ return hashlib.sha256(data).hexdigest()
35
+
36
+
37
+ def save(path, value):
38
+ with path.open("x", encoding="utf-8") as stream:
39
+ json.dump(value, stream, indent=2, sort_keys=True, ensure_ascii=False)
40
+ stream.write("\n")
41
+
42
+
43
+ def write(root, name, data):
44
+ path = root / name
45
+ path.parent.mkdir(parents=True, exist_ok=True)
46
+ with path.open("xb") as stream:
47
+ stream.write(data.encode() if isinstance(data, str) else data)
48
+
49
+
50
+ def command(argv, *, cwd=None, expected=0, env=None):
51
+ result = subprocess.run(argv, cwd=cwd, env=env, capture_output=True, text=True, timeout=180)
52
+ if expected is not None and result.returncode != expected:
53
+ raise RuntimeError(f"Command failed: {argv}: {result.returncode}\n{result.stderr[-4000:]}")
54
+ return result
55
+
56
+
57
+ def cases(root):
58
+ from PIL import Image
59
+ import yaml
60
+ png = io.BytesIO()
61
+ Image.new("RGB", (2, 2), "navy").save(png, format="PNG")
62
+ encoded = base64.b64encode(png.getvalue()).decode()
63
+ rows = [
64
+ {"id": "baseline", "body": 'Text.\n\n![Alt](assets/img/plain.svg "Caption")\n', "ok": True},
65
+ {"id": "missing-id", "no_id": True, "error": "unaltraweb.manual.id"},
66
+ {"id": "missing-figure", "body": '![Alt](assets/img/missing.svg "Caption")\n', "error": "Missing editorial input"},
67
+ {"id": "public-input", "compute": "assets/data/input.gpkg", "ok": True},
68
+ {"id": "private-input", "compute": "_practices/fixture/data/input.gpkg", "error": "outside the public manual source roots"},
69
+ {"id": "private-input-other-chapter", "compute": "_practices/fixture/data/input.gpkg", "unreferenced": True,
70
+ "chapter": "first", "error": "outside the public manual source roots"},
71
+ {"id": "changed-public-input", "compute": "assets/data/input.gpkg", "changed_input": True,
72
+ "error": "source_or_environment_changed"},
73
+ {"id": "private-image", "body": '![Alt](_practices/fixture/private.png "Private")\n',
74
+ "extra": {"_practices/fixture/private.png": png.getvalue()}, "error": "outside the public manual source roots"},
75
+ {"id": "components", "body": '''{% include note.md %}
76
+
77
+ {% cite fixture %}
78
+
79
+ ::: table "Table caption"
80
+ | A | B |
81
+ | --- | --- |
82
+ | 1 | 2 |
83
+ :::
84
+
85
+ ::: subfigures a+b "Group caption"
86
+ ![A]({{ site.baseurl }}/assets/img/plain.svg "Panel A")
87
+ ![B](assets/img/plain.svg "Panel B")
88
+ :::
89
+ ''', "extra": {"_includes/note.md": "Term\n: Exact definition.\n\n>>>> Warning text.\n",
90
+ "_bibliography/manual.bib": '@book{fixture, title={Fixture reference}, author={Author, A}, year={2026}}'}, "ok": True},
91
+ {"id": "liquid-expression", "body": "Title: {{ page.title }}\n", "error": "unresolved Liquid/include"},
92
+ {"id": "include-parameters", "body": '{% include note.md label="Example" %}\n',
93
+ "extra": {"_includes/note.md": "A note."}, "error": "without parameters"},
94
+ {"id": "html-caption-code", "body": '<img src="assets/img/plain.svg" alt="Alt `code`" title="Caption `literal`">\n',
95
+ "ok": True, "expected_caption": "Caption `literal`"},
96
+ {"id": "embedded-raster", "body": '![Raster](assets/img/embedded.svg "Raster")\n',
97
+ "image": "assets/img/embedded.svg", "extra": {"assets/img/embedded.svg": '<svg xmlns="http://www.w3.org/2000/svg"><image href="data:image/png;base64,' + encoded + '" width="2" height="2"/></svg>'},
98
+ "error": "cannot receive SVG <image>"},
99
+ {"id": "external-raster", "body": '![Raster](assets/img/external.svg "Raster")\n',
100
+ "image": "assets/img/external.svg", "extra": {"assets/img/external.svg": '<svg xmlns="http://www.w3.org/2000/svg"><image href="https://example.invalid/image.png"/></svg>'},
101
+ "error": "cannot receive SVG <image>"},
102
+ {"id": "qgis-vector", "body": '![Map](assets/img/map.svg "Map")\n', "image": "assets/img/map.svg",
103
+ "extra": {"assets/img/map.svg": SVG.replace("<rect", '<metadata>{"producer":"unaltracaptura-qgis"}</metadata><rect')},
104
+ "error": "QGIS capture handoff is not supported"},
105
+ {"id": "qgis-untyped", "body": '![QGIS result](assets/img/map.svg "Authored static bytes")\n',
106
+ "image": "assets/img/map.svg", "extra": {"assets/img/map.svg": SVG}, "ok": True},
107
+ ]
108
+ for row in rows:
109
+ case = root / row["id"]
110
+ case.mkdir()
111
+ config = {"theme": "unaltraweb", "title": "Synthetic review", "default_lang": "ca", "languages": ["ca"],
112
+ "exclude": ["tmp/", "_practices/"], "unaltraweb": {"site_profile": "unaltremanual", "manual": {"id": "review-121"}}}
113
+ if row.get("no_id"):
114
+ del config["unaltraweb"]["manual"]["id"]
115
+ write(case, "_config.yml", yaml.safe_dump(config))
116
+ write(case, ".gitignore", "/tmp/\n")
117
+ write(case, "assets/img/plain.svg", SVG)
118
+ body = row.get("body", 'Text.\n\n![Alt](assets/img/plain.svg "Caption")\n')
119
+ if row.get("compute"):
120
+ write(case, ".unaltraweb/computations.yml", "version: 1\nsource_roots: [assets/compute]\n")
121
+ write(case, row["compute"], b"OPAQUE SYNTHETIC FINGERPRINT INPUT; NOT A GIS VALIDITY TEST\n")
122
+ front = {"title": "Computed fixture", "lang": "ca", "ref": "computed-fixture", "unaltraweb_compute": {
123
+ "engine": "python", "mode": "figure", "inputs": [row["compute"]], "outputs": ["assets/img/result.svg"]}}
124
+ write(case, "assets/compute/figure.qmd", "---\n" + yaml.safe_dump(front) + "---\nSynthetic source; no computation execution.\n")
125
+ write(case, "assets/img/result.svg", SVG)
126
+ if not row.get("unreferenced"):
127
+ body = '![Computed](assets/compute/figure.qmd "Computed fixture")\n'
128
+ write(case, "_chapters/ca/first.md", "---\ntitle: First\nlang: ca\nref: first\n---\n" + body)
129
+ for name, data in row.pop("extra", {}).items():
130
+ write(case, name, data)
131
+ command(["git", "init", "-b", "fixture"], cwd=case)
132
+ paths = [p.relative_to(case).as_posix() for p in case.rglob("*") if p.is_file() and ".git" not in p.parts]
133
+ command(["git", "add", "--", *paths], cwd=case)
134
+ command(["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", "commit", "-m", "Synthetic review sources"], cwd=case)
135
+ save(root / "cases.json", rows)
136
+
137
+
138
+ def worker(root):
139
+ """Executed in the exact published image. No exporter function is replaced."""
140
+ from unaltraweb_mcp import __version__
141
+ from unaltraweb_mcp.manual_context import producer_identity
142
+ assert root == Path("/cases") and os.environ.get("UW_REVIEW_WORKER") == "1"
143
+ identity = producer_identity(Path("/opt/unaltraweb"))
144
+ assert __version__ == "0.8.0" and identity["revision"] == PRODUCER
145
+ rows = json.loads((root / "cases.json").read_text())
146
+ results = []
147
+ for row in rows:
148
+ case = root / row["id"]
149
+ native_status = None
150
+ if row.get("compute"):
151
+ spec = importlib.util.spec_from_file_location("review_native_compute", "/opt/unaltraweb/scripts/computations/render.py")
152
+ native = importlib.util.module_from_spec(spec)
153
+ spec.loader.exec_module(native)
154
+ config, config_path = native.load_config(case)
155
+ record, = native.discover_sources(case, config)
156
+ fingerprint, dependencies, image = native.fingerprint(case, config, config_path, record)
157
+ saved = {"fingerprint": fingerprint, "dependencies": dependencies, "image": image,
158
+ "image_identity": native.inspect_image(image["image"]),
159
+ "outputs": [{"path": p, **native.file_signature(case / p)} for p in record["output_paths"]]}
160
+ save(case / ".unaltraweb/computations.lock.json", {"version": 1, "records": {record["source_path"]: saved}})
161
+ native_status = native.status(case)
162
+ assert native_status["ok"], native_status
163
+ if row.get("changed_input"):
164
+ with (case / row["compute"]).open("ab") as stream:
165
+ stream.write(b"CHANGED INPUT\n")
166
+ before = {p.relative_to(case).as_posix(): sha(p.read_bytes()) for p in case.rglob("*")
167
+ if p.is_file() and ".git" not in p.parts and "tmp" not in p.relative_to(case).parts}
168
+ argv = [sys.executable, "-I", "-B", "-m", "unaltraweb_mcp.cli", "--project", str(case), "mcp",
169
+ "manual-context-export", "--name", "review", "--language", "ca"]
170
+ if row.get("chapter"):
171
+ argv += ["--chapter", row["chapter"]]
172
+ ran = command(argv, expected=None)
173
+ result = json.loads(ran.stdout)
174
+ expected = (ran.returncode == 0 and result.get("ok") is True) if row.get("ok") else (
175
+ ran.returncode == 1 and result.get("ok") is False and row["error"] in result.get("error", ""))
176
+ item = {"id": row["id"], "expected_behavior_observed": expected, "command": argv, "exit_code": ran.returncode,
177
+ "result": result, "stderr": ran.stderr, "source_hashes": before,
178
+ "synthetic_native_status": native_status, "renderer_executed": False}
179
+ archive = case / "tmp/manual-context/review/manual-context.tar.gz"
180
+ item["archive_created"] = archive.exists()
181
+ if result.get("ok"):
182
+ assert archive.is_file() and sha(archive.read_bytes()) == result["sha256"]
183
+ with tarfile.open(archive) as content:
184
+ members = {m.name: content.extractfile(m).read() for m in content.getmembers()}
185
+ manifest = json.loads(members.pop("manual-context.json"))
186
+ assert set(manifest["files"]) == set(members)
187
+ assert all(entry["sha256"] == sha(members[p]) and entry["size_bytes"] == len(members[p]) for p, entry in manifest["files"].items())
188
+ item["manifest"] = manifest
189
+ if row.get("expected_caption"):
190
+ item["expected_caption"] = row["expected_caption"]
191
+ item["caption_preserved"] = manifest["figures"][0]["caption"] == row["expected_caption"]
192
+ else:
193
+ assert not archive.exists(), "A refusal left a transport package"
194
+ assert all(sha((case / p).read_bytes()) == value for p, value in before.items())
195
+ save(root / (row["id"] + ".result.json"), item)
196
+ results.append(item)
197
+ save(root / "producer-review.json", {"ok": all(row["expected_behavior_observed"] for row in results),
198
+ "purpose": "Version-bound baseline review, not acceptance of known defects", "producer": identity,
199
+ "synthetic_only": True, "real_manual_mounted": False, "renderer_executed": False, "cases": results})
200
+
201
+
202
+ RECEIVER = '''import json, sys
203
+ from pathlib import Path
204
+ import itemarium
205
+ from itemarium.app.media import read_image
206
+ from itemarium.app.manual_package import check_package
207
+ from itemarium.runtime_identity import source_sha256
208
+ root = Path(sys.argv[1]); engine = Path(itemarium.__file__).parent
209
+ before = source_sha256(engine); results = []
210
+ for case in json.loads((root/'cases.json').read_text()):
211
+ result = json.loads((root/(case['id']+'.result.json')).read_text())['result']
212
+ row = {'id':case['id']}
213
+ if result.get('ok'):
214
+ row['package_check'] = check_package(package_path=root/case['id']/result['path'], sha256=result['sha256'])
215
+ if case.get('image'):
216
+ try:
217
+ read_image(root/case['id']/case['image']); row['image_check'] = {'ok':True}
218
+ except ValueError as error:
219
+ row['image_check'] = {'ok':False, 'error':str(error)}
220
+ results.append(row)
221
+ assert source_sha256(engine) == before
222
+ report = {'engine_version':itemarium.__version__, 'engine_source_sha256':before, 'cases':results,
223
+ 'imports_performed':False, 'manufacturer_of_alternate_packages':False,
224
+ 'image_checks_are_member_validation_only':True}
225
+ with (root/'receiver-review.json').open('x') as output:
226
+ json.dump(report, output, sort_keys=True, indent=2); output.write('\\n')
227
+ '''
228
+
229
+
230
+ def main():
231
+ parser = argparse.ArgumentParser(description=__doc__)
232
+ parser.add_argument("--output", type=Path)
233
+ parser.add_argument("--receiver-python", type=Path)
234
+ parser.add_argument("--receiver-source", type=Path)
235
+ parser.add_argument("--worker", type=Path, help=argparse.SUPPRESS)
236
+ args = parser.parse_args()
237
+ if args.worker:
238
+ worker(args.worker)
239
+ return
240
+ if not args.output or bool(args.receiver_python) != bool(args.receiver_source):
241
+ parser.error("Supply --output; pair --receiver-python with --receiver-source")
242
+ if not (ROOT / "mcp-factory.yml").is_file():
243
+ parser.error("Run this review harness from the unaltraweb source checkout")
244
+ output = args.output.resolve()
245
+ if output != args.output or not output.is_relative_to(ROOT / "tmp") or not output.parent.is_dir():
246
+ parser.error("Use a new absolute output below this checkout's tmp, with an existing parent")
247
+ observed, = json.loads(command(["docker", "image", "inspect", IMAGE]).stdout)
248
+ assert observed["Id"] == IMAGE and observed["Config"]["Labels"]["org.opencontainers.image.revision"] == SOURCE
249
+ output.mkdir(mode=0o700)
250
+ cases(output)
251
+ name = "unaltraweb-review121-" + uuid.uuid4().hex
252
+ argv = ["docker", "run", "--rm", "--pull", "never", "--name", name, "--network", "none", "--read-only",
253
+ "--user", f"{os.getuid()}:{os.getgid()}", "--cpus", "2", "--memory", "2g", "--pids-limit", "256",
254
+ "--cap-drop", "ALL", "--security-opt", "no-new-privileges", "--tmpfs", "/tmp:rw,nosuid,nodev,size=268435456",
255
+ "-e", "HOME=/tmp", "-e", "PYTHONDONTWRITEBYTECODE=1", "-e", "UW_REVIEW_WORKER=1",
256
+ "--mount", f"type=bind,source={output},target=/cases",
257
+ "--mount", f"type=bind,source={Path(__file__).resolve()},target=/review.py,readonly",
258
+ "--entrypoint", "python3", IMAGE, "-I", "-B", "/review.py", "--worker", "/cases"]
259
+ for path in (output, Path(__file__).resolve()):
260
+ if any(c in str(path) for c in ",\r\n"):
261
+ raise ValueError("Review mount path needs a simple literal spelling")
262
+ ran = command(argv, expected=None)
263
+ save(output / "execution.json", {"command": argv, "exit_code": ran.returncode, "stdout": ran.stdout, "stderr": ran.stderr})
264
+ command(["docker", "info", "--format", "{{.ID}}"])
265
+ absent = command(["docker", "container", "inspect", name], expected=None)
266
+ assert absent.returncode == 1 and "No such" in absent.stderr, "Exact review container absence not established"
267
+ if ran.returncode:
268
+ raise RuntimeError(ran.stderr[-6000:])
269
+ report = json.loads((output / "producer-review.json").read_text())
270
+ assert report["ok"], "A scenario differed from the recorded 0.8.0 behavior; inspect retained results"
271
+ old, = json.loads(command(["docker", "image", "inspect", OLD_IMAGE]).stdout)
272
+ assert old["Id"] == OLD_IMAGE
273
+ old_result = command(["docker", "run", "--rm", "--pull", "never", "--network", "none", "--read-only",
274
+ "--entrypoint", "unaltraweb-mcp", OLD_IMAGE, "mcp", "manual-context-export", "--name", "probe"], expected=None)
275
+ assert old_result.returncode == 2 and "invalid choice" in old_result.stderr
276
+ save(output / "old-runtime.json", {"image_id": OLD_IMAGE, "exit_code": old_result.returncode, "stderr": old_result.stderr})
277
+ if args.receiver_python:
278
+ environment = {**os.environ, "PYTHONPATH": str(args.receiver_source.resolve()), "PYTHONDONTWRITEBYTECODE": "1"}
279
+ # Keep a venv interpreter's spelling: resolving its python symlink would
280
+ # silently select the system interpreter and lose the prepared dependencies.
281
+ command([str(args.receiver_python.absolute()), "-B", "-c", RECEIVER, str(output)], env=environment)
282
+ print(json.dumps({"ok": True, "output": str(output), "cases": len(report["cases"]),
283
+ "receipt_sha256": sha((output / "producer-review.json").read_bytes()), "exact_container_absent": True}, indent=2))
284
+
285
+
286
+ if __name__ == "__main__":
287
+ main()
@@ -14,11 +14,12 @@ from pathlib import Path
14
14
  import sys
15
15
  import subprocess
16
16
  import uuid
17
+ import tarfile
17
18
 
18
19
  ROOT = Path(__file__).resolve().parents[1]
19
20
  sys.path.insert(0, str(ROOT / "src"))
20
21
  sys.path.insert(0, str(ROOT / "test"))
21
- from test_manual_context import fixture, write, commit # noqa: E402
22
+ from test_manual_context import fixture, write, commit, computation_fixture # noqa: E402
22
23
 
23
24
 
24
25
  def save(path, value):
@@ -27,6 +28,18 @@ def save(path, value):
27
28
  stream.write("\n")
28
29
 
29
30
 
31
+ def verify_private_inputs_html(project, result):
32
+ with tarfile.open(project / result["path"]) as archive:
33
+ files = {entry.name: archive.extractfile(entry).read() for entry in archive.getmembers()}
34
+ manifest = json.loads(files["manual-context.json"])
35
+ assert manifest["figures"][0]["caption"] == "Caption `literal`"
36
+ assert manifest["figures"][0]["alt"] == "Alt `code`"
37
+ assert set(files) == {"manual-context.json", *manifest["files"]}
38
+ data = b"".join(files.values())
39
+ assert b"PRIVATE_CHECK_ONLY_SENTINEL" not in data and b"_practices/" not in data
40
+ assert b"UNALTRAWEBEXPORTPROTECTED" not in data
41
+
42
+
30
43
  async def export(project, name, image=""):
31
44
  from mcp import ClientSession, StdioServerParameters
32
45
  from mcp.client.stdio import stdio_client
@@ -78,6 +91,7 @@ def main():
78
91
  parser.add_argument("--output", type=Path, required=True)
79
92
  parser.add_argument("--single", action="store_true")
80
93
  parser.add_argument("--image", default="", help="Already-built MCP image; pull-never, network-disabled fixture stdio")
94
+ parser.add_argument("--private-inputs-html", action="store_true", help="Exercise P1/P2 with explicitly synthetic native computation records")
81
95
  args = parser.parse_args()
82
96
  output = args.output.resolve()
83
97
  if output != args.output or not output.is_relative_to(ROOT / "tmp") or not output.parent.is_dir():
@@ -86,7 +100,20 @@ def main():
86
100
  project = output / "manual-fixture"
87
101
  project.mkdir()
88
102
  fixture(project)
103
+ if args.private_inputs_html:
104
+ write(project, "_practices/fixture/data/input.csv", "PRIVATE_CHECK_ONLY_SENTINEL,1\n")
105
+ write(project, "_practices/fixture/data/parameters.yml", "defaults: &defaults {value: PRIVATE_CHECK_ONLY_SENTINEL}\nselected: *defaults\n")
106
+ computation_fixture(project, inputs=["_practices/fixture/data/input.csv", "_practices/fixture/data/parameters.yml"])
107
+ chapter = project / "_chapters/ca/first.md"
108
+ original = chapter.read_text()
109
+ original = original.replace('![Alt](assets/img/map.png "Mapa de prova"){: data-caption-source="Autoria sintètica"}',
110
+ '<img src="assets/img/map.png" alt="Alt `code`" title="Caption `literal`">')
111
+ write(project, "_chapters/ca/first.md", original)
112
+ commit(project, ["_practices/fixture/data/input.csv", "_practices/fixture/data/parameters.yml", ".unaltraweb/computations.yml", ".unaltraweb/computations.lock.json",
113
+ "assets/compute/figure.qmd", "assets/img/figure.svg", "_chapters/ca/first.md"], "P1/P2 synthetic native check inputs")
89
114
  first = asyncio.run(export(project, "v1", args.image))
115
+ if args.private_inputs_html:
116
+ verify_private_inputs_html(project, first["export"])
90
117
  save(output / "producer-v1.json", first)
91
118
  command = [sys.executable, "-B", "-m", "unaltraweb_mcp.cli", "--project", str(project), "mcp", "manual-context-export",
92
119
  "--name", "cli-block", "--language", "ca", "--chapter", "first", "--section", "first#method"]
@@ -96,13 +123,16 @@ def main():
96
123
  assert cli["ok"] and cli["sections"] == 1 and cli["figures"] == 1, cli
97
124
  save(output / "producer-cli.json", {"command": command, "result": cli})
98
125
  result = {"ok": True, "synthetic_manual": True, "producer_export_invoked": True, "transport": "mcp-stdio",
99
- "first": first["export"], "cli_verified": True, "questions_generated": 0}
126
+ "first": first["export"], "cli_verified": True, "questions_generated": 0,
127
+ "private_inputs_html": args.private_inputs_html, "computation_render_executed": False}
100
128
  if not args.single:
101
129
  original = (project / "_chapters/ca/first.md").read_text()
102
130
  write(project, "_chapters/ca/first.md", original + "\nSegona versió: text afegit.\n")
103
131
  write(project, "_chapters/ca/second.md", "---\ntitle: Segon\nref: second\nlang: ca\nweight: 20\n---\nCapítol afegit en la segona versió.\n")
104
132
  commit(project, ["_chapters/ca/first.md", "_chapters/ca/second.md"], "Second fixture revision")
105
133
  second = asyncio.run(export(project, "v2", args.image))
134
+ if args.private_inputs_html:
135
+ verify_private_inputs_html(project, second["export"])
106
136
  save(output / "producer-v2.json", second)
107
137
  result["next"] = second["export"]
108
138
  assert first["export"]["manual"]["id"] == second["export"]["manual"]["id"]
@@ -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:10bcaf1411f27a518969283a77e23dcc99119014b216270b3089ba85ba0b6c7e}"
18
+ image="${UNALTRAWEB_MCP_IMAGE:-ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:8b41abacad9ca2894755db7f3587a6b47c8fa8f536d88a151bdfc4ae53ba7f08}"
19
19
  project="${MCP_CONSUMER_WORKSPACE:-${UNALTRAWEB_PROJECT:-}}"
20
20
  operation=serve
21
21
  expected_image="${UNALTRAWEB_EXPECTED_IMAGE_ID:-}"
@@ -2,8 +2,8 @@
2
2
  "$schema": "component-contract.schema.json",
3
3
  "schema_version": 1,
4
4
  "release": {
5
- "version": "0.8.0",
6
- "tag": "v0.8.0"
5
+ "version": "0.8.1",
6
+ "tag": "v0.8.1"
7
7
  },
8
8
  "consumer_integration": {
9
9
  "schema_version": 1,
@@ -62,10 +62,10 @@
62
62
  "gem": {
63
63
  "kind": "gem",
64
64
  "name": "unaltraweb",
65
- "version": "0.8.0",
66
- "release": "v0.8.0",
65
+ "version": "0.8.1",
66
+ "release": "v0.8.1",
67
67
  "release_status": "ready",
68
- "reference": "unaltraweb (= 0.8.0)",
68
+ "reference": "unaltraweb (= 0.8.1)",
69
69
  "repository": "https://github.com/dosquartsdedocs/unaltraweb",
70
70
  "included_in_wheel": false,
71
71
  "features": ["jekyll-core", "site-build"]
@@ -73,10 +73,10 @@
73
73
  "wheel": {
74
74
  "kind": "python-wheel",
75
75
  "name": "unaltraweb-mcp",
76
- "version": "0.8.0",
77
- "release": "v0.8.0",
76
+ "version": "0.8.1",
77
+ "release": "v0.8.1",
78
78
  "release_status": "ready",
79
- "reference": "unaltraweb-mcp==0.8.0",
79
+ "reference": "unaltraweb-mcp==0.8.1",
80
80
  "repository": "https://github.com/dosquartsdedocs/unaltraweb",
81
81
  "included_in_wheel": true,
82
82
  "features": ["calibre-import", "doctor", "new-web", "offline-inspection", "mcp-control-plane", "source-ci"]
@@ -84,10 +84,10 @@
84
84
  "runtime": {
85
85
  "kind": "container",
86
86
  "name": "unaltraweb runtime",
87
- "version": "0.8.0",
88
- "release": "v0.8.0",
87
+ "version": "0.8.1",
88
+ "release": "v0.8.1",
89
89
  "release_status": "ready",
90
- "reference": "ghcr.io/dosquartsdedocs/unaltraweb:0.8.0",
90
+ "reference": "ghcr.io/dosquartsdedocs/unaltraweb:0.8.1",
91
91
  "image_repository": "ghcr.io/dosquartsdedocs/unaltraweb",
92
92
  "repository": "https://github.com/dosquartsdedocs/unaltraweb",
93
93
  "included_in_wheel": false,
@@ -96,10 +96,10 @@
96
96
  "mcp": {
97
97
  "kind": "container",
98
98
  "name": "unaltraweb MCP runtime",
99
- "version": "0.8.0",
100
- "release": "v0.8.0",
99
+ "version": "0.8.1",
100
+ "release": "v0.8.1",
101
101
  "release_status": "ready",
102
- "reference": "ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.8.0",
102
+ "reference": "ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.8.1",
103
103
  "image_repository": "ghcr.io/dosquartsdedocs/unaltraweb-mcp",
104
104
  "repository": "https://github.com/dosquartsdedocs/unaltraweb",
105
105
  "included_in_wheel": false,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unaltraweb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - dosquartsdedocs
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-11-03 00:00:00.000000000 Z
11
+ date: 2026-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: classifier-reborn
@@ -808,6 +808,8 @@ files:
808
808
  - docs/agents/deployment-integration-0.7.1.md
809
809
  - docs/agents/manual-authoring-components.md
810
810
  - docs/agents/manual-context-121.md
811
+ - docs/agents/manual-context-fix-121.md
812
+ - docs/agents/manual-context-pilot-review-121.md
811
813
  - docs/agents/mcp-contract.md
812
814
  - docs/agents/owner-closeout-76.md
813
815
  - docs/agents/owner-closeout-80.md
@@ -859,6 +861,7 @@ files:
859
861
  - scripts/manual/verify_release_assets.py
860
862
  - scripts/prepare_practice_candidate.py
861
863
  - scripts/prepare_practice_editions_candidate.py
864
+ - scripts/review_manual_context_pilots.py
862
865
  - scripts/test_gem_build.py
863
866
  - scripts/test_manual_context_export.py
864
867
  - scripts/test_reproducible_jekyll_build.py