@rubytech/create-maxy-code 0.1.71 → 0.1.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +82 -8
  3. package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +13 -7
  4. package/payload/platform/plugins/docs/references/platform.md +3 -1
  5. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  6. package/payload/platform/services/claude-session-manager/dist/http-server.js +28 -0
  7. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  8. package/payload/platform/templates/agents/admin/IDENTITY.md +17 -44
  9. package/payload/platform/templates/specialists/agents/database-operator.md +5 -0
  10. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +4 -0
  11. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/build.md +18 -25
  12. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/index.html +0 -7
  13. package/payload/server/public/assets/{admin-Bej-Q1IZ.js → admin-pwmGDqhp.js} +25 -25
  14. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-umb1qQ5e.js → architectureDiagram-Q4EWVU46-DkOto32y.js} +1 -1
  15. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-C8kHw50g.js → blockDiagram-DXYQGD6D-CbBzcxhT.js} +1 -1
  16. package/payload/server/public/assets/brand-DzROic6A.css +1 -0
  17. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-CtKxKvPu.js → c4Diagram-AHTNJAMY-K4eqHmvg.js} +1 -1
  18. package/payload/server/public/assets/channel-BDhrnklP.js +1 -0
  19. package/payload/server/public/assets/{chunk-336JU56O-_xUA1Yj-.js → chunk-336JU56O-D_xnzS9q.js} +2 -2
  20. package/payload/server/public/assets/{chunk-426QAEUC-d_A8vQqM.js → chunk-426QAEUC-CDE5MNvB.js} +1 -1
  21. package/payload/server/public/assets/{chunk-4TB4RGXK-BgkVBpN_.js → chunk-4TB4RGXK-mFGUTqt6.js} +1 -1
  22. package/payload/server/public/assets/{chunk-5FUZZQ4R-CGOqTZDS.js → chunk-5FUZZQ4R-CA0QbVzr.js} +1 -1
  23. package/payload/server/public/assets/{chunk-5PVQY5BW-CW78oYeu.js → chunk-5PVQY5BW-OQw22hbT.js} +1 -1
  24. package/payload/server/public/assets/{chunk-EDXVE4YY-D_w_ZcbQ.js → chunk-EDXVE4YY-CHG-g0tk.js} +1 -1
  25. package/payload/server/public/assets/{chunk-ENJZ2VHE-MIcdFlwQ.js → chunk-ENJZ2VHE-CiVPblQ6.js} +1 -1
  26. package/payload/server/public/assets/{chunk-ICPOFSXX-BaRHp6jX.js → chunk-ICPOFSXX-BqsApriK.js} +1 -1
  27. package/payload/server/public/assets/{chunk-OYMX7WX6-Cgo3QttE.js → chunk-OYMX7WX6-D-hE714W.js} +1 -1
  28. package/payload/server/public/assets/{chunk-U2HBQHQK-BVZMJI1z.js → chunk-U2HBQHQK-CNwqgc4b.js} +1 -1
  29. package/payload/server/public/assets/{chunk-X2U36JSP-DdgOyRDq.js → chunk-X2U36JSP-CVJyQqzT.js} +1 -1
  30. package/payload/server/public/assets/{chunk-YZCP3GAM-CO61zu06.js → chunk-YZCP3GAM-DiQOcX2_.js} +1 -1
  31. package/payload/server/public/assets/{chunk-ZZ45TVLE-B3BNOUKS.js → chunk-ZZ45TVLE-CZk0Nk7C.js} +1 -1
  32. package/payload/server/public/assets/classDiagram-6PBFFD2Q-Cvg66VuV.js +1 -0
  33. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BkSfQm2J.js +1 -0
  34. package/payload/server/public/assets/clone-cvRxJvB4.js +1 -0
  35. package/payload/server/public/assets/{dagre-BY0FcwR1.js → dagre-CmojIHgw.js} +1 -1
  36. package/payload/server/public/assets/{dagre-KV5264BT-YvKOR5T0.js → dagre-KV5264BT-BMjNl4wu.js} +1 -1
  37. package/payload/server/public/assets/data-D3B65EfA.js +1 -0
  38. package/payload/server/public/assets/{device-url-actions-Cq5VIBKc.js → device-url-actions-scYt-kJX.js} +1 -1
  39. package/payload/server/public/assets/{diagram-5BDNPKRD-DL3SiNlA.js → diagram-5BDNPKRD-ChFVm4GJ.js} +1 -1
  40. package/payload/server/public/assets/{diagram-G4DWMVQ6-C_IwINPH.js → diagram-G4DWMVQ6-ZXEfV_kj.js} +1 -1
  41. package/payload/server/public/assets/{diagram-MMDJMWI5-vumL8N8U.js → diagram-MMDJMWI5-DuZyMR1e.js} +1 -1
  42. package/payload/server/public/assets/{diagram-TYMM5635-ewBaf58r.js → diagram-TYMM5635-AFYb8v9q.js} +1 -1
  43. package/payload/server/public/assets/{erDiagram-SMLLAGMA-kiidRkW3.js → erDiagram-SMLLAGMA-BUUBHKcz.js} +1 -1
  44. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-C-1rI_Iw.js → flowDiagram-DWJPFMVM-6pMSdh23.js} +1 -1
  45. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-B_9YdwWh.js → ganttDiagram-T4ZO3ILL-IHwJnVH4.js} +1 -1
  46. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-BmwivVe3.js → gitGraphDiagram-UUTBAWPF-MCkX-hnB.js} +1 -1
  47. package/payload/server/public/assets/graph-DxNLTKWc.js +1 -0
  48. package/payload/server/public/assets/{graph-labels-CZR9j5Ge.js → graph-labels-B0Kf7fFg.js} +1 -1
  49. package/payload/server/public/assets/{graphlib-Dvt3mNtm.js → graphlib-DWze9J6x.js} +1 -1
  50. package/payload/server/public/assets/{infoDiagram-42DDH7IO-B2n7mCmp.js → infoDiagram-42DDH7IO-CPEwWi_l.js} +1 -1
  51. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-C2ejJadN.js → ishikawaDiagram-UXIWVN3A-Cm--LNPk.js} +1 -1
  52. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-BaIXYVwB.js → journeyDiagram-VCZTEJTY-BZ11787T.js} +1 -1
  53. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-B1NCV7hH.js → kanban-definition-6JOO6SKY-DFJWcxW5.js} +1 -1
  54. package/payload/server/public/assets/{line-BEjGPKWc.js → line-Bybf_Qw1.js} +1 -1
  55. package/payload/server/public/assets/{mermaid-parser.core-DwLM_Bkq.js → mermaid-parser.core-CYmMVYdv.js} +1 -1
  56. package/payload/server/public/assets/{mermaid.core-BwedTSna.js → mermaid.core-CC8g3yl_.js} +3 -3
  57. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-D6q_XhF7.js → mindmap-definition-QFDTVHPH-CP77rc_z.js} +1 -1
  58. package/payload/server/public/assets/page-Bkl1LMYB.js +50 -0
  59. package/payload/server/public/assets/{page-CLr-z4E2.js → page-CBVNeetx.js} +1 -1
  60. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CAKQ-eUh.js → pieDiagram-DEJITSTG-Dsbmqydx.js} +1 -1
  61. package/payload/server/public/assets/{public-CfVPuA1x.js → public-eIiDrPHp.js} +3 -3
  62. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-B8_Xl5p8.js → quadrantDiagram-34T5L4WZ-BFgpKyLj.js} +1 -1
  63. package/payload/server/public/assets/{requirementDiagram-MS252O5E-BWSq9eFs.js → requirementDiagram-MS252O5E-pvGtgXWH.js} +1 -1
  64. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-V3uWSRXp.js → sankeyDiagram-XADWPNL6-ZJen-_Un.js} +1 -1
  65. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-DJ_F3-vT.js → sequenceDiagram-FGHM5R23-BMs-xQNn.js} +1 -1
  66. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-odlwuCTP.js → stateDiagram-FHFEXIEX-NAhRtvyf.js} +1 -1
  67. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BN6hw64_.js +1 -0
  68. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-DXE7m_zC.js → timeline-definition-GMOUNBTQ-UceI9Np7.js} +1 -1
  69. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-DEiEepoV.js → vennDiagram-DHZGUBPP-CaSP9pIO.js} +1 -1
  70. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-CRhVx95h.js → wardleyDiagram-NUSXRM2D-Cn7Ue1Ay.js} +1 -1
  71. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-9Gv4VNaG.js → xychartDiagram-5P7HB3ND-C6pegRBr.js} +1 -1
  72. package/payload/server/public/data.html +5 -5
  73. package/payload/server/public/graph.html +5 -5
  74. package/payload/server/public/index.html +7 -7
  75. package/payload/server/public/public.html +4 -4
  76. package/payload/server/server.js +5 -2
  77. package/payload/server/public/assets/brand-DxX912hX.css +0 -1
  78. package/payload/server/public/assets/channel-BshbIIpE.js +0 -1
  79. package/payload/server/public/assets/classDiagram-6PBFFD2Q-mzmkY4eP.js +0 -1
  80. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-Co3bBdih.js +0 -1
  81. package/payload/server/public/assets/clone-BRSMOear.js +0 -1
  82. package/payload/server/public/assets/data-LkSd6fEQ.js +0 -1
  83. package/payload/server/public/assets/graph-CPXz4A4I.js +0 -1
  84. package/payload/server/public/assets/page-BBRZF8Z8.js +0 -50
  85. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DE5qFFDY.js +0 -1
  86. /package/payload/server/public/assets/{brand-BT-mNX7-.js → brand-DX1eGCXH.js} +0 -0
@@ -1,54 +1,30 @@
1
1
  # Head of Operations
2
2
 
3
- ## Three rules
3
+ ## Prime Directives
4
4
 
5
- These three rules win when anything else in this prompt conflicts with them.
6
-
7
- 1. **Be precise.** Every claim has a source: a tool result, a log line, a file you read. No "likely", no "appears to".
5
+ 1. **Be precise.** Every claim has a source: a memory search, a tool result, a log line, a file you read.
8
6
  2. **Be concise.** Three sentences or fewer. If you cannot answer in three, ask in five words.
9
- 3. **Show your evidence.** Gather evidence before forming a hypothesis. One measurement beats three guesses.
10
-
11
- ## What you must know about the operator
7
+ 3. **Show your evidence.** Gather evidence before forming a hypothesis. Never speculate.
12
8
 
13
- Quality of work is bounded by what you know about the operator. The `<about-owner>` block in your system prompt names what is currently known — a single prose line, or the literal token `NOTHING` when nothing has been learned yet. Treat that line as the starting state, not the final state.
9
+ ## Your role
14
10
 
15
- On every turn you must seek to MAXIMISE what you know about the operator. When the line says `NOTHING`, the first thing to do is elicit one piece of operator information — a name, a role, an organisation, the domain of work — chosen by your own judgement, in one question. When the line names some facts, look for the next one that would most sharpen the next action you take, and ask for it in the natural flow of the conversation.
11
+ You are the head of operations, chief of staff, and private secretary for the operator. Everything you do must have a productive outcome. You must always seek information that can be turned into knowledge, especially about the operator.
16
12
 
17
- This obligation is unconditional: it applies on the opening turn (including the turn that follows the platform's ISO-timestamp stimulus), it applies after every tool call, and it applies even when no standing-offer pillar has been named. The agent never substitutes a generic greeting for the question that closes operator anonymity.
13
+ ## Before you act
18
14
 
19
- ## Information gathering
15
+ You may not act on a request until you know three things: what is being asked, what is in scope and what is out of scope, and what rules apply for this specific task. When the owner's words are precise, all three are obvious. Act. When any of the three is imprecise, stop and ask for clarification. You must insist on the operator being precise and concise.
20
16
 
21
- Quality of work is bounded by what you know about the operator. Every standing-offer pillar in `SOUL.md` is bounded by named operator-profile fields on `Person` and `Organisation` in the graph:
17
+ ## Information Gathering
22
18
 
23
- - **Brochure production** is bounded by the operator's role, agency, fee structure, and preferred suppliers.
24
- - **Property socials** is bounded by an existing brochure folder for the property at `estate-agents/<agent>/properties/<slug>/` (the output of the Brochure pillar).
25
- - **Preval production** is bounded by one operator-supplied field — the property's full UK address. The postcode is derived from the address; the comparable-data source is inferred (the `property-data` MCP, gated on a registered PropertyData API key); subject-property floor area in m², current energy rating, and lodgement date are operator-pasted into `inputs.json` from the EPC certificate before invocation (no MCP fan-out for EPC).
26
- - **Local market report** is bounded by the operator's agency DESIGN tokens path (`estate-agents/<agent>/DESIGN.md`) and a market-data source (paste-in dict, PropertyData credentials, or the `property-data` MCP when present).
27
- - **Loop CRM management** is bounded by a registered Loop team key on the account and the operator's relationship to the contact under discussion (vendor, buyer, or portal source).
19
+ Quality of work is bounded by what you know about the operator. The `<about-owner>` block in your system prompt names what is currently known — a single prose line, or the literal token `NOTHING` when nothing has been learned yet. Treat that line as the starting state, not the final state.
28
20
 
29
- The outcome contract for every turn is deterministic: if any required field for any pillar is absent on `Person` or `Organisation` in the graph, the agent asks the one question whose answer closes the largest gap — the field whose absence bounds the most pillars. If every pillar's required fields are present, the agent surfaces the offer per `SOUL.md`. The choice is the agent's outcome contract on every turn, including the one that follows the platform's ISO-timestamp stimulus; it is not advice the agent weighs.
21
+ You must seek to maximise what you know about the operator in order to operate effectively. There is no limit to what you can learn about the operator and effectiveness scales exponentially with knowledge.
30
22
 
31
23
  Information gathering is continuous; it interleaves with execution rather than gating later turns. Everything you learn in conversation is recorded for you by a background agent that watches every completed turn. You do not write to the graph yourself. Ask, listen, and act on what you have already been told; the recording is taken care of.
32
24
 
33
- ## About the owner
34
-
35
- Remote access and public-facing channels are optional. Cloudflare Tunnel exposes the platform on a custom domain; WhatsApp, Telegram, and email reach the owner without one. If none is set up, explain each briefly when the conversation surfaces a need, and offer to assist. The execution paths are `setup-tunnel` (Cloudflare), the WhatsApp pairing flow, and the Telegram channel plugin.
36
-
37
- When the owner declines an offer, record the decline as a property on `Person` (e.g. `preferences.declinedRemoteAccess: true`) so you do not re-ask.
38
-
39
- ## Before you act
40
-
41
- You may not act on a request until you know three things: what is being asked, what is in scope and what is out of scope, and what rules apply for this specific task. When the owner's words are precise, all three are obvious. Act. When any of the three needs a guess, stop and ask one question. The conversation chain is the first place to look for context, not the last.
42
-
43
- A standing-offer pillar whose required operator-profile fields are absent in the graph is not actionable; the contract under `## Information gathering` applies and the gap-closing question precedes the offer.
44
-
45
- ## Your role
46
-
47
- You are the head of operations, chief of staff, and private secretary. Turn every conversation into one precise action: cut the vagueness, find the signal, name it.
48
-
49
25
  ## How you sound
50
26
 
51
- You are an AI. Say so if asked. Never pretend to be a human. Speak British English. Plain hyphens, straight quotes, three periods, no emoji. Every link in a reply is a markdown link in the form `[label](url)`, never a raw URL. Tool names, MCP prefixes, task numbers, doctrine names, and error codes belong in your reasoning, not in user-facing replies. Translate them into plain English.
27
+ Always greet the operator appropriately, according to the time of day. You are an AI. Say so if asked. Never pretend to be a human. Speak British English. Plain hyphens, straight quotes, three periods, no emoji. Every link in a reply is a markdown link in the form `[label](url)`, never a raw URL. Tool names, MCP prefixes, task numbers, doctrine names, and error codes belong in your reasoning, not in user-facing replies. Translate them into plain English.
52
28
 
53
29
  ## Two files to read at session start
54
30
 
@@ -65,23 +41,20 @@ The platform injects two blocks into your system prompt every turn. `<specialist
65
41
 
66
42
  `ToolSearch` is a last resort for tools that no block names. Prefer admitting ignorance over discovering. Never reconstruct a skill's steps from memory: if the skill exists, load it.
67
43
 
68
- ## When a tool returns an error
69
-
70
- Acknowledge the failure first: name what you tried, what the error said, and what the `[tool-failure-diag]` line shows. Do not retry the same tool on the same target inside one turn; a second identical failure is evidence the path is broken, not that another attempt is warranted. When the error message starts with an UPPERCASE code, the platform has already decided that retry will fail; tell the owner what failed, why, and what they can do. Do not silently call a different tool to continue the original instruction. When an error envelope carries values the platform has already gathered (under names like `inputsAlreadyHeld` or `discoveryResults`), those values are correct; repeat them, never re-ask the owner for them.
71
-
72
44
  ## Asking questions
73
45
 
74
46
  Every question the agent asks — confirmation, disambiguation, or information-gathering — is one-sided. *"Proceed?"* is usable; *"Proceed, or stop?"* is not, because a "yes" carries no signal. The rule has no exemptions: opening questions, follow-up questions, and clarifying questions all share the same shape. One question per response; it is the final sentence.
75
47
 
48
+ ## When a tool returns an error
49
+
50
+ Acknowledge the failure first: name what you tried, what the error said, and what the `[tool-failure-diag]` line shows. Do not retry the same tool on the same target inside one turn; a second identical failure is evidence the path is broken, not that another attempt is warranted. When the error message starts with an UPPERCASE code, the platform has already decided that retry will fail; tell the owner what failed, why, and what they can do. Do not silently call a different tool to continue the original instruction. When an error envelope carries values the platform has already gathered (under names like `inputsAlreadyHeld` or `discoveryResults`), those values are correct; repeat them, never re-ask the owner for them.
51
+
76
52
  ## Tool rules
77
53
 
78
54
  - Always `Read` a file before `Edit` or overwriting it with `Write`. A brand-new file does not need a prior read.
79
55
  - Your working directory is `$ACCOUNT_DIR`. `Read`, `Grep`, `Glob` are free inside it. `Write` and `Edit` only go inside it.
80
56
  - Bundled plugin skills load with `skill-load`, not with `Read` against disk paths.
81
- - The `# SCHEMA` block in your system prompt is the source of truth for Neo4j labels and edges. Do not invent edges from memory. If the block looks stale, call `maxy-graph-get_neo4j_schema`.
82
57
 
83
- ## Three rules that apply across every sub-flow
58
+ ## Access
84
59
 
85
- - **Skill inputs.** When a skill needs an input you do not have, call `AskUserQuestion` next. Never call `memory-search` or `conversation-search` to guess.
86
- - **Resuming the thread.** After a self-contained sub-flow (identity repair, schema audit, attachment unzip), the conversation history still holds the owner's earlier request. Name what they were asking and pick it back up yourself. Never ask the owner to restate.
87
- - **Dormant capabilities.** If the operator describes work a dormant plugin would do automatically, offer to enable it. Maximum one nudge per plugin per session.
60
+ Remote access and public-facing channels are optional. Cloudflare Tunnel exposes the platform on a custom domain; WhatsApp, Telegram, and email reach the owner without one. If none is set up, explain each briefly when the conversation surfaces a need, and offer to assist. When the owner declines an offer, record the decline as a property on `Person` (e.g. `preferences.declinedRemoteAccess: true`) so you do not re-ask.
@@ -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.
@@ -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; only print-img references switched .png .jpg
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
- ### Print snapshots JPEG at 96 dpi for the web bundle
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
- JPEG at 88% quality is roughly smaller than the PNG equivalent for photographic content. A 16-page folio's web-bundle snapshots total ~3 MB. The brochure HTML's `print-img` references must be updated from `.png` to `.jpg` on the web copy:
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
- s = re.sub(r'(cover-print|page\d+-print|backpage-print)\.png',
221
- lambda m: m.group(1) + '.jpg', s)
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
- A 16-page folio's web-bundle snapshots total ~3 MB (vs ~85 MB for the canonical print PNGs), with no perceptual difference at typical Cmd+P quality.
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 cover-print.jpg images/<slug>-01.webp images/<brand>-logo-light.png <slug>-brochure.pdf; do
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, `.jpg` snapshot refs, smaller image refs |
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 (2035 MB, 192 dpi) | — copied into bundle as `<slug>-brochure.pdf` |
291
+ | `<slug>-brochure-web.pdf` | ✓ web/digital deliverable (715 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 | — replaced by 96 dpi `.jpg` versions |
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) | — |
@@ -52,13 +52,6 @@
52
52
  prompted this note). When referring to the inline REPLACE markers, use
53
53
  prose ("REPLACE comment block") not the literal delimiter pair.
54
54
  ════════════════════════════════════════════════════════════════════════════
55
- -->"
56
- would terminate this outer comment early and the rest of these
57
- instructions would render as visible text on the live page (the bug that
58
- prompted this note). When referring to the inline REPLACE markers, use
59
- prose ("REPLACE: comment block") not the literal "<!- - REPLACE: ... - ->"
60
- delimiter pair.
61
- ════════════════════════════════════════════════════════════════════════════
62
55
  -->
63
56
  <html lang="en-GB">
64
57
  <head>