@rubytech/create-maxy-code 0.1.71 → 0.1.72
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.
- package/package.json +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +78 -7
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +11 -4
- package/payload/platform/templates/specialists/agents/database-operator.md +5 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +4 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/build.md +18 -25
package/package.json
CHANGED
|
@@ -232,6 +232,7 @@ RECORDER_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cu
|
|
|
232
232
|
BODY_OK=$(printf '%s' "$RECORDER_BODY" | python3 -c '
|
|
233
233
|
import sys, json
|
|
234
234
|
op = sys.argv[1]
|
|
235
|
+
acct = sys.argv[2]
|
|
235
236
|
try:
|
|
236
237
|
outer = json.load(sys.stdin)
|
|
237
238
|
msg = outer.get("initialMessage")
|
|
@@ -245,8 +246,14 @@ try:
|
|
|
245
246
|
msg.rstrip().endswith("do not emit it.")))
|
|
246
247
|
conds.append(("no-placeholder-schema", "{schema}" not in msg))
|
|
247
248
|
conds.append(("no-placeholder-conversation", "{conversation}" not in msg))
|
|
249
|
+
conds.append(("no-placeholder-accountid", "{accountId}" not in msg))
|
|
248
250
|
# schema-base.md content is interpolated.
|
|
249
251
|
conds.append(("contains-schema-base-header", "Schema Reference" in msg))
|
|
252
|
+
# Task 199 — accountId clarifier sentence + literal value rendered.
|
|
253
|
+
conds.append(("has-accountid-clarifier",
|
|
254
|
+
"accountId" in msg and "automatically supplied" in msg))
|
|
255
|
+
conds.append(("has-accountid-value",
|
|
256
|
+
"The current accountId is `" + acct + "`." in msg))
|
|
250
257
|
# The conversation lines appear in role: text format.
|
|
251
258
|
conds.append(("has-user-1", "\nuser: New Real Agent session" in msg))
|
|
252
259
|
conds.append(("has-asst-1",
|
|
@@ -266,20 +273,22 @@ try:
|
|
|
266
273
|
print("yes" if not failed else "no:"+repr(failed))
|
|
267
274
|
except Exception as e:
|
|
268
275
|
print("parse-fail:"+str(e))
|
|
269
|
-
' "$OP_ID" 2>/dev/null)
|
|
276
|
+
' "$OP_ID" "$ACCT_ID" 2>/dev/null)
|
|
270
277
|
if [[ "$BODY_OK" == "yes" ]]; then
|
|
271
|
-
pass "case-5c initialMessage is the filled
|
|
278
|
+
pass "case-5c initialMessage is the filled paragraph; schema/accountId/conversation placeholders substituted; accountId value rendered; transcript as role: text lines"
|
|
272
279
|
else
|
|
273
280
|
fail "case-5c filled-prompt shape wrong ($BODY_OK)"
|
|
274
281
|
fi
|
|
275
282
|
|
|
276
|
-
# 5h. (Task 195) exactly one `substitution` log-ingest line carrying
|
|
277
|
-
# positive byte counts for schema / conversation / body
|
|
278
|
-
|
|
283
|
+
# 5h. (Task 195 + 199) exactly one `substitution` log-ingest line carrying
|
|
284
|
+
# positive byte counts for schema / conversation / body AND
|
|
285
|
+
# accountIdPresent=yes (the envelope's accountId is non-empty here —
|
|
286
|
+
# run_hook always passes ACCOUNT_ID="$ACCT_ID" into the hook env).
|
|
287
|
+
SUBST_LINE_COUNT=$(ingest_lines | grep -cE "^substitution sessionId=${OP_ID} schemaBytes=[1-9][0-9]* conversationBytes=[1-9][0-9]* bodyBytes=[1-9][0-9]* accountIdPresent=yes$" || true)
|
|
279
288
|
if [[ "$SUBST_LINE_COUNT" -eq 1 ]]; then
|
|
280
|
-
pass "case-5h substitution log line emitted once with positive byte counts"
|
|
289
|
+
pass "case-5h substitution log line emitted once with positive byte counts and accountIdPresent=yes"
|
|
281
290
|
else
|
|
282
|
-
fail "case-5h expected exactly 1 substitution line w/ positive bytes, got $SUBST_LINE_COUNT (lines: $(ingest_lines))"
|
|
291
|
+
fail "case-5h expected exactly 1 substitution line w/ positive bytes + accountIdPresent=yes, got $SUBST_LINE_COUNT (lines: $(ingest_lines))"
|
|
283
292
|
fi
|
|
284
293
|
|
|
285
294
|
# 5i. (Task 195) substitution line lands between envelope and spawn-request.
|
|
@@ -605,6 +614,68 @@ else
|
|
|
605
614
|
fail "case-10 escaping wrong ($ESC_OK): $ESC_BODY"
|
|
606
615
|
fi
|
|
607
616
|
|
|
617
|
+
# --- Case 11 (Task 199): missing accountId → accountIdPresent=no, no crash ---
|
|
618
|
+
# When ACCOUNT_ID env is empty, the envelope's accountId field is "" and
|
|
619
|
+
# the {accountId} placeholder collapses to an empty string. The hook does
|
|
620
|
+
# NOT loud-fail (the agent's new clarifier sentence + the server-side
|
|
621
|
+
# validator on the writers handle the bad signal). The substitution log
|
|
622
|
+
# line emits accountIdPresent=no — the regression signature for upstream
|
|
623
|
+
# envelope-shape drift.
|
|
624
|
+
: > "$REQ_LOG"
|
|
625
|
+
NO_ACCT_STDIN_FILE=$(mktemp); TMPFILES+=("$NO_ACCT_STDIN_FILE")
|
|
626
|
+
printf '%s' "$ORDERED_ENVELOPE" > "$NO_ACCT_STDIN_FILE"
|
|
627
|
+
NO_ACCT_STDERR=$(mktemp); TMPFILES+=("$NO_ACCT_STDERR")
|
|
628
|
+
NO_ACCT_STDOUT=$(mktemp); TMPFILES+=("$NO_ACCT_STDOUT")
|
|
629
|
+
MAXY_SESSION_ROLE="admin" \
|
|
630
|
+
MAXY_SPECIALIST="" \
|
|
631
|
+
MAXY_UI_INTERNAL_PORT="$LISTENER_PORT" \
|
|
632
|
+
ACCOUNT_ID="" \
|
|
633
|
+
bash "$HOOK" <"$NO_ACCT_STDIN_FILE" >"$NO_ACCT_STDOUT" 2>"$NO_ACCT_STDERR"
|
|
634
|
+
NO_ACCT_RC=$?
|
|
635
|
+
sleep 0.1
|
|
636
|
+
[[ "$NO_ACCT_RC" -eq 0 ]] || fail "case-11 rc=$NO_ACCT_RC stderr=$(cat "$NO_ACCT_STDERR")"
|
|
637
|
+
[[ -z "$(cat "$NO_ACCT_STDERR")" ]] || fail "case-11 stderr must be empty, got: $(cat "$NO_ACCT_STDERR")"
|
|
638
|
+
|
|
639
|
+
# 11a. substitution log line carries accountIdPresent=no
|
|
640
|
+
SUBST_NO_COUNT=$(ingest_lines | grep -cE "^substitution sessionId=${OP_ID} schemaBytes=[1-9][0-9]* conversationBytes=[1-9][0-9]* bodyBytes=[1-9][0-9]* accountIdPresent=no$" || true)
|
|
641
|
+
if [[ "$SUBST_NO_COUNT" -eq 1 ]]; then
|
|
642
|
+
pass "case-11a empty ACCOUNT_ID → substitution log carries accountIdPresent=no"
|
|
643
|
+
else
|
|
644
|
+
fail "case-11a expected 1 substitution line w/ accountIdPresent=no, got $SUBST_NO_COUNT (lines: $(ingest_lines))"
|
|
645
|
+
fi
|
|
646
|
+
|
|
647
|
+
# 11b. recorder spawn still fires — the hook does NOT loud-fail on absent
|
|
648
|
+
# accountId. The filled body still substitutes; the placeholder
|
|
649
|
+
# collapses to empty backticks.
|
|
650
|
+
NO_ACCT_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
|
|
651
|
+
NO_ACCT_OK=$(printf '%s' "$NO_ACCT_BODY" | python3 -c '
|
|
652
|
+
import sys, json
|
|
653
|
+
try:
|
|
654
|
+
outer = json.load(sys.stdin)
|
|
655
|
+
msg = outer.get("initialMessage")
|
|
656
|
+
if not isinstance(msg, str):
|
|
657
|
+
print("no:initialMessage-not-string")
|
|
658
|
+
sys.exit(0)
|
|
659
|
+
conds = []
|
|
660
|
+
conds.append(("starts-template", msg.startswith("You are an expert Neo4J graph operator.")))
|
|
661
|
+
conds.append(("no-placeholder-accountid", "{accountId}" not in msg))
|
|
662
|
+
# Placeholder collapses to empty backticks.
|
|
663
|
+
conds.append(("empty-accountid-rendered", "The current accountId is ``." in msg))
|
|
664
|
+
conds.append(("clarifier-still-present",
|
|
665
|
+
"never refuse a write because accountId" in msg))
|
|
666
|
+
# Transcript still rendered.
|
|
667
|
+
conds.append(("has-user-line", "\nuser: " in msg))
|
|
668
|
+
failed = [name for name, ok in conds if not ok]
|
|
669
|
+
print("yes" if not failed else "no:"+repr(failed))
|
|
670
|
+
except Exception as e:
|
|
671
|
+
print("parse-fail:"+str(e))
|
|
672
|
+
' 2>/dev/null)
|
|
673
|
+
if [[ "$NO_ACCT_OK" == "yes" ]]; then
|
|
674
|
+
pass "case-11b empty ACCOUNT_ID → filled body still substitutes; {accountId} → empty backticks; clarifier sentence intact"
|
|
675
|
+
else
|
|
676
|
+
fail "case-11b filled-body shape wrong on empty accountId ($NO_ACCT_OK): $NO_ACCT_BODY"
|
|
677
|
+
fi
|
|
678
|
+
|
|
608
679
|
# --- Summary ------------------------------------------------------------
|
|
609
680
|
echo "---"
|
|
610
681
|
echo "PASSED: $PASS FAILED: $FAIL"
|
|
@@ -366,6 +366,7 @@ if vertical_path:
|
|
|
366
366
|
|
|
367
367
|
with open(envelope_path, "r", encoding="utf-8") as f:
|
|
368
368
|
envelope = json.load(f)
|
|
369
|
+
account_id = envelope.get("accountId", "") or ""
|
|
369
370
|
turns = envelope.get("turns") or []
|
|
370
371
|
lines = []
|
|
371
372
|
for t in turns:
|
|
@@ -391,11 +392,17 @@ for t in turns:
|
|
|
391
392
|
# the last one from the closing `, using the tools at your disposal.`
|
|
392
393
|
conversation_text = "\n" + "\n".join(lines) + "\n"
|
|
393
394
|
|
|
394
|
-
filled =
|
|
395
|
+
filled = (
|
|
396
|
+
body_template
|
|
397
|
+
.replace("{schema}", schema_text)
|
|
398
|
+
.replace("{accountId}", account_id)
|
|
399
|
+
.replace("{conversation}", conversation_text)
|
|
400
|
+
)
|
|
395
401
|
|
|
396
402
|
schema_bytes = len(schema_text.encode("utf-8"))
|
|
397
403
|
conv_bytes = len(conversation_text.encode("utf-8"))
|
|
398
404
|
body_bytes = len(filled.encode("utf-8"))
|
|
405
|
+
account_id_present = "yes" if account_id else "no"
|
|
399
406
|
|
|
400
407
|
spawn_body = {
|
|
401
408
|
"adminSessionId": sid,
|
|
@@ -405,7 +412,7 @@ spawn_body = {
|
|
|
405
412
|
"initialMessage": filled,
|
|
406
413
|
}
|
|
407
414
|
|
|
408
|
-
print(f"{schema_bytes}\t{conv_bytes}\t{body_bytes}")
|
|
415
|
+
print(f"{schema_bytes}\t{conv_bytes}\t{body_bytes}\t{account_id_present}")
|
|
409
416
|
print(json.dumps(spawn_body))
|
|
410
417
|
PY
|
|
411
418
|
)
|
|
@@ -413,9 +420,9 @@ rm -f "$ENVELOPE_FILE"
|
|
|
413
420
|
|
|
414
421
|
BYTES_LINE=$(printf '%s' "$COMBINED" | head -n1)
|
|
415
422
|
SPAWN_BODY=$(printf '%s' "$COMBINED" | tail -n +2)
|
|
416
|
-
IFS=$'\t' read -r SCHEMA_BYTES CONV_BYTES BODY_BYTES <<<"$BYTES_LINE"
|
|
423
|
+
IFS=$'\t' read -r SCHEMA_BYTES CONV_BYTES BODY_BYTES ACCOUNT_ID_PRESENT <<<"$BYTES_LINE"
|
|
417
424
|
|
|
418
|
-
emit_log "substitution sessionId=${ADMIN_SESSION_ID} schemaBytes=${SCHEMA_BYTES} conversationBytes=${CONV_BYTES} bodyBytes=${BODY_BYTES}"
|
|
425
|
+
emit_log "substitution sessionId=${ADMIN_SESSION_ID} schemaBytes=${SCHEMA_BYTES} conversationBytes=${CONV_BYTES} bodyBytes=${BODY_BYTES} accountIdPresent=${ACCOUNT_ID_PRESENT}"
|
|
419
426
|
emit_log "spawn-request sessionId=${ADMIN_SESSION_ID} specialist=database-operator initialMessageBytes=${BODY_BYTES}"
|
|
420
427
|
|
|
421
428
|
SPAWN_RES_FILE=$(mktemp)
|
|
@@ -7,6 +7,11 @@ tools: mcp__memory__memory-write, mcp__memory__memory-update, mcp__memory__memor
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
You are an expert Neo4J graph operator. Here is the schema {schema}.
|
|
10
|
+
|
|
11
|
+
The `accountId` property is automatically supplied by the writers from
|
|
12
|
+
server-side environment state — never refuse a write because accountId
|
|
13
|
+
is absent from this prompt. The current accountId is `{accountId}`.
|
|
14
|
+
|
|
10
15
|
Use your expert judgement to update the graph in reaction to the
|
|
11
16
|
following conversation {conversation}, using the tools at your disposal.
|
|
12
17
|
You are not user-facing and your text goes nowhere — do not emit it.
|
package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md
CHANGED
|
@@ -151,6 +151,8 @@ The recurring failure pattern is trusting a derived summary (a WebFetch markdown
|
|
|
151
151
|
|
|
152
152
|
When any field above is unresolved after consulting its primary source, **stop before substitution** and present the operator with one consolidated prompt listing every unknown by name, the primary source already consulted, and the candidate value (if any). Operator answers in one pass; substitution then proceeds from confirmed values. **Never render a brochure with fields silently coerced to `TBC`, an approximation, or a default. A `TBC` shipped to a buyer must have been confirmed as a `TBC` by the operator, not chosen by the agent.**
|
|
153
153
|
|
|
154
|
+
**EPC is in a stricter tier — `TBC` is not a confirmable outcome.** UK law requires an EPC for any marketed property, so the brochure cannot ship with `epc_rating: "TBC"` even with operator confirmation. If `property-extract` returned `TBC` (the listing displays it that way, or the page lacks the data), the operator confirmation prompt for the EPC slot must demand an actual A–G band plus the current/potential scores and a path to the certificate PDF/image. The operator's path of last resort is the [EPC Register](https://www.epcregister.com/) — look up by address or postcode, download the certificate, and supply the band + scores back to the brochure step. Confirming `TBC` for `epc_rating` is the one answer the prompt must refuse to accept; the brochure halts until a real rating arrives.
|
|
155
|
+
|
|
154
156
|
## Hard rules (load-bearing — do not violate)
|
|
155
157
|
|
|
156
158
|
- **Never `Read` an image whose longest edge exceeds 2000px** — it can drop the session. Measure with `sips -g pixelWidth -g pixelHeight <file>` first.
|
|
@@ -170,6 +172,8 @@ When any field above is unresolved after consulting its primary source, **stop b
|
|
|
170
172
|
- **Floorplan ships as PNG on disk.** The brochure templates reference `images/<slug>-floorplan.png` and the contract is PNG-only on disk regardless of source format. If the operator-supplied source is JPG, convert at build time (`magick <src>.jpg <slug>-floorplan.png`) before substitution. JPEG compression artefacts on thin lines and small text are unacceptable for floorplans.
|
|
171
173
|
- **Brand-token override block must carry both vocabularies and target both templates.** The brochure (`template.html`) and the landing page (`index.html`) declare independent `:root` token vocabularies (`--paper-25 / --gold-700 / …` vs `--paper / --gold / …`). The brand-tokens block must emit **both** sets of token names with the same brand values and substitute at the `BRAND_TOKENS_OVERRIDE_SLOT` sentinel in **both** templates. A block that only carries the brochure's vocabulary leaves the landing page rendering Premium silently — the substitution gate passes and the snapshot capture proceeds. See `references/build.md → Brand-token override emission` for the mapping table and example block. Long-term vocabulary unification is a follow-up; until it lands the dual emission is structurally required.
|
|
172
174
|
- **Doc-comment strip pass runs against BOTH templates.** After substitution, the `<!-- REPLACE … -->` authoring notes must be removed from both `brochure.html` AND `index.html`. A strip pass that covers only the brochure leaves landing-page authoring notes rendering as visible body text whenever a comment delimiter is misplaced. The companion source-side check in `references/build.md → Template authoring hygiene + CI lint` catches the same defect class at template-edit time.
|
|
175
|
+
- **EPC is mandatory — `TBC` is not shippable.** UK law requires an EPC for any marketed property. The brochure must not render with `epc_rating: "TBC"`, no matter what `property-extract` returned. If the rating is missing or the listing displayed `TBC`, the operator confirmation prompt must demand an actual A–G band (plus current/potential scores and the certificate path) before substitution proceeds. The operator's authoritative fallback is the [EPC Register](https://www.epcregister.com/) — look up by address or postcode, download the certificate, supply the values. `TBC` is the one answer the operator cannot confirm for this field.
|
|
176
|
+
- **Web bundle ships the PDF, not per-page JPGs.** The bundle does not include `cover-print.jpg … backpage-print.jpg` — those duplicate the brochure content already embedded in `<slug>-brochure.pdf`. The web copy of `brochure.html` has its `.print-img` src attributes cleared so the print stylesheet drops to the live-DOM fallback; operators who want a printable PDF download the bundled PDF directly. See `references/build.md → Clear .print-img src in the web copy`. Shipping both was the historical bundle-bloat defect.
|
|
173
177
|
- **No `{{ token }}` may remain in rendered output** — `grep '{{' output/brochure.html` must return zero matches before PDFs are built.
|
|
174
178
|
|
|
175
179
|
## Scope
|
|
@@ -170,9 +170,8 @@ The web bundle is a parallel directory with the same on-disk shape as `output/`
|
|
|
170
170
|
|
|
171
171
|
```
|
|
172
172
|
output/web/
|
|
173
|
-
brochure.html # identical content;
|
|
173
|
+
brochure.html # identical content; .print-img src attributes cleared (see below)
|
|
174
174
|
index.html # companion landing page (mandatory in bundle)
|
|
175
|
-
cover-print.jpg, page2-print.jpg … backpage-print.jpg # 96 dpi JPEG snapshots (q=88)
|
|
176
175
|
<slug>-brochure.pdf # identical bytes to <slug>-brochure-web.pdf at the property level — simpler name inside the bundle since there's only one PDF here
|
|
177
176
|
images/
|
|
178
177
|
<slug>-NN.webp # web-tier per-slot encodings (see table below)
|
|
@@ -180,6 +179,8 @@ output/web/
|
|
|
180
179
|
<brand>-logo-light.png
|
|
181
180
|
```
|
|
182
181
|
|
|
182
|
+
The bundle deliberately does **not** include the per-page JPG snapshots (`cover-print.jpg` etc). They duplicate the brochure content already embedded in `<slug>-brochure.pdf` at smaller cost-to-quality. Operators who want printed output download the PDF from the bundle; operators who view `brochure.html` in a browser and hit Cmd+P get the live-DOM print path (see `Print snapshot capture → Live-DOM fallback`). Shipping both was the historical defect that pushed a 16-page folio bundle past 50 MB.
|
|
183
|
+
|
|
183
184
|
### Web-tier image encoding
|
|
184
185
|
|
|
185
186
|
The same per-slot tier idea as the digital floor in `images.md`, but with smaller widths and slightly lower quality — sized for screen viewing at 100% zoom, not for print sharpness.
|
|
@@ -196,34 +197,27 @@ A 27-image folio typically lands at **~2.5 MB** of web photographs, with the flo
|
|
|
196
197
|
|
|
197
198
|
**Floor-plan rule, repeated for emphasis.** Line-art PNG floor plans are **never re-encoded** for the web bundle — copy the source file as-is. Browsers render PNG line art crisply at any zoom level; converting to WebP introduces visible artefacts on text and rules even at q90+. The bundle gains a few hundred KB but the trade-off is on the right side.
|
|
198
199
|
|
|
199
|
-
###
|
|
200
|
-
|
|
201
|
-
The canonical snapshots in `output/` are 300 dpi PNG (~4–7 MB each, ~65 MB total) — far too heavy for the web bundle's `.print-img` swap layer. For the web bundle, derive 96 dpi JPEG copies directly from the canonical 300 dpi PNGs (no PDF round-trip):
|
|
202
|
-
|
|
203
|
-
```bash
|
|
204
|
-
cd output/web
|
|
205
|
-
for png in ../*-print.png; do
|
|
206
|
-
base=$(basename "$png" .png)
|
|
207
|
-
magick "$png" -resize 1123x794^ -quality 88 -strip -interlace none -colorspace sRGB "$base.jpg"
|
|
208
|
-
done
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
`-resize 1123x794^` downsamples to A4-landscape at 96 dpi (use `794x1123^` for portrait). `-strip` removes embedded metadata (EXIF, ICC) so the bundle stays minimal. `-interlace none` produces baseline JPEG which decodes faster in mobile previewers than progressive.
|
|
200
|
+
### Clear `.print-img` src in the web copy
|
|
212
201
|
|
|
213
|
-
|
|
202
|
+
The canonical `output/brochure.html` carries `<img class="print-img" src="cover-print.png">` references that power the snapshot path in the print stylesheet (Cmd+P → embedded PNG fills the page). The web bundle does **not** include those PNGs — and does not derive smaller JPEG copies either, because the PDF embedded in the bundle already covers the "I want a printable artefact" need. Instead, the web copy of `brochure.html` has its `.print-img` src attributes cleared so the print stylesheet drops to the live-DOM fallback path:
|
|
214
203
|
|
|
215
204
|
```python
|
|
216
|
-
# Edit the web copy of brochure.html only — leave the canonical output/brochure.html unchanged
|
|
205
|
+
# Edit the web copy of brochure.html only — leave the canonical output/brochure.html unchanged.
|
|
217
206
|
import re
|
|
218
207
|
with open("output/web/brochure.html") as f:
|
|
219
208
|
s = f.read()
|
|
220
|
-
|
|
221
|
-
|
|
209
|
+
# Clear every .print-img src — the print stylesheet's :has(.print-img[src]:not([src=""]))
|
|
210
|
+
# selector then fails, and the @media print live-DOM path renders the page directly.
|
|
211
|
+
s = re.sub(
|
|
212
|
+
r'(<img\s+class="print-img"[^>]*?\s+)src="[^"]*"',
|
|
213
|
+
r'\1src=""',
|
|
214
|
+
s,
|
|
215
|
+
)
|
|
222
216
|
with open("output/web/brochure.html", "w") as f:
|
|
223
217
|
f.write(s)
|
|
224
218
|
```
|
|
225
219
|
|
|
226
|
-
|
|
220
|
+
The `.print-img` element on screen is `opacity: 0; width: 1px; height: 1px` regardless of `src`, so clearing the attribute has zero visible effect — only the print stylesheet branches differently. Operators who want a printable PDF download `<slug>-brochure.pdf` directly from the bundle.
|
|
227
221
|
|
|
228
222
|
### Copy the web PDF into the bundle
|
|
229
223
|
|
|
@@ -280,7 +274,7 @@ Serve the unzipped bundle from an isolated temp directory and verify every refer
|
|
|
280
274
|
TMP=/tmp/web-test && rm -rf $TMP && mkdir -p $TMP
|
|
281
275
|
cd $TMP && unzip -q /path/to/<slug>-web.zip
|
|
282
276
|
python3 -m http.server 8765 &
|
|
283
|
-
for f in brochure.html index.html
|
|
277
|
+
for f in brochure.html index.html images/<slug>-01.webp images/<brand>-logo-light.png <slug>-brochure.pdf; do
|
|
284
278
|
echo "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8765/$f) $f"
|
|
285
279
|
done
|
|
286
280
|
```
|
|
@@ -291,13 +285,12 @@ The brochure should render the same as the canonical preview, just lighter on th
|
|
|
291
285
|
|
|
292
286
|
| File | `output/` (canonical archive) | `output/web/` (web bundle) |
|
|
293
287
|
|---|---|---|
|
|
294
|
-
| `brochure.html` | full-resolution images, `.png` snapshot refs | identical structure, `.
|
|
288
|
+
| `brochure.html` | full-resolution images, `.png` snapshot refs | identical structure, `.print-img` srcs cleared (live-DOM print fallback) |
|
|
295
289
|
| `index.html` | — | ✓ companion landing page; see `index-landing.md` |
|
|
296
290
|
| `<slug>-brochure-print.pdf` | ✓ canonical print master (50–80 MB, 300 dpi) | — (too large to bundle) |
|
|
297
|
-
| `<slug>-brochure-web.pdf` | ✓ web/digital deliverable (
|
|
291
|
+
| `<slug>-brochure-web.pdf` | ✓ web/digital deliverable (7–15 MB, 192 dpi JPEG-embedded) | — copied into bundle as `<slug>-brochure.pdf` |
|
|
298
292
|
| `<slug>-brochure.pdf` | — | ✓ identical bytes to `-web.pdf`; matches index.html / brochure.html link |
|
|
299
|
-
| `cover-print.png … backpage-print.png` | ✓ 300 dpi PNG (~4–7 MB each) — canonical snapshots | —
|
|
300
|
-
| `cover-print.jpg … backpage-print.jpg` | — | ✓ 96 dpi JPEG (~150 KB each, derived from the 300 dpi PNGs) |
|
|
293
|
+
| `cover-print.png … backpage-print.png` | ✓ 300 dpi PNG (~4–7 MB each) — canonical snapshots | — bundle ships the PDF instead of per-page JPGs |
|
|
301
294
|
| `images/<slug>-NN.webp` | full-quality per Render-slot table | web-tier per the web table above |
|
|
302
295
|
| `images/qr-*.png`, `images/<brand>-logo-*.png` | ✓ | ✓ (copied unchanged — already small) |
|
|
303
296
|
| `.snapshots-web/*-print.png` | ✓ intermediate (192 dpi PNGs used by the web-PDF build; deletable after) | — |
|