DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-152-example-generator-sales-perspective-spike-v0_26.md

CR-2026-152 — Example Generator: Sales Perspective Spike — v0_26

Version. 0.26 Date. 2026-07-17 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.)

Change from v0.4. Sections 3.1, 3.2, and 5. The four reader archetypes (owner, ops lead, food safety/audit, systems lead) were perishables-specific and hardcoded; an Operator test on legal work produced columns shaped for readers that did not exist in that scenario. Readers are now derived from the work: analyze's output gains a readers array (§3.1), and the page becomes two-phase — an empty, generic ask; then, only after analyze returns, a reader section populated from what it proposed (§5). §3.2's example carried a hardcoded perishables reader ("reader": "owner", a margin-and-dock-level rationale) and was made reader-agnostic — the only concrete shape example in the CR, and a build session reads examples as intent. No other section changes. (v0.4 is kept byte-intact as the superseded prior.)

Change from v0.5. Section 3.1 only. v0.5 introduced derived readers but said only "readers" without specifying which kind — the legal test that followed derived readers of the prospect's own work product (lead trial counsel, litigation paralegal, testifying expert) rather than readers of the sales material itself. The tool produced feature illustrations of how the prospect's work would be shaped, not pitches for the prospect to buy the tool — collapsing this spike into what the public Example Generator already does. Section 3.1 now specifies which readers are meant, immediately after the readers array structure: decision-makers and influencers at the prospect organization, not roles inside the prospect's own work; observations about the prospect's own work-product readers belong in findings, not readers. Prompt posture is rewritten accordingly. Follow-on for next build, not done here: §5's reader-section subtitle was written before this correction and will need a matching update — noted, not edited, since this amendment is scoped to §3.1 only. No other section changes. (v0.5 is kept byte-intact as the superseded prior.)

Change from v0.6. Section 4.1 only. The contrast definition intersected foregrounded with cut, which reported no contrast on pairs differing on a third of the findings — measured on two real pairs, 0 narrow against 5 broad, and 1 narrow against 9 broad. The computation was correct; the definition was too narrow. Corrected to report both tiers. No other section changes. (v0.6 is kept byte-intact as the superseded prior.)

Change from v0.7. Section 3.3 only. Render's constraints did not forbid Loomworks internal vocabulary or invented illustrations; a legal run surfaced both. Section 3.3's prompt posture gains two additional constraints: no Loomworks internal vocabulary (the "Operator" role name must not appear in prospect-facing prose), and no invented illustrations (render may not manufacture example scenarios, hypothetical timelines, or named incidents that trace to no finding). Both are observed defects from the 2026-07-16 legal run, both are prompt-enforced and therefore soft per section 7. No other section changes. (v0.7 is kept byte-intact as the superseded prior.)

Change from v0.8. Section 3.1 only. v0.8 constrained render against Loomworks internal vocabulary but not analyze; a playbook run produced a finding whose claim text contained "Operator", which the panels then displayed verbatim to a prospect-facing surface. Section 3.1's prompt posture gains a matching constraint: a finding's claim text is displayed verbatim by the contrast and check panels, not paraphrased the way render paraphrases, so "Operator" and other Loomworks internal vocabulary must never appear in a finding. Observed 2026-07-16: a finding read "The commercial legal team lead or General Counsel is the Operator who must confirm each new version" — render correctly paraphrased it away, but the panels displayed it verbatim. This is the same class of guarantee as render's v0.8 constraint, not a stronger one — prompt-enforced and therefore soft, per section 7. No other section changes. (v0.8 is kept byte-intact as the superseded prior.)

Change from v0.9. Section 7 only. Reader-set non-determinism observed during the acceptance run. Section 7 gains a new item: analyze proposes readers by sampling, so the same four fields can produce a different room on a second run — an acceptance run on the playbook input derived a reader set that did not include the Chief Scientific Officer proposed by an earlier run on the identical input, while shape's partition arrays reproduced byte-identical across independent runs on a stored analysis. Not addressed here; noted as a question for the Operator Layer build. No other section changes. (v0.9 is kept byte-intact as the superseded prior.)

Change from v0.10. Section 5 only. Two corrections. First, section 5 specified side-by-side columns, following the mockup's layout — but the mockup made side-by-side look readable only because its reader texts were short hand-written blurbs; real rendered output runs to six paragraphs per reader, and the Operator reported the columns unreadable when judging acceptance criterion 2. Both the reader cards and the rendered readers were specified as grids against the mockup's short hand-written text; both fail on real derived content, so both are replaced with a stacked sequence — one full-width block per reader in the order checked; the comparison work moves to the contrast panel, where it is computed rather than left for the Operator to spot across narrow gutters. Second, section 5 specified the output surface but no way to take the output off the page; the Operator cannot carry a reader's version into a room. Section 5 gains a new subsection, "Taking the output off the page": a per-reader Copy control (plain text, one reader's prose, no page furniture), and a run-level Download control (one HTML file — four inputs, every rendered reader, the computed contrast panel, and the check panel verbatim, including its incompleteness statement, so the fabrication-risk flag travels with the prose it flags). Neither persists — clipboard action and client-side blob only, same posture as the rest of the page. No other section changes. (v0.10 is kept byte-intact as the superseded prior.)

Change from v0.11. Sections 3.3, 4.2, 5, and 10 — a pitch mode is added. The output carried reader-specific columns but no general overview: nothing the Operator says before getting specific to a room. The pitch is a general overview and opportunity, not shaped for any one reader — it runs through shape and render like a reader, but with fixed rules that are structural rather than domain-specific (forward: what the work is, what is hard about it, what becomes possible; cut: role-specific operational detail). It goes through shape so its selection is recorded and auditable and the check panel can scope to it — but it is not a reader: it does not appear in the reader list and does not participate in the contrast panel, which compares readers. The honesty constraint is the point of the pitch: an opportunity is a gap between where the prospect is and where they could be, and the pitch is the highest-fabrication-risk surface in this tool because its genre invites exactly a manufactured "before". Therefore its claims about the prospect's present state must trace to findings with source "input" — what they told the Operator, not what the model guessed; claims about what Loomworks does may draw on any finding; where the input does not establish where they are today, the pitch says what Loomworks does with work of this kind and manufactures no before. No superlatives, no transformation language, no invented pain. Prompt-enforced and therefore soft, per section 7. §3.3 adds the pitch mode; §4.2 extends the check panel to the pitch's inferred findings; §5 places the pitch first, above the rendered readers, with its own Copy control, inclusion in the Download, an explicit on-by-default control separate from the reader list and absent from the fan. §10's acceptance gained a criterion for the pitch (read aloud to the prospect, surviving their corrections), and its criterion 2 still said "columns" after v0.11 replaced them with stacked rendered readers — corrected. No other section changes. (v0.11 is kept byte-intact as the superseded prior.)

Change from v0.12. Section 7 only. The pitch's honesty constraint held on first exercise, and the audit surfaced where its boundary actually sits. Section 7 gains a new item: exercised 2026-07-17, the pitch made no claim tracing to nothing, manufactured no "before", and used no superlative or transformation language — but three inferred claims reached the prose by being scoped to the work-kind rather than the prospect ("a playbook of this kind", "likely", "may be"), within the letter of §3.3a's rule, and one of them reads in a room as a diagnosis of the prospect despite its generic framing. Not tightened — recorded so the boundary is known rather than assumed; the check panel's listing of these claims is the intended containment. No other section changes. (v0.12 is kept byte-intact as the superseded prior.)

Change from v0.13. Sections 3.3, 4.2, 5, and 10 — a Foundation Document draft is added. The output had no closing artifact: nothing the Operator leaves behind. A Foundation Document names what an engagement is committed to, and there is one engagement — the prospect's work — regardless of who is in the room, so the draft sits outside the reader axis: one per run, not one per reader, absent from the reader list and the contrast panel. §3.3 gains subsection 3.3b — the draft runs as a render over the findings with no shape step (it describes the work, it does not select material for someone), and its honesty constraint is sharper than the public Example Generator's because the Operator, not the prospect, wrote the four fields: findings with source "input" become commitments (the prospect stated these), findings with source "inferred" become open questions in a "still to work out" section (the model inferred them; the prospect has not confirmed them). It is labeled "a draft toward the engagement's Foundation Document", carries the D8.4 qualifier, and never uses the word "seed". §4.2 extends the check panel to the draft's inferred findings; §5 renders the draft last, below the rendered readers, with its own Copy control, inclusion in the Download, and an explicit on-by-default control separate from the reader list and absent from the fan; §10 gains a criterion (commitments trace to entered input, inferences appear as open questions). Prompt-enforced and therefore soft, per section 7. No other section changes. (v0.13 is kept byte-intact as the superseded prior.)

Change from v0.14. Sections 5 and 3.1 — wording only. The surface called the readers "the room", a phrase carried from the mockup when readers were four fixed archetypes invented for a single meeting. Derived readers span organizations and are reached separately — in a meeting, by a forwarded document, or in a procurement review weeks apart (observed 2026-07-16: one run derived readers at two different organizations) — so the metaphor over-specifies a meeting that does not happen and does not say what the readers are. §5 gains "The readers are not 'the room'": the surface asks "Who do you need to convince?", the hero heading, lede, and review banner drop "room", and the banner says the draft is not for the prospect until the Operator has read it. The reader question must also hold its distance from the four fields' "Who is the work for?" — a different question, and conflating them is what produced work-product readers in v0.5. §3.1's prompt posture, which described the readers as "the room the Operator is walking into", gets the same wording alignment; the readers derive well as they stand, so this is not a behavioural change. No other section changes. (v0.14 is kept byte-intact as the superseded prior.)

Change from v0.15. Section 3.3b only. The draft's honesty constraint leaked on first exercise. The v0.14 build partitions the findings by source server-side but hands the model both lists and asks it to respect the labels — prompt-enforced, and exercised 2026-07-17 it did not hold: two passages reached the commitments section from inferred findings, both Loomworks's own versioning and provenance value proposition (version history, supersession dates, approval authority legible in the artifact), presented to the prospect as their own commitment, and the same run put approval authority as a settled commitment in one section and an open question in the other. Corrected to a structural split: the draft is now two render calls, not one — a commitments call that receives only source "input" findings, an open-questions call that receives only source "inferred" findings, and a server that composes the two into one draft. The input-to-commitment boundary becomes structural rather than requested — a model not handed the inferred material cannot write it into the commitments, the same principle as shape emitting only finding ids. It does not fix within-section fabrication (section 7's standing limit, unchanged); what is now guaranteed is that no inferred finding reaches the commitments section. No other section changes. (v0.15 is kept byte-intact as the superseded prior.)

Change from v0.16. Section 7 only. The two-call split held on first exercise; the residual within-section risk was observed once during wiring. Section 7 gains an item recording it — the commitments call can still fabricate within its own section (a synthetic run produced one such commitment; the real biotech run produced none) — alongside a new item that nothing verifies the prose against the findings: every fabrication this build found was found by hand audit, so the fabrication surface is unmonitored, not merely unfixed, which is the largest gap in the design. The source-field item is amended — three guarantees now route through analyze's own source classification (the check panel, the pitch's present-state constraint, and the draft's two-call split), making that model judgement the design's load-bearing weakness. The latency item is amended with measured figures (analyze 24–56s, shape 4.2–13.1s, render 12.8–21.5s, about 49s for a two-reader run; the 183s outlier did not recur) and the note that the pitch and draft now make four render-class calls where one was planned, with no target set. No other section changes. (v0.16 is kept byte-intact as the superseded prior.)

Change from v0.17. Sections 6 and 8. Rate limiting was listed in section 7 as an inherited caveat; it is a deployment blocker, and section 8 (Access) now names it as one: the limiter keys on IP-and-hour and on day with no route, so this spike and the public Example Generator share one pool, and the caps — set at f2dc67f for a two-call generator — allow only two eight-request sales runs per hour before an Operator is locked out mid-run, and roughly twelve per day globally, taking the public marketing page down with them for the rest of the day. Re-sizing (separate counters by route family, raised caps, or both) trades against Anthropic spend and is an Operator decision, named not solved. Section 6 gains two failure findings from exercising the pitch and draft paths, which did not exist when the failure work was first done: the draft's commitments and open-questions calls run under Promise.all and fail together (a commitments failure discards a successful open-questions result), and the in-block failure message calls every surface — including the pitch and the draft — a reader. No other section changes. (v0.17 is kept byte-intact as the superseded prior.)

Change from v0.18. Sections 2, 6, and 8. The rate-limit deployment blocker is resolved and the in-block failure copy is corrected. §8's "must be re-sized before this deploys" item is replaced: the spike no longer shares the public generator's limiter pool but carries its own in functions/api/sales/_lib.js, keyed rl:sales:*, on the same RATE_LIMIT_KV binding, with Operator-set caps of 120/hour/IP and 400/day (fifteen runs/hour, fifty/day at eight requests per run); the public generator's 20/hour and 100/day are unchanged, and fail-open is retained (settled by Operator direction 2026-07-17). §2 gains the one permitted exception to not modifying the shared functions/api/_lib.js: the spike does not use its rate limiter, calling the spike-local one instead — the shared file is still not edited. §6's failure item is corrected: each failed block now names its own surface (the pitch, a named reader, or the Foundation Document draft) rather than calling every surface a reader. No other section changes. (v0.18 is kept byte-intact as the superseded prior.)

Change from v0.19. Sections 3.3, 4.2, 5, and 10 — a leave-behind is added. The output had nothing the Operator leaves with the prospect: the pitch is what is said, not what is left behind. §3.3 gains subsection 3.3c — one per run, not one per reader; a structured one-pager that does not go through shape (it is not an argument selected for someone, it is the case) and does not appear in the reader list or the contrast panel. Two of its six sections make claims about the prospect's present state and about other people's products, and neither is knowable from four sentences: every fabrication this tool has produced was the model inventing a present state (the step-3 audit, §4.2, is the record of it), and a competitor claim drawn from training data is worse — possibly stale, framed as fact, to someone who uses the product daily. Three new inputs are added, all optional, all Operator-supplied, all in phase one: how the prospect does this today as far as the Operator knows, who else they are considering and what the Operator knows about how it works, and what the Operator wants to happen next. Enforced in code, not the prompt: the leave-behind is two render calls. The first — sections 1, 2, and 4 — receives the findings, claims about the prospect's situation held to the pitch's source constraint (§3.3a). The second — sections 3, 5, and 6 — receives only the three new inputs and Loomworks's own description of what it does; no inferred findings are in its context, so it cannot invent a current state or a competitor because neither is available to it. If all three inputs are empty, the second call does not run and its sections are absent, not placeholdered. The table compares approaches, never products: a named vendor's approach is only describable from what the Operator supplied about it. §4.2 extends the check panel to the leave-behind's inferred findings, noting the second call contributes none by construction. §5 places the three new inputs in phase one below the four, as an optional group feeding the leave-behind; the leave-behind is an explicit control, on by default, separate from the reader list and absent from the fan, rendering after the readers and before the Foundation Document draft — the pitch is what is said, the readers are the arguments, and the two leave-behinds close: the case, then the starting point for the work. The draft's own description is corrected to match, since it no longer stands as the only artifact of that kind. The leave-behind carries its own Copy control and is included in the Download. §10 gains an acceptance criterion: with the three inputs empty, sections 3, 5, and 6 are absent rather than invented; supplied, every claim about the prospect's present state and any named alternative traces to what the Operator entered. No other section changes. (v0.19 is kept byte-intact as the superseded prior.)

Change from v0.20. Section 3.3c only. Exercised 2026-07-17 on the biotech supplier-agreements scenario: the leave-behind's two-call split held completely against inventing a competitor's facts — the named-alternative column read "Not known from what was shared" on every cell, five times, the highest-risk cell in the design — but leaked on the prospect's own current state inside the comparison table, in four of five rows: two extrapolated from what the Operator entered, two invented outright (an obligation-tracking row and a who-holds-the-record row, neither mentioned at all). The mechanism was the table's rows, not the prompt: the model chose which dimensions to compare from what Loomworks does, and every row it invented then demanded a current-approach cell it could not source, so it wrote one rather than leave a gap. §3.3c gains a new passage after the two-call description: each row must now be a dimension the Operator actually described — sourced by construction, because the row exists only because the Operator described it — not a dimension the model selected from Loomworks's own capabilities; a row with no sourced current-approach cell does not exist, the same principle as the two-call split itself and as shape emitting only finding ids. The existing "the model frames the comparison; it does not source it" sentence is corrected to match, with a "Prior position, corrected" note — the model was never meant to be sourcing product claims (that held), but it was still framing which dimensions existed, and that framing is exactly what leaked. Not a prompt tightening: the freedom to choose a row is removed, not asked to be used more carefully. No other section changes. (v0.20 is kept byte-intact as the superseded prior.)

Change from v0.21. Section 3.3c only. Exercised 2026-07-17: the row-sourcing fix held — two rows, both current-approach cells traced to what was entered, the two fabricated rows gone. But with the current-approach input supplied and no alternative named, the table rendered a third column in which every cell read "Not known from what was shared" — the per-cell rule working exactly as v0.21 wrote it, and the wrong shape: a column of refusals is a visible gap in front of the prospect, drawing their eye to a comparison the Operator never raised. §3.3c gains a passage alongside the row-sourcing rule: the named-alternative column exists only if the Operator named one — not a column of refusals, no column. The refusal text stays correct for a named alternative whose behavior on a particular dimension is unknown; it was never meant to stand in for the whole column when no alternative exists at all. The existing "the named alternative's cell remains 'Not known from what was shared' unless the Operator supplied it" sentence is corrected to match, with a "Prior position, corrected" note — same rule as everywhere else in this design: only show what is available; absent, not empty. No other section changes. (v0.21 is kept byte-intact as the superseded prior.)

Change from v0.22. Sections 3.3 and 7. Exercised 2026-07-17: the leave-behind's rows call wrote "the Operator" directly into prospect-facing prose. The vocabulary constraint exists in every other generating prompt in this file — it was never added to this one, because this one is new. It is the third time this has happened: render's own vocabulary constraint was missing until v0.8, analyze's until v0.9, and now the rows call's until this amendment — each caught by audit, none by the build. §3.3 gains a standing rule, stated once at the top of the subsection and governing 3.3, 3.3a, 3.3b, and 3.3c as a family rather than repeated per prompt: every generating call that produces prospect-facing prose carries the vocabulary constraint, not optionally and not as a per-prompt choice — a new call added to this chain after this amendment inherits it without a further amendment. §7 gains an item naming the pattern itself: the constraint lives in prose inside each prompt, nothing enforces that a new prompt carries it, and nothing in the code makes a missing constraint visible — recorded, not fixed, so the next person adding a call to this chain knows what has already gone wrong three times, and so a build of this properly in the Operator Layer knows that per-prompt prose constraints do not survive the addition of new prompts. No other section changes. (v0.22 is kept byte-intact as the superseded prior.)

Change from v0.23. Section 3.2 only. shape's rationale field is generated on every call, required by validation, and never displayed — the page reads foregrounded, included, and cut and discards the rest. Measured 2026-07-17: roughly 177 tokens per shape call, almost entirely output, so a two- or three-reader run spends 350 to 700 tokens on it. §3.2 gains a passage recording why it is kept rather than dropped as waste: the partition proves selection happened, but the rationale is the only artifact that shows the reasoning behind it, and every audit in this build has read it when a selection looked wrong — at step 2 it was the evidence the thesis held at all, since a partition alone shows identifiers moved between buckets, not that the model reasoned about a reader. A second possible justification — that asking for it may improve the partition itself — is named as unproven, not claimed. The passage also records the coupling for whoever considers dropping it later: the prompt's instruction and validateShapeResponse's explicit rationale check must go together, or every shape call starts failing validation for a field the model has stopped sending; the partition invariant itself does not touch rationale and is unaffected either way. No other section changes. (v0.23 is kept byte-intact as the superseded prior.)

Change from v0.24. Sections 3.3c and 7. Exercised 2026-07-17 on a real run, the leave-behind produced a one-row table and an alternatives column that refused on the only cell it had. Both were the specification working exactly as written — v0.21's row-sourcing rule and v0.22's per-column rule, correct against the fabrication they were built to stop, and unusable against an input shaped differently than the one that motivated them: a nine-word "how today" answer ("they use a product from CG.AI") produced one row, and seven named alternatives produced seven honest refusals, one of which buried the single most competitively useful fact in the entire input — that a named alternative may be claiming Loomworks's own ground (memory). Two rules are reversed. §3.3c: the dimensions call now receives the four original fields (chiefly the constraints field — what the work must get right or avoid), never howToday, never the findings, never Loomworks's own description; each dimension is something the work demands, not something Loomworks offers, and a dimension whose current-approach cell cannot be sourced from howToday still appears, refusing honestly, rather than the row disappearing. The named-alternative column is removed from the table outright — three columns, not four — and alternatives become their own section, section 6 ("the landscape," next steps renumbered to section 7), gated on the Operator having named one, forbidden from characterising a named product beyond what the Operator said about it, and required to foreground any alternative whose Operator-described behaviour overlaps Loomworks's own ground. Both reversals are argued safe against v0.21's original finding: the dimensions call still cannot see Loomworks's description, so it still cannot choose a dimension shaped by what Loomworks offers — what changed is only where a dimension's label comes from and whether an unsourced cell is shown or hidden. §7 gains an item naming the pattern this run exposed: a specification can be correct — tested against the exact failure it was written to prevent, and holding — while being unusable against a different shape of input the text predicted and misread ("a thin description gives a thin table, and that is correct" read as a virtue when the Operator experienced it as a broken tool). No other section changes. (v0.24 is kept byte-intact as the superseded prior.)

Change from v0.25. Sections 4.2, 5, and 10 only — the consequential renumbering v0.25 left behind. §3.3c was rewritten to describe the leave-behind's actual shape (five calls, not two; seven sections, not six; a three-column table with alternatives moved into their own section, "the landscape"), but three sections still described the old shape, and §10's acceptance criterion in particular tested sections that no longer mean what it said: "sections 3, 5, and 6" under the old numbering named how-today, the four-column table, and next steps; under the new numbering, section 6 is the landscape and next steps is section 7. §4.2 is corrected to describe the leave-behind's calls as §3.3c now does, including that the row-fill call — unlike the old "second call" — does see findings for its Loomworks cell, the same latitude the case call has, so the check panel now covers it the same way; the dimensions and landscape calls, which never see a finding, contribute none, by construction. §5 is corrected: the leave-behind control now describes issuing several calls rather than two, and the Copy/Download passage that named "sections 3, 5, or 6" as what disappears when all three inputs are blank now correctly names sections 3, 5, 6, and 7. §10's acceptance criterion 4 is replaced with four criteria matching §3.3c's actual rules: the three-way absence check for the table, the landscape, and next steps; the table-without-landscape case; the landscape's own honesty rules, including the Loomworks's-ground foregrounding; and the table's dimension-sourcing rule, including that an unsourced cell now refuses rather than the row disappearing. No claim is made here that §3.3c does not already make; no new behavior is introduced. No other section changes. (v0.25 is kept byte-intact as the superseded prior.)


Plain-language summary

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.


1. Why the existing chain cannot carry this

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.


2. What is built

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 one permitted exception to reusing the shared functions/api/_lib.js is that the spike does not use its rate limiter. functions/api/sales/*.js call a spike-local limiter in functions/api/sales/_lib.js instead (§8). The shared file is still not edited.

The existing analyze.js, illustrate.js, foundation-draft.js, and example-generator.astro are not touched.


3. The chain

3.1 analyze — find the material, write for nobody

Input. 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"] },
    ...
  ],
  "readers": [
    {
      "key": "short_stable_key",
      "label": "The supervising partner",
      "description": "One short line — what they are measured on",
      "forward": "What this reader's version leads with",
      "cut": "What this reader's version leaves out"
    },
    ...
  ]
}

Which readers. These are the people who would read this sales material — the people whose buy-in the Operator needs at the prospect organization, reached however they are reached: in a meeting, by a forwarded document, or in a procurement review weeks apart. They are decision-makers and influencers: whoever holds the budget, whoever owns the operational pain, whoever must sign off, whoever assesses the risk. Derived from the domain, in that domain's vocabulary.

They are not readers of the prospect's own work product. For a legal matter, "managing partner" and "general counsel" are readers of this material. "Testifying expert" and "litigation paralegal" are not — they are roles inside the work, and a column shaped for them is a feature description rather than a pitch. The public Example Generator already illustrates how work is shaped for its own readers; that is not what this tool is for.

Where work-product readers belong. Observations about who reads the prospect's work, and what must not cross between them, are valuable — they are findings, not readers. That outside co-counsel must not receive privileged work product is ammunition inside a pitch to a managing partner, not a column of its own. analyze should surface such observations as findings with source "inferred".

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.

readers is derived, not fixed. Between three and five readers, proposed by the model in the vocabulary of the work it was just given — not the perishables set, not any fixed set. The model proposes; the Operator selects, at generation time, by checking the ones that apply. This replaces a hardcoded four-archetype set (owner, ops lead, food safety/audit, systems lead) that shipped in v0.1–v0.4 — see the Change-from-v0.4 note.

Prompt posture. Find everything worth saying. Organize it. Address nobody. Do not write prose. Emit only the JSON object. Propose the readers of the sales material itself — the decision-makers and influencers at the prospect organization who would read a pitch about adopting something like this, not the people who read or act on the prospect's own work. Grounded in the four fields, in that organization's vocabulary — not a generic template, and not the roles that already exist inside the work being described.

No Loomworks internal vocabulary in findings. A finding's claim text is displayed verbatim by the contrast and check panels — it is not paraphrased the way render paraphrases. Anything a finding says reaches the surface as written. "Operator" is this system's role name and must never appear in a finding; name the actual role in the prospect's world. The four rooms may be named in plain terms; codebase shorthand and internal role names may not. Observed 2026-07-16: a finding read "The commercial legal team lead or General Counsel is the Operator who must confirm each new version" — render correctly paraphrased it away, but the panels displayed it verbatim. This is prompt-enforced and therefore soft, per section 7. It is the same class of guarantee as render's, not stronger.

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, any reader missing one of its five fields, reader keys not unique, or reader count outside three to five. A malformed readers array fails the whole analyze response, exactly as a malformed findings array does. On failure, surface the plain recovery message (§6). Never render a malformed analysis.

3.2 shape — select, do not write

Input. The findings array and exactly one reader key.

Output. A selection over finding IDs. It cannot introduce text.


{
  "reader": "<key from the readers array returned by analyze>",
  "foregrounded": ["f3","f7"],
  "included": ["f1","f4","f9"],
  "cut": ["f2","f5","f6","f8"],
  "rationale": "What this reader is measured on, and what is therefore not theirs to read."
}

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.

The rationale field is diagnostic, not display. Every shape call returns a rationale alongside the partition. Nothing renders it — the page reads foregrounded, included and cut, and discards the rest. Measured 2026-07-17: roughly 177 tokens per shape call, almost entirely output, so a two or three reader run spends 350 to 700 tokens on it.

It is retained deliberately. The partition proves that selection happened; the rationale is the only thing that shows the reasoning behind it. At step 2 of this build it was the evidence that the thesis held — a partition alone shows identifiers moved between buckets, not that the model reasoned about a reader. Every audit in this build has read it. When a selection looks wrong, it is the first place to look.

A possible second reason, unproven: asking for a justification alongside the partition may improve the partition. Nobody has tested that here.

If it is ever dropped, two things must go together — the prompt's instruction and validateShapeResponse's explicit rationale check. Removing only the first makes every shape call fail validation, because the model stops sending a field the validator still requires. The partition invariant itself does not touch rationale and is unaffected either way.

3.3 render — prose, from the selection only

Every prompt that produces prospect-facing prose carries the vocabulary constraint. No Loomworks internal vocabulary. "Operator" is this system's role name and must never reach a prospect — name the actual person in their world, or say "your". The four rooms may be named in plain terms; codebase shorthand and internal role names may not.

This is stated once, here, because it applies to every generating call — everything under 3.3, 3.3a, 3.3b, and 3.3c — and has now been missed three times. Observed 2026-07-17: a rendered column told a law firm's technology director to "wait for the Operator's confirmation" — closed in v0.8 for render. A finding's claim text named the Operator and the panels displayed it verbatim — closed in v0.9 for analyze. The comparison table's rows call wrote "the Operator ratifies" into prospect-facing prose — this amendment.

A new generating call inherits this constraint. It is not optional and it is not per-prompt. Any call added to this chain after this amendment carries it without a further amendment.

Input. 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.

No Loomworks internal vocabulary. The reader is a stranger at the prospect organization. "Operator" is this system's role name and must never appear — name the actual person in their world, or say "your". The four rooms may be named in plain terms; codebase shorthand and internal role names may not. Observed 2026-07-16: a rendered column told a law firm's technology director to "wait for the Operator's confirmation."

No invented illustrations. Render may not manufacture example scenarios, hypothetical timelines, or named incidents that trace to no finding. Where a finding warrants an example, the example comes from what the Operator described. Observed 2026-07-16: a column invented a "deponent said on Monday, correction arrives on Friday" scenario with no source finding. This is prompt-enforced and therefore soft, per section 7 — it narrows the fabrication surface, it does not close it.

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.

3.3a The pitch mode — a general overview, not a reader

The pitch. A general overview and opportunity, not shaped for any one reader. It runs through shape and render like a reader, but with fixed rules that are structural rather than domain-specific:

It goes through shape so its selection is recorded and auditable, and so the check panel (§4.2) can scope to what reached it. It is not a reader: it does not appear in the reader list, and it does not participate in the contrast panel (§4.1), which compares readers. The pitch's shape call uses the fixed forward/cut rules above in place of a reader's per-reader rationale; its selection partitions the findings exactly as any shape call does, so it cannot introduce text (§3.2's invariant holds).

The honesty constraint — the point of the pitch. An opportunity is a gap between where the prospect is and where they could be. The model knows four fields; everything else about their present state is inference, and the pitch is the highest-fabrication-risk surface in this tool because its genre invites exactly that — a manufactured "before" that makes the opportunity look larger.

Therefore: the pitch's claims about the prospect's present state must trace to findings with source: "input" — what they told the Operator, not what the model guessed. Claims about what Loomworks does with work of this kind may draw on any finding. If the input does not establish where they are today, the pitch says what Loomworks does with work of this kind and does not manufacture a before.

No superlatives, no transformation language, no invented pain. Realistic means it would survive being read aloud to the prospect with them correcting it.

This is prompt-enforced and therefore soft, per section 7 — like render's no-invented-illustrations constraint, it narrows the fabrication surface, it does not close it. It is the same class of guarantee, applied to the surface where the genre pushes hardest against it.

3.3b The Foundation Document draft — one per run, outside the reader axis

One draft per run, not one per reader. A Foundation Document names what an engagement is committed to. There is one engagement — the prospect's work — regardless of who is in the room. Readers shape the argument for adopting it; they do not shape the work itself. The draft therefore sits outside the reader axis, does not appear in the reader list, and does not participate in the contrast panel.

It does not go through shape: it is not an argument that selects material for someone, it is a description of the work.

The draft is two render calls, not one.

The commitments call receives only findings with source "input". The inferred findings are not in its context. It writes what the engagement is committed to, from what the prospect stated.

The open-questions call receives only findings with source "inferred". It writes the "still to work out" section, phrased as questions to confirm.

The server composes the two into one draft.

Why the split rather than a stronger instruction. The single-call design partitioned the findings correctly and labeled both lists for the model, then asked it to respect the labels. Exercised 2026-07-17, it did not. Two passages reached the commitments section from inferred findings, and both were Loomworks's own versioning and provenance value proposition — version history, supersession dates, approval authority legible in the artifact — presented to the prospect as their own commitment. The model reached for the product's story and wrote it into the prospect's foundation.

The same run produced a direct contradiction: approval authority appeared as a settled commitment in one section and as an open question in the other, in the same draft.

A model handed material and told not to use it in one section will use it. A model not handed the material cannot. The split makes the input-to-commitment boundary structural rather than requested — the same principle as shape emitting only finding ids.

What the split does not fix. The commitments call can still fabricate within its own section — assert something in no finding at all. That is section 7's standing limit and is unchanged. What is now guaranteed is that no inferred finding reaches the commitments section.

The honesty constraint, which is sharper here than in the public Example Generator. There, the prospect writes the four fields and the draft reflects their own words. Here the Operator writes them, so the draft is a Foundation Document for the prospect's engagement built from the Operator's account of their work — and handed to the prospect as theirs.

Therefore:

A draft with honest gaps invites the correction that makes it useful. A draft that asserts confidently invites the prospect to find the error and stop trusting the room.

Labeling. The output is "a draft toward the engagement's Foundation Document" and carries the qualifier per the public generator's D8.4. The word "seed" never appears — the draft travels to a prospect who has never heard it, and the tool being Operator-facing does not change what the artifact is.

This is prompt-enforced and therefore soft, per section 7.

3.3c The leave-behind — one per run, two render calls split by source

One per run, not one per reader. A structured one-pager the Operator leaves with the prospect. It does not go through shape — it is not an argument selected for someone, it is the case. It does not appear in the reader list and does not participate in the contrast panel.

Its sections, in order:

  1. The problem, in plain English. One short paragraph. What this work is fighting. No jargon, no methodology nouns, no product names.
  2. Why the Loomworks approach. Three to five benefits. Claims about Loomworks, not about the prospect.
  3. How this is done today. Absent unless the Operator supplied it.
  4. Where Loomworks sits. Category positioning — what kind of thing this is and what kind of thing it is not.
  5. The comparison. A table: the dimension, the current approach, and Loomworks. Present whenever the four original fields yield at least one dimension the work demands.
  6. The landscape. What the Operator named, and what the Operator said about it. Absent unless the Operator named an alternative.
  7. Next steps. Absent unless the Operator supplied what they want to happen.

Three new inputs, all optional, all Operator-supplied, all in phase one:

Why these are inputs and not inferences — this is the point of the whole subsection. Sections 3, 6, and — for their content, though not any longer for their shape — 5 make claims about the prospect's present state and about other people's products. Neither is knowable from four sentences.

Every fabrication this tool has produced was the model inventing a present state. The step-3 audit (§4.2, 2026-07-16, perishables input, systems-lead reader) is the record of it — with nothing in the four fields to license either claim, the model asserted that a prior system might be "storing a single current value" and that "the current workflow allows offers to go out from a live, mutable inventory view." Section 3 is that failure mode as a requirement.

Competitor claims are worse and differently wrong. A claim about another company's product is drawn from training data with a cutoff. "They probably use spreadsheets" is a guess the prospect can correct. "That vendor requires manual reconciliation" is a fact that may have been true once and false since, said to someone who uses it daily. Section 6 is that failure mode as a requirement: naming a product is not knowing it.

Therefore, enforced in code and not in the prompt: the leave-behind is five render calls.

The first call — the Loomworks case. Sections 1, 2, and 4. It receives the findings. Claims about Loomworks may draw on any finding. Claims about the prospect's situation trace to findings with source "input", per the pitch's constraint in §3.3a, with the same soft limit.

The second call — the prose comparison. Sections 3 and 7. It receives only howToday and what the Operator wants to happen next. No inferred findings are in its context. It cannot invent a current state because that isn't available to it. Absent per section: section 3 without howToday, section 7 without a stated next step.

The dimensions call. Feeds section 5. It receives the four original fields — what the work is, who it's for, what done looks like, and, most importantly, what it must get right or avoid — never the three new leave-behind inputs, never the findings, never Loomworks's own description. Each dimension it proposes is something the work demands, not something Loomworks offers: a constraint, a requirement, a thing that must go right. A rich constraints field produces a rich table; a thin one produces a thin table, honestly, on its own terms rather than howToday's.

The row-fill call. Given the dimensions call's fixed, ordered list, it fills two more things per dimension: the current-approach cell, sourced from howToday where howToday addresses that dimension and stated as unsourced — never omitted — where it does not; and the Loomworks cell, which may draw on any finding, the same latitude the case call has for claims about Loomworks.

If none of the three new inputs are supplied at all, none of the calls that depend on them fire, and sections 3, 5, 6, and 7 are absent accordingly. Not placeholdered, not "not specified" — absent. Same rule as a tool the mint refuses: not shown, not disabled, gone.

A dimension whose current-approach cell cannot be sourced still appears, and its cell says so. "Not known — they use a product from CG.AI, and nothing was said about how it handles this." Reversed from v0.21: a row is no longer suppressed for lacking a sourced cell; the cell itself carries the admission.

Prior position, corrected. v0.21 said: "each row is a dimension the Operator described… A row with no sourced current-approach cell does not exist. Not 'not specified' — absent." That rule was written to stop the model choosing rows from Loomworks's feature list and inventing a current-approach cell for each — it worked, and the invented obligation-tracking and who-holds-the-record rows it was built to prevent are still prevented, by the same mechanism: the dimensions call still cannot see Loomworks's own description, so it still cannot choose a row shaped by what Loomworks offers. What v0.21 got wrong was tying the table's existence to howToday's phrasing rather than to the work's own requirements — a nine-word answer about which product the prospect currently uses produced a one-row table, on a real run, 2026-07-17. Dimensions now come from the four original fields, chiefly the constraints field; an unsourced current-approach cell now refuses honestly rather than the row disappearing.

Why the reversal is safe. The fabrication mechanism v0.21 identified was rows chosen from Loomworks's capabilities. That is still forbidden — the dimensions call cannot choose them, because Loomworks's description is not in its context. The dimensions are bounded by the Operator's own words about the work, not by what Loomworks does. An unsourced cell now refuses rather than being suppressed, and refusal is proven behaviour: the same 2026-07-17 run's alternatives column refused on every cell it was given, correctly, rather than inventing.

The absence is the artifact's most useful output. A table of several dimensions the work demands, with current-approach cells reading "not known," is not a gap — it is the question list for the follow-up conversation, which on the 2026-07-17 run is exactly what the Operator said he wanted to happen next. The table stops pretending to end the conversation and starts generating the next one.

The table is three columns: the dimension, the current approach, and Loomworks. There is no fourth column.

Prior position, corrected. v0.21 said the named alternative's cell refuses unless the Operator supplied detail about that dimension. v0.22 corrected that to a per-column rule: no alternative named, no column. Both answered the wrong question. Exercised 2026-07-17 with seven alternatives named by name, every honest cell in that column still refused, because naming a product is not describing how it works. A grid of refusals does not show a comparison; it displays the Operator's own ignorance to the prospect, in a format that makes the gap look deliberate. The column is removed, not conditioned.

The named alternative becomes its own section. Section 6, "the landscape" — placed after the comparison and before next steps, absent unless the Operator named an alternative. It carries what the Operator named and what the Operator said about any of them. It does not characterise a product from the model's own knowledge: naming a product is not knowing it, and the model may not say what a named alternative does, what category it belongs to, or how it works, unless the Operator supplied that.

It foregrounds any alternative the Operator's own description places on Loomworks's ground. Exercised 2026-07-17: the Operator wrote that one named alternative "may be promoting similar capabilities (memory)." Memory is what Loomworks is. Connecting those two is not a claim about the world — it is a comparison between two things the landscape call is given: the Operator's own observation, and Loomworks's description of itself, the same standing description the second call receives, not the findings. That connection is the section's whole value, and on the run that motivated this amendment it was the single most competitively useful fact in the entire input, reaching the artifact as a refused cell before this amendment.

Where the Operator named a product and said nothing about it, the section says nothing about it beyond the name. Six names with no detail is a short section, and that is honest — it says which one is worth talking about and leaves the rest as a list.

The landscape compares approaches, never products. A named vendor's approach is only describable from what the Operator supplied about it — the same discipline that used to live in a table cell now lives in a section with room to say it plainly. Same principle as FORAY's regulatory flags: Operator-selected, never model-inferred.

The absence is useful. If the leave-behind cannot write section 3, the Operator does not know how the prospect works today — and that is the thing to ask them, not to generate.

Enforcement. Each call's context is deliberately bounded, the same principle as §3.2's partition and §3.3b's commitments/open-questions split. The dimensions call cannot see Loomworks's own description, so it cannot choose a dimension shaped by what Loomworks offers. The landscape call cannot see the findings, so its "memory" connection can only ever be a comparison to Loomworks's own standing description, never an inference about this specific prospect. The first call's claim about the prospect's situation rests on the same prompt-level source constraint as the pitch (§3.3a) and is therefore soft, per section 7, in exactly the same way and to exactly the same degree.


4. Two panels that are computed, not written

Both were hand-authored in the mockup. Neither should be.

4.1 The contrast panel — computed from the selection diff, at two tiers

Given two or more shape selections, the contrast is set arithmetic over finding IDs. No model call. If it can be computed it must not be generated — a generated contrast can claim a difference that is not there.

Two tiers are reported, because one alone misleads.

The sharp tier: foregrounded(A) ∩ cut(B). What one reader leads with and the other refuses. The strongest available signal, and rare — readers often share headline findings while differing underneath.

The broad tier: (foregrounded ∪ included)(A) ∩ cut(B). What A's document carries that B's does not have at all. This is the honest measure of how far two documents differ.

Both directions of both tiers. Where a direction is empty it is omitted, not placeholdered.

The panel leads with the broad tier. The sharp tier is reported when non-empty, marked as the stronger signal. Where both are empty in both directions, the two selections genuinely overlap and the panel says so plainly rather than manufacturing a difference.

Prior position, corrected. CR v0.1 through v0.6 defined contrast solely as foregrounded(A) ∩ cut(B). That definition was measured against two real pairs on 2026-07-16 and found to report 0 where 5 findings differed, and 1 where 9 differed. Both readers led with the same headline findings while their documents diverged substantially underneath — the narrow definition looks exactly where difference is not. It was not a computation error. It would have reported "no contrast" on pairs that differ by a third of the analysis, in a tool whose entire purpose is to show that difference.

4.2 The check panel — filtered from 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.

The check panel covers the pitch's inferred findings alongside the readers'. The pitch's shape selection is recorded (§3.3a), so every finding with source === "inferred" that survived into the rendered pitch is filtered onto the panel exactly as a reader's are. The pitch carries the highest fabrication risk of any surface here — its genre invites a manufactured "before" (§3.3a) — and it is not exempt from the audit; its inferred specifics appear on the same list, scoped to what reached the pitch.

The check panel covers the draft's inferred findings too, alongside the pitch's and the readers'. The Foundation Document draft (§3.3b) renders over the findings without a shape step, so its inferred findings reach the panel by the same source filter. The draft already routes them into its "still to work out" section rather than asserting them as commitments (§3.3b); listing them here is the same audit applied to the same surface, not a second mechanism.

The check panel covers the leave-behind's inferred findings too, alongside the draft's, the pitch's, and the readers'. The leave-behind is five calls, not two (§3.3c). Two of them render over findings: the case call (sections 1, 2, 4) the same way the pitch and the draft do, and the row-fill call, whose Loomworks cell "may draw on any finding, the same latitude the case call has" — so it is covered the same way. Any inferred finding that survives into either reaches the panel by the same source filter. The prose-comparison call, the dimensions call, and the landscape call contribute none, by construction — none of the three ever receives a finding (the dimensions call sees the four original fields; the landscape call sees the alternatives text and Loomworks's own standing description; the prose-comparison call sees only howToday and the stated next step) — so there is nothing there for the filter to find.

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.


5. The page

Follows sales-perspective mockup v0_2 for visual language; the flow itself is now two-phase, not the mockup's single-screen form. The mockup's four hardcoded readers are gone along with the fixed archetype set — see the Change-from-v0.4 note.

The reader question — "Who do you need to convince?"

The readers are not "the room". The surface asks "Who do you need to convince?" — these are the people whose buy-in the Operator needs, deciders and influencers alike, reached in a meeting or by a forwarded document or in a procurement review weeks apart. Observed 2026-07-16: one run derived readers at two different organizations.

The phrasing also has to hold its distance from the four fields' "Who is the work for?", which asks who reads the prospect's work. That is a different question with a different answer, and conflating them is what produced work-product readers in v0.5. The reader question asks who reads this sales material.

Replace "room" in the surface copy. The hero heading, the lede, and the review banner carried "room" from the mockup, when readers were four fixed archetypes invented for a single meeting; the metaphor over-specifies a meeting that does not happen and does not say what the readers are. The hero heading asks "Who do you need to convince?"; the lede and the review banner drop "room" for the same framing. The review banner says the draft is not for the prospect until the Operator has read it.

Phase one — the ask

Phase two — the room

The reader cards stack too. Each proposed reader is a full-width row, not a cell in a grid: the checkbox and label, the one-line description, then its forward and cut rules at a readable measure. The mockup's grid held four hand-written one-line descriptions. Derived readers carry a description plus a forward rule plus a cut rule — several sentences each — and a two-up grid renders them as tall narrow stacks with ragged empty cells beside them.

The same correction as the rendered readers, for the same reason: the layout was specified against short placeholder text and does not hold real content.

Output — stacked, not columns

The pitch renders first, above the rendered readers. It is the overview, and the Operator reads it before the role-specific versions — the general opportunity, then who in the room the specifics are for. It is a full-width block like the readers, but it sits at the top and is not one of them.

The rendered readers stack, one above the other, below the pitch, in the order they were checked. Each is a full-width block: the reader's label, then their prose at a readable measure. Not columns. Rendered output runs to several paragraphs per reader, and side-by-side reduces that to unreadable gutters.

The leave-behind renders after the readers, before the Foundation Document draft. The pitch is what the Operator says; the readers are the arguments; the two leave-behinds close — the case for them (§3.3c), then the starting point for the work (§3.3b). It is a full-width block like the others, sitting between the readers and the draft.

The Foundation Document draft renders last, below the leave-behind. It is the starting point for the work, read after the case has been made for it — the pitch opens, the readers argue, the leave-behind makes the case in a form the prospect keeps, and the draft is the thing the engagement would actually commit to. It is a full-width block like the others, but it sits at the bottom and is not one of them (§3.3b).

Prior position, corrected. Through v0.19, this section called the draft "what the Operator leaves behind," as if it were the only artifact of that kind. §3.3c adds a second, more literal one — a structured case the Operator physically leaves with the prospect. Both are leave-behinds now; the draft's role is recast as the second of the two, the starting point for the work rather than the case for it.

The comparison work moves to the contrast panel, which is where it belongs — it computes the difference rather than asking the Operator to spot it across two narrow columns. Section 4.1's two tiers already report what each reader's document carries that another's does not.

Prior position, corrected. CR v0.1 through v0.10 specified side-by-side columns, following the mockup's layout. The mockup made side-by-side look readable only because its reader texts were short hand-written blurbs; real rendered output runs to six paragraphs per reader, and the Operator reported the columns unreadable when judging acceptance criterion 2.

Taking the output off the page

Each rendered reader carries its own Copy control — the Operator will paste one reader's version into one message. Copy takes that reader's prose as plain text, without the surrounding page furniture.

The pitch carries its own Copy control, the same as a reader's — plain text, its prose, no page furniture — and is included in the Download. The overview travels with the run.

The leave-behind carries its own Copy control, the same as the pitch's and a reader's — plain text, its rendered case, no page furniture — and is included in the Download. Where a section's call did not run (§3.3c — all three new inputs empty, or an individual input left blank), the copied text simply has no sections 3, 5, 6, or 7 for the calls that didn't fire — the same absence as on the page.

The Foundation Document draft carries its own Copy control, the same as the leave-behind's, the pitch's, and a reader's — plain text, its prose, no page furniture — and is included in the Download. The starting point for the work travels with the run.

The run as a whole carries a Download control taking everything as one file: the four fields, the three leave-behind inputs, the pitch, every rendered reader, the leave-behind, the Foundation Document draft, the computed contrast panel, and the check panel. HTML, so it prints from the browser without a separate Print control.

The check panel travels with the output. A downloaded run carrying the prose without the list of inferred specifics hands the Operator the fabrication risk and withholds the thing that flags it. The check panel's copy — including its statement that it does not cover specifics introduced during rendering — appears in the download verbatim.

Copy on a single reader is the exception: it is a deliberate act on one version, and the Operator has the panel on screen when they do it.

Nothing about this persists. Copy is a clipboard action; Download is a client-side blob. No storage, no server round-trip. Same posture as the rest of the page.

Standing, unchanged


6. Failure

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.

The draft's two calls fail together. The commitments and open-questions calls run under Promise.all, so a failure in either discards the other's output. Observed 2026-07-17: a commitments failure produced a total draft failure, indistinguishable from either call failing, despite the open-questions call having succeeded.

Whether a half-draft should render is a genuine design question — commitments without open questions reads as more settled than it is, which is the opposite of what the split was built for. Recorded as known, not resolved.

The in-block failure message names its own surface. Each failed block says what failed in its own terms — the pitch, a named reader, or the Foundation Document draft — rather than calling every surface a reader. Corrected 2026-07-17 after the generic copy was observed on the pitch and draft blocks.


7. What this spike does not solve

Observed 2026-07-17: during wiring, a synthetic run produced a commitment reading "the record of what was approved travels with the work" from input findings that never said it — a provenance-flavoured claim of exactly the kind the split was built to keep out, arriving from the other direction. The subsequent real run on the biotech supplier-agreements scenario produced five commitments, all tracing to input findings, and showed none of this.

This is the standing section 7 limit, now observed on the draft specifically. It matters more here than elsewhere: a fabricated commitment is presented to the prospect as something they said about their own business.

When this CR was written, source fed only the check panel. It now also carries the pitch's present-state constraint and the draft's two-call split. The split is structural given correct tags — but if analyze tags an inference as "input", that finding enters the commitments call's context and no mechanism downstream can see the error. The boundary was drawn wrong before the call ran.

Every structural guarantee in this design ultimately routes through a model deciding what to hand over. That is the design's load-bearing weakness. It is not addressed here.

One of them — "there is no automatic way to surface those linkages" — reads in a room as a diagnosis of the prospect's situation despite its generic framing. A listener does not hear "a playbook of this kind" and exempt themselves.

This is not being tightened. The hedged form is honest about how the model knows the claim, and a stricter rule would push the pitch toward saying nothing about the work's nature, leaving no opportunity to name. The check panel lists these claims, which is the intended containment. It is recorded here so the boundary is known rather than assumed.

The constraints live in prose in the prompts. Nothing enforces that a new prompt has them, and nothing in the code makes a missing constraint visible. This is not fixed here: it is named so the next person adding a call knows the pattern, and so that anyone building this properly in the Operator Layer knows that per-prompt prose constraints do not survive the addition of new prompts.

It was correct and it was unusable. Thinness reads to the Operator as a broken tool, not as a prompt to go and ask. This is recorded because the same trap is available to every rule in this document that says an absence is informative: an absence is only informative if someone reads it as one.


8. Access

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.

Rate limiting: a spike-local limiter, settled by Operator direction 2026-07-17.

The shared limiter keys on IP and hour, and on day, with no route in either key, so every function calling it increments the same two counters. Its caps were sized at f2dc67f for a two-call generator. A sales run makes eight requests. Under the shared caps that is two runs per hour before a mid-run lockout, and roughly twelve per day taken from the same pool as the public marketing page.

The spike does not modify the shared limiter — section 2 forbids it, and the public generator depends on its behaviour. Instead functions/api/sales/_lib.js carries its own limiter, keyed rl:sales:ip:${ip}:${hourBucket} and rl:sales:global:${dayBucket}, using the same RATE_LIMIT_KV binding. The two pools do not compete.

Sales caps: 120 per hour per IP, 400 per day global. At eight requests per run that is fifteen runs per hour and fifty per day. Sized for Operator use behind Access, not for public traffic.

The public generator's 20 per hour and 100 per day are unchanged.

Fail-open is retained, matching the shared limiter: an unbound RATE_LIMIT_KV disables limiting with a console warning. Behind Access the exposure is bounded to whoever the Operator admits to the policy. Noted, not solved.


9. Build sequence

  1. analyze.js with its schema and validation. Verify locally: real input returns valid structured findings with sane source classification.
  2. shape.js with partition validation. Verify: two readers over one analysis produce genuinely different selections.
  3. render.js. Verify: prose per reader from selection only.
  4. The page: inputs, checkboxes, fan, parallel dispatch, columns.
  5. Computed contrast and check panels.
  6. Failure paths.

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.


10. Acceptance

The spike is accepted when the Operator can, locally:

  1. Enter four fields, check two or more readers, and generate.
  2. Read the pitch and confirm it would survive being read aloud to the prospect with them correcting it — no invented account of how they work today, no superlatives, no manufactured pain.
  3. Read the Foundation Document draft and confirm that everything stated as a commitment traces to something the Operator actually entered, and that everything the model inferred appears as an open question rather than a commitment.
  4. With all three new inputs empty, confirm the table, the landscape, and next steps are all absent rather than invented.
  5. With the current-approach input supplied and no alternatives named, confirm the table appears with three columns and the landscape does not appear at all.
  6. With alternatives named, confirm the landscape appears, says nothing about a product beyond what was entered about it, and foregrounds any alternative the entered text places on Loomworks's own ground.
  7. Confirm every dimension in the table traces to something entered in the four original inputs, and that a dimension with no sourced current-approach cell refuses rather than being suppressed or invented.
  8. See rendered readers that select different material — not the same material in different registers.
  9. See a contrast panel computed from the selection diff, naming findings one reader got and another did not.
  10. See a check panel listing every inferred finding that reached the output, and stating plainly that it does not cover specifics introduced during rendering.
  11. Uncheck all readers and get a single plain column.
  12. Confirm nothing was saved.

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.


11. Handoff must report


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Example Generator — Sales Perspective Spike — CR-2026-152 — v0.26 — 2026-07-17