Version. 0.4
Date. 2026-07-16
Author. Claude.ai, on Operator direction.
Target. /Users/dunin7/loomworks-marketing. Cloudflare Pages project. New chain under functions/api/sales/; new Astro page. The existing Example Generator is not touched.
Technical consumer. Claude Code on DUNIN7-M4.
Status. Build specification for a disposable spike. Filed as CR-2026-152 — confirmed next-free against the record's highest existing numbered CR (CR-2026-151, change-requests/cr-2026-151-dismiss-not-recognized-v0_1.md) at filing time (2026-07-16).
Companion to. Example Generator CR v0.2 (D1–D8, the public generator), scoping note v0.3, sales-perspective mockup v0_2.
Change from v0.1. Section 8 (Access) only. The Operator settled the access decision v0.1 left open: the spike sits behind Cloudflare Access from the first deploy, reachable from the web, not local-only. Section 8 is replaced entirely — the Access application's scope (page + API routes, no wider), the dashboard-only-configuration limitation, and the Operator-owns-policy-membership boundary are now specified. No other section changes. (v0.1 is kept byte-intact as the superseded prior.)
Change from v0.2. Section 4.2 only. The step-3 fabrication audit (2026-07-16, perishables input) disproved this section's completeness claim: the check panel was stated to be "complete by construction," and two rendered specifics with no corresponding finding — found in the systems-lead column on the same run that produced a clean owner column — show it is not. Section 4.2 is replaced entirely: the check panel is still filtered from source, still computed not generated, but the completeness claim is retracted and the panel's real scope (complete with respect to findings, not with respect to prose) is stated instead, with the audit's two quoted specifics as evidence. The prior, now-corrected claim is preserved below the correction rather than deleted. No other section changes. (v0.2 is kept byte-intact as the superseded prior.)
Change from v0.3. Section 10 only. Acceptance criterion 4 still carried the completeness claim that section 4.2 retired in v0.3; corrected. This was specified during the v0.3 amendment and did not land. No other section changes. (v0.3 is kept byte-intact as the superseded prior.)
The Example Generator asks four questions about a prospect's work and produces one example. This spike adds a fifth question — who is going to read this — and produces one artifact holding a version per reader, side by side.
It is not a variant of the existing generator. The existing chain writes finished prose in its first call, which means there is nothing left to shape afterwards. This spike replaces that with a chain that finds the material first, selects from it per reader second, and writes prose last. That is the only way the fifth question can do real work rather than re-tone the same document.
The spike is disposable. It exists to answer one question: does one analysis genuinely shape differently per reader, or does the model just re-word the same thing? If the answer is yes, the real build goes in the Operator Layer. If no, delete the spike.
analyze.js emits a five-section HTML fragment — finished prose, written for a generic reader. By the time it returns, the selection has already happened: it chose what to say and what to omit, for nobody in particular.
Hand that output to a shaping call and instruct it to address the owner, and the model has two available moves: re-word what survived, or invent material that was never there. Re-toning or fabricating. Neither is Shaping.
This was confirmed by inspection of both models' output on the same input (2026-07-16, perishables). Sonnet's analyze is good prose and has still discarded the structure. The material that makes the strongest case — an unmatched balance of stock, a negotiation trajectory — appears only in illustrate, downstream of where shaping would have to happen.
The lesson applied: a guarantee is enforced in code, not requested in a prompt. Asking a shaping prompt to "select rather than re-tone" cannot work when the upstream call has already thrown the selectable material away. The fix is architectural.
A parallel chain under /api/sales/, and one new page. Four new files:
| File | Route | Role |
|---|---|---|
| functions/api/sales/analyze.js | POST /api/sales/analyze | Four fields → structured findings. No prose. |
| functions/api/sales/shape.js | POST /api/sales/shape | Findings + one reader → a selection over finding IDs. No prose. |
| functions/api/sales/render.js | POST /api/sales/render | Selection + findings → prose for that reader. |
| src/pages/sales-generator.astro | /sales-generator/ | The page. |
functions/api/_lib.js is reused for the Anthropic caller, rate limiting, input caps, and sanitisation. It is not modified — the public generator depends on it. If the spike needs a helper _lib.js does not have, add it to a new functions/api/sales/_lib.js rather than editing the shared one.
The existing analyze.js, illustrate.js, foundation-draft.js, and example-generator.astro are not touched.
analyze — find the material, write for nobodyInput. The four existing fields (work, audience, done, constraints), same 2000-char caps.
Output. Structured JSON. Not HTML, not prose.
{
"findings": [
{
"id": "f1",
"claim": "The window between grade-out and shrink is measured in hours",
"room": "memory" | "manifestation" | "shaping" | "rendering" | "why",
"source": "input" | "inferred",
"weight": "central" | "supporting"
},
...
],
"groups": [
{ "label": "What Memory would hold", "finding_ids": ["f1","f4"] },
...
]
}
The source field is the point of this whole design. input means the finding traces to something the Operator typed. inferred means the model produced it — the Brix number, the assumption that they run a WMS, the claim that they work off spreadsheets today. Every finding is one or the other, and the model must classify each one.
Prompt posture. Find everything worth saying. Organize it. Address nobody. Do not write prose. Emit only the JSON object.
Validation. Reject if: not valid JSON, any finding missing a field, any finding_ids entry not matching a finding id, any source value outside the enum, zero findings. On failure, surface the plain recovery message (§6). Never render a malformed analysis.
shape — select, do not writeInput. The findings array and exactly one reader key.
Output. A selection over finding IDs. It cannot introduce text.
{
"reader": "owner",
"foregrounded": ["f3","f7"],
"included": ["f1","f4","f9"],
"cut": ["f2","f5","f6","f8"],
"rationale": "The owner is measured on margin. The dock-level sequence is not their decision."
}
The invariant, enforced in code: every ID in foregrounded, included, and cut must exist in the findings array, and the three lists together must partition it exactly — no ID in two lists, no finding unaccounted for. Reject otherwise.
This is what makes shaping structurally incapable of fabrication. The call selects from a fixed set. It has no field in which to invent.
One call per reader. N readers checked = N shape calls, issued in parallel. They are independent.
render — prose, from the selection onlyInput. One shape selection, plus the findings it references.
Output. HTML fragment for that reader's column. Sanitised via the existing sanitizeFragment before injection.
Prompt posture. Write for this reader using only the findings supplied. Lead with the foregrounded ones. Do not introduce facts that are not in the findings.
Note honestly: this last constraint is prompt-enforced, not code-enforced, and is therefore the weakest link in the chain. render is where new fabrication can still enter. This is a known limit of the spike, not a solved problem — see §7.
render runs in parallel per reader, following its shape call.
Both were hand-authored in the mockup. Neither should be.
Given two or more shape selections, the contrast is a set difference over finding IDs. Findings foregrounded for reader A and cut for reader B are exactly what changed between them.
No model call. The panel states which findings each reader got and which they did not, in plain terms. If it can be computed, it must not be generated — a generated contrast can lie about a difference that isn't there.
source, and incomplete
Every finding where source === "inferred" and that survived into at least one rendered column is a specific the draft asserts about the prospect's business that came from the model, not from the Operator.
That list is the check panel. Filter, do not generate.
The panel is complete with respect to findings. It is not complete with respect to prose. render can introduce specifics that correspond to no finding. Those have no id and no source field, so the filter cannot see them, and no way of building the panel changes that — it audits what analyze guessed, not what render added on top.
This is not theoretical. The step-3 audit (2026-07-16, perishables input, systems-lead reader) found two such specifics in a single rendered column, both asserting things about the prospect's existing tooling that appear in no finding:
Both are hedged and neither invents a number or a named system. Both are assertions about how the prospect works today, produced by a model that was told nothing about how they work today. The owner column, on the same analysis, introduced nothing.
Therefore the panel must not claim completeness on the surface. It says what it is: the specifics the analysis inferred. It does not say or imply that it lists everything unverified in the output. The Operator reads the prose regardless — the panel narrows that job, it does not replace it.
Prior position, corrected. CR v0.1 and v0.2 stated at this section that the panel was "complete by construction — nothing inferred can reach the Operator without appearing on the list." That claim was false. It was contradicted within the same document by section 7, which already admitted render can fabricate, and it was disproven in practice at step 3 before the panel was built.
Follows sales-perspective mockup v0_2. Points of substance:
Any validation failure at any stage surfaces one plain recovery message and renders nothing malformed. Name what happened, offer the way out, do not apologise, do not expose the stage that failed. Pattern per D8's recovery surface.
A shape or render failure for one reader must not discard the others. Render the columns that succeeded; name the one that did not.
render can still fabricate. The constraint is in the prompt. Code enforces selection integrity, not prose fidelity. Detecting a fact in the prose that isn't in the findings is a harder problem and is out of scope.source field. The model decides whether a finding came from input or inference. It may be wrong. This is prompt-enforced and therefore soft — but it is soft in one auditable place rather than diffused through the copy._lib.js behaviour, which fails open when RATE_LIMIT_KV is unbound. The binding lives only in the Cloudflare dashboard, not in the repo.Settled by Operator direction, 2026-07-16: the spike sits behind Cloudflare Access from the first deploy. It is reachable from the web, not local-only.
The spike is not public. It is not linked from navigation, the home page, or any sitemap, and carries a noindex robots meta tag — same posture as the Harvest Surge mockups.
The Access application covers both the page and its API routes. /sales-generator/ and /api/sales/ are gated together. Gating the page alone leaves the generation endpoints openly callable, which spends the Anthropic budget without the gate. The page's calls are same-origin and carry the Access session.
The Access application must not extend beyond those two paths. The public Example Generator at /example-generator/, its API routes at /api/analyze, /api/illustrate, /api/foundation-draft, the Harvest Surge mockups at /harvest-surge/, and the marketing site itself all stay open. CC verifies this explicitly before the spike is announced as reachable, and reports what it confirmed.
Known limitation, recorded not solved: the Access application and its policy live on the Cloudflare dashboard, outside version control — the same pattern as the RATE_LIMIT_KV binding. Neither is visible in the repository, and neither survives a repository restore.
Policy membership is an Operator decision and is not specified here. CC does not create or modify the Access policy. The Operator configures it.
analyze.js with its schema and validation. Verify locally: real input returns valid structured findings with sane source classification.shape.js with partition validation. Verify: two readers over one analysis produce genuinely different selections.render.js. Verify: prose per reader from selection only.Halt after step 2 and report. If two readers over one analysis produce near-identical selections, the thesis has failed and steps 3–6 are wasted. That is the cheapest available kill point, and it comes before any prose is written.
The spike is accepted when the Operator can, locally:
Acceptance is not correctness. It means the surface works well enough to answer the thesis question. The Operator answers that by reading the output, not by the tests passing.
RATE_LIMIT_KV bound locally or failed open._lib.js, analyze.js, illustrate.js, foundation-draft.js, and example-generator.astro are unmodified.DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Example Generator — Sales Perspective Spike — CR-2026-152 — v0.4 — 2026-07-16