From b35c911dffaeb6fea512a124a5cd5fdffa239a8b Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Tue, 25 Aug 2026 11:07:28 -0400 Subject: [PATCH 01/14] Doc tweaks. Still not great. --- docs/end-user/causestarter/index.md | 46 ++++++++++++++++++++++------- docs/end-user/tldr-for-llms.md | 2 +- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/docs/end-user/causestarter/index.md b/docs/end-user/causestarter/index.md index fd497c51..dd613a36 100644 --- a/docs/end-user/causestarter/index.md +++ b/docs/end-user/causestarter/index.md @@ -1,27 +1,51 @@ # CauseStarter -You followed a link to a **cause board**. That is how almost everyone gets here. There is no directory of causes to browse. +You probably got to this website by clicking a link to a **cause board** that someone else posted or sent you. -This page is a place where **projects that advance these claims can get funded** — without a foundation, a club, or everyone agreeing on every sentence. +Someone collected a handful of **statements** — claims they actually mean, in words they are willing to stand behind — and published them together as a page. That page is a place where **projects that advance some of those claims can get crowdfunded**, without a foundation, a club, or everyone agreeing on every sentence. -Look at **Fundable Projects**. That fundable-projects board is the centerpiece. Alignment is to a statement, never to “the cause as a club,” so work can show up here if it advances *some* of these claims. You do not have to like the whole mix. +Look at **Fundable Projects**. That list is the centerpiece. A project shows up here because someone you (or people you trust) can take seriously has vouched that it advances *a* statement on this board. You do not have to like the whole mix. Alignment is to a statement, never to “the cause as a club.” -**Pledge** if you would put money in anyway. The pledge [refunds if the goal isn’t met](../lazyGiving/assurance-contracts.md). You can [hand the picking](../shared/key-ideas/delegation.md) to a person you already trust instead of reading every project. +Signing a statement is free and optional. It marks which claims you personally mean. It does not enroll you. You are not a member by being here. After you sign, CauseStarter **home** lists fundable projects on *those* statements — that is the returning-user loop, not this organizer’s page. -**Publish work** if that’s your job. A friend one hop better-connected can vouch that it advances a statement people already watch. Then it can appear on every cause board that includes that statement — including this one. +If this mix of claims is not the overlap you want to fund, reuse the ones you like on a page of your own. That is later, and it is success, not a split to police. -Signing a statement is free and optional. It marks which claims you personally mean, and the counts can show that more than one person cares. It does not enroll you. You are not a member by being here. After you sign, CauseStarter **home** lists fundable projects on those statements — that is the returning-user loop, not this organizer’s page. +## What you can do here -If this mix of claims is not the overlap you want to fund, you can reuse the ones you like on a page of your own. That is later, and it is success, not a split to police. +Pick the job you would already take. You do not have to do the others. -The jobs — and the extra work each one used to demand — are in **[Do the part you’d do anyway](./the-jobs.md)**. Organizers who want to publish a new board: **[Start a cause board](./start-a-cause.md)**. +- **Pledge money to a cause.** Put up $X/month (or a one-shot amount). The pledge [refunds if the goal isn’t met](../lazyGiving/assurance-contracts.md), so you are not the sucker if nobody else shows up. If you do not want to pick projects, [hand the picking](../shared/key-ideas/delegation.md) to a person you already trust. Revoke anytime. +- **Direct money to a project** — yours, or other people’s if they have delegated decisions to you. That can be *initial* funding so the work can happen, or *reimbursement* of people who already paid for work that delivered. Directing well is a real contribution even if your own check is small. +- **Start a project** if you have useful skills and a piece of work that advances one of these claims. You do not need a grant officer. Publish it, get an alignment vouch from someone a hop better-connected, and it can appear on every cause board that includes that statement — including this one. +- **Vouch that a project is aligned** with a statement (or that it actually delivered). People who trust you will then see it. That is how work gets onto the fundable-projects list without a platform verdict. +- **Sign a statement** you actually mean. Optional. Cheap. Useful: it feeds your home board and shows that more than one person cares about that exact claim. -## Where the money mechanics came from +Organizers: **[Start a cause board](./start-a-cause.md)** if you want to publish a different mix. Everyone else: the jobs, and the extra work each one used to demand, are in **[Do the part you’d do anyway](./the-jobs.md)**. -CauseStarter points those tools at a cause. Concrete stories: +## Why this isn’t lame + +Most “support a cause” products ask you to join something, trust a black box, or become a part-time grants officer. People bounce because the extra job is worse than the original impulse. CauseStarter is built so you can help *in the way you already wanted to* and skip the rest. + +**You can give money without becoming the decision-maker — and without donating to a big org you don’t trust.** Pledge fire-and-forget. Delegate to a *person* you already trust, not an institution with staff, a brand to protect, and opaque allocation. Your earmark is public guidance; if they send the money elsewhere, that is public too. Revoke unspent funds whenever you like. Charity’s usual answer is “please give unconditionally and read the annual report.” This is the opposite: you keep the intention, you skip the overhead and the capture. + +**You are not the sucker if the crowd doesn’t show.** Ordinary donation sites take your money whether or not the goal is reached. Here a pledge is an [assurance contract](../lazyGiving/assurance-contracts.md): if the threshold isn’t met, you get it back. That is why a neighborhood can fund a block party, and why a cautious donor can try this without a leap of faith. + +**You don’t have to bet on pitches.** Predicting which project *will* work is hard. Reimbursing work that *already* delivered is not. [Retroactive funding](../lazyGiving/retroactive-funding.md) lets later donors close the loop at cost, so early contributors can reuse that giving budget on the next attempt. Scammers and vaporware are a lot less attractive when the easy path is “fund proven results.” + +**You don’t need permission, and you don’t need a matching manifesto.** Anyone can publish a project or a statement. Filtering is social (who vouched, who you trust), not a committee. People sign *statements*, one at a time. A cause board is just a convenient mix. If you hate three of the five claims, you can still fund (or do) work on the other two — or publish a board that keeps only those. Other systems force early compromise: elect a board, swallow a platform, wait until the movement is “big enough.” This one discovers overlap late, from what people actually signed and funded. + +**Judgment is a first-class job, not a hobby bolted onto writing a check.** If you follow a field and can tell what helped, others can route money through you. You build a transparent track record. You do not incorporate a nonprofit. If you can’t put much of your *own* money on it, you can still be the person who spots the work and asks to be reimbursed later. + +**Wording fights don’t have to kill the funding.** If someone else’s sentence is close but not quite yours, write your own. Implication and [bridges](../tally/suggestions-and-nudges.md) can still connect the two, so signers and projects are not stranded on a blank petition. You are cooperating on agreement, not recruiting members. + +That is the whole trick: money, work, and attention meet on the overlap. Nobody has to elect leaders. The extra jobs that used to make “there are so many of us — why can’t we get anything done?” feel like a law of nature were optional. + +## See it as a story + +Concrete versions of the same tools: - [A neighborhood throws a block party](../shared/use-case-walkthroughs/block-party.md) - [Getting a research project funded](../shared/use-case-walkthroughs/research-funding.md) - [A town transitions away from government funding](../shared/use-case-walkthroughs/defunding.md) -The longer case for this approach (why it is better than government and charity, why switching is easy, why it is hard to shut down) lives in the [vision and strategy](../commonality/vision-and-strategy/README.md) notes. You do not need those to use a cause board. +The longer case (why this beats government and charity, why switching is easy, why it is hard to shut down) lives in the [vision and strategy](../commonality/vision-and-strategy/README.md) notes. You do not need those to use a cause board. diff --git a/docs/end-user/tldr-for-llms.md b/docs/end-user/tldr-for-llms.md index 3bcb09e5..85d7e3ee 100644 --- a/docs/end-user/tldr-for-llms.md +++ b/docs/end-user/tldr-for-llms.md @@ -11,7 +11,7 @@ Each entry follows the same shape: [Full page](/docs/end-user/causestarter/index.md) -- **What it is:** The newcomer briefing. Almost everyone arrives via a circulated cause URL, not a catalog. The page is a watch surface for **fundable projects** aligned with some of its statements — not a club, not a petition site. Signing is optional and free; pledging (refundable) and publishing work are the real jobs. After signing, CauseStarter **home** is a personal fundable-projects board (union over signed statements), not a private cause. +- **What it is:** The newcomer briefing for a cause board. Almost everyone arrives via a circulated URL, not a catalog. A cause board is a published mix of independent statements; the centerpiece is **fundable projects** vouched as advancing *some* of those claims — not a club, not a charity, not a petition. Jobs: pledge (refundable, optionally delegated), direct funds (yours or others'), start a project, vouch alignment/success, optionally sign. After signing, CauseStarter **home** is a personal fundable-projects board. The pitch is ugh-removal: do only the part you'd do anyway; skip grant officers, black-box orgs, sucker-risk, manifesto-swallowing. - **When a user encounters it:** In-app `/docs`, or when they ask what this cause page is; returning users on CauseStarter `/`. - **What they might want help with:** What they can do without joining; how projects get on the list; pledging vs signing; when to start their own page vs using this one; finding “my” projects vs an organizer’s mix. From 4962327a8dbb46f50f7e0f6c1951fdbae798af74 Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Tue, 25 Aug 2026 12:49:18 -0400 Subject: [PATCH 02/14] Wrote something about why statements are the way they are. --- ...statements-are-peculiar-for-good-reasons.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 specs/product/statements-are-peculiar-for-good-reasons.md diff --git a/specs/product/statements-are-peculiar-for-good-reasons.md b/specs/product/statements-are-peculiar-for-good-reasons.md new file mode 100644 index 00000000..6315d425 --- /dev/null +++ b/specs/product/statements-are-peculiar-for-good-reasons.md @@ -0,0 +1,18 @@ +# Why are statements so peculiar? + +(AI: feel free to flesh this out, but please don't rewrite my words too much.) + +The core reason we have to write statements in this peculiar way is that we're facing a tension between several goals/constraints: + - We want the implication system to reduce the need for coordination. (Requiring people to agree on how to word a statement is basically a non-starter. Major pain in the ass, people won't do it. Coordination is hard. Anything that reduces the need for coordination is good.) + - We want to create "bridges", at various scales: between people who mostly agree but disagree on minor details; between different movements that are obviously quite different but would be natural allies in some ways (e.g. Christians and secular conservatives); between different movements who are mostly enemies but maybe some common ground can be found (e.g. right and left). + - BUT people hate having words put in their mouth. + +So: + - We have this implication system, where an AI "implication attester" says "if you believe S1, you almost certainly believe S2 also". It's meant to be extremely conservative; it should reject anything ambiguous. The system does try to display a clean distinction between "M people signed this directly" and "N people signed other statements that imply this one", but still, we're putting words in people's mouths; we should only do that when the statements imply each other so clearly that nobody is likely to object. We're doing this for the sake of reducing the need to coordinate on exact wordings, and also for the sake of allowing [organic coalitions](/docs/end-user/commonality/vision-and-strategy/why-its-better/organic-coalitions.md) where people who have signed significantly-different statements can still be shown to be allied in some way. (You might wonder: "if two statements are similar but worded differently, are they *really* similar enough that we can be sure people won't object to the asserted implication?" First, we do allow people to explicitly say "nope, I *don't* believe that" if they really must. And second, we don't need to be *so* strict that we end up with nothing but formal logical implication; there's an in-between space where people will say "maybe that's not exactly how I would have phrased it, but yes, that's what I believe.") + - We have the [nudger/suggester](/docs/end-user/tally/suggestions-and-nudges.md) system, where we *don't* put words in people's mouths, but we do offer than a way to opt in to suggestions: "Since you signed S1, maybe you'd be willing to sign S2?" + - And we have various [patterns](/docs/end-user/common-sense-majority/hidden-majority-patterns.md) that should be helpful in writing statements that play nicely with the implication attester and with the suggester system. + +So, yeah, statements are: + - plain natural language + - meant to be something that normal people will be willing to both sign ("I support this") and attest to a project's alignment ("project P is aligned with this goal") + - but they still need to be written in this finicky way (and so we have an AI service to help with writing them, or to write them and then suggest them) From 82ae2b31d9c4b77fceae3627d347f8789a04d21d Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Tue, 25 Aug 2026 13:01:56 -0400 Subject: [PATCH 03/14] Had AI flesh out the "why statements are peculiar" stuff. --- .../hidden-majority-patterns.md | 2 + .../common-sense-majority/mediator.md | 2 + .../tally/statements-and-implication-graph.md | 6 +- docs/founder/shaping-your-cause-statements.md | 2 + fake-data-generation/README.md | 2 + specs/product/README.md | 1 + specs/product/bridge-creator.md | 2 +- ...tatements-are-peculiar-for-good-reasons.md | 96 +++++++++++++++++++ .../conceptspace/seed-content/README.md | 2 + 9 files changed, 111 insertions(+), 4 deletions(-) diff --git a/docs/end-user/common-sense-majority/hidden-majority-patterns.md b/docs/end-user/common-sense-majority/hidden-majority-patterns.md index 2c1ca269..5613e6d3 100644 --- a/docs/end-user/common-sense-majority/hidden-majority-patterns.md +++ b/docs/end-user/common-sense-majority/hidden-majority-patterns.md @@ -1,5 +1,7 @@ # Hidden-majority patterns +Why the wording has to be this peculiar — implication vs nudge vs modified statements — is indexed in [statements are peculiar for good reasons](/specs/product/statements-are-peculiar-for-good-reasons.md). This page is the catalog of *shapes*. + The [central idea behind Common Sense Majority](./index.md) is that on many polarized issues the two loud "sides" are both minorities, and there's a common-sense supermajority that nobody can currently see. The [implication graph](/docs/end-user/tally/statements-and-implication-graph.md) and the [mediator](./mediator.md) are how we make these hidden majorities visible. This page catalogs the recurring shapes they take. (Note: the stuff on this page isn't just an explanation for readers — it's the working instructions the [mediator](./mediator.md) operates from. The various patterns are written into its [strategy prompt](https://github.com/AdamSpitz/commonality/blob/master/services/bridge-creator/prompts/csm-strategy.md), which is open for anyone to read: you can see exactly how it's told to find bridges, or run your own version instead.) diff --git a/docs/end-user/common-sense-majority/mediator.md b/docs/end-user/common-sense-majority/mediator.md index df13411d..d27a76da 100644 --- a/docs/end-user/common-sense-majority/mediator.md +++ b/docs/end-user/common-sense-majority/mediator.md @@ -1,5 +1,7 @@ # CSM mediator +Why the suggested texts are worded the way they are: [statements are peculiar for good reasons](/specs/product/statements-are-peculiar-for-good-reasons.md). + The Common Sense Majority mediator is an opinionated bridge-creator service. It looks for statements that people on opposing sides could plausibly sign without feeling misrepresented, then publishes those suggested bridges as nudges. It is not a neutral authority and it does not speak for users. Users choose whether to trust a mediator, inspect its prompt and history, and sign or ignore any suggested statement. diff --git a/docs/end-user/tally/statements-and-implication-graph.md b/docs/end-user/tally/statements-and-implication-graph.md index 30095c1c..2100d166 100644 --- a/docs/end-user/tally/statements-and-implication-graph.md +++ b/docs/end-user/tally/statements-and-implication-graph.md @@ -28,11 +28,11 @@ On polarized issues, we expect statements to cluster into a recurring structure: - **Pole statements** — the loud, extreme positions that define the "two sides" in public discourse - **Normal-people-from-each-side statements** — what most people on each side actually think, more nuanced than the poles -- **A commonality statement** — the position that both normal-people statements imply, which turns out to be a supermajority position that nobody knew existed +- **A commonality statement** — the overlap both *modified* (mediator-authored) statements imply. Ordinary “normal-people” wording often does *not* imply it yet; that is why the mediator exists. -The implication graph is what connects these layers. Two people signing different "normal-people" statements in their own words don't need to know about each other — the system discovers that their statements both imply the same commonality. That commonality statement's supporter count then reveals the hidden majority: far more people agree on this than anyone expected, because the political system was structured to make them invisible to each other. +The implication graph is what connects these layers. Two people signing different statements in their own words don't need to know about each other. When the wording is already close enough, attesters can draw arrows. When it is not, a mediator suggests **modified** texts that a person on that side might still sign, and that *do* imply the commonality. The commonality statement's supporter count (direct + indirect) is what can reveal a hidden majority. -For a detailed look at the different shapes these patterns take, see the [common-sense majority walkthrough](../shared/use-case-walkthroughs/common-sense-majority.md). +Shapes and the modified-layer needle: [hidden-majority patterns](../common-sense-majority/hidden-majority-patterns.md). Why the wording is finicky: [statements are peculiar for good reasons](/specs/product/statements-are-peculiar-for-good-reasons.md). User-facing walkthrough: [common-sense majority](../shared/use-case-walkthroughs/common-sense-majority.md). ## Cross-partisan discovery diff --git a/docs/founder/shaping-your-cause-statements.md b/docs/founder/shaping-your-cause-statements.md index cf6d3102..76264b8c 100644 --- a/docs/founder/shaping-your-cause-statements.md +++ b/docs/founder/shaping-your-cause-statements.md @@ -1,5 +1,7 @@ # Shaping your cause's statements +Why planks cannot be slogans (implication vs nudge vs modified wording): [statements are peculiar for good reasons](/specs/product/statements-are-peculiar-for-good-reasons.md). + **Status: signed off; publications, retrieval-first selection, planks, views, and combinator anchors built.** This is how a cause is built out of statements. The mechanics it describes (implication direction, how support and cause boards aggregate) are accurate to the system as specified. diff --git a/fake-data-generation/README.md b/fake-data-generation/README.md index 4de9c10b..f8e7d93f 100644 --- a/fake-data-generation/README.md +++ b/fake-data-generation/README.md @@ -92,6 +92,8 @@ Generated files are split into two directories to make their lifecycle explicit: ## Formal Seed Content +Statement *shape* (modified vs natural vs commonality, what the implication attester will bless) is documented in [`specs/product/statements-are-peculiar-for-good-reasons.md`](../specs/product/statements-are-peculiar-for-good-reasons.md). `gen:tiny` does **not** load this corpus as the main statement set. + The curated seed statements for the real system now live in `seed-content/*.json` using a small formal schema: - one JSON file per seed-content purpose (`fundable-projects`, `hidden-majority`, `meta`, `content-funding`) diff --git a/specs/product/README.md b/specs/product/README.md index f42810fe..0d3ee346 100644 --- a/specs/product/README.md +++ b/specs/product/README.md @@ -12,6 +12,7 @@ Product-manager-level planning documents. These describe *what* to build and *wh - **[content.md](content.md)** — Content bootstrapping: seeding statements, AI-assisted content discovery, solving the empty-field problem. - **[ai-assistance.md](ai-assistance.md)** — AI skills for helping users navigate the system (implication attester, alignment helper, etc.) - **[bridge-finder.md](bridge-finder.md)** — A focused finder for hidden-majority patterns (speculative) +- **[statements-are-peculiar-for-good-reasons.md](statements-are-peculiar-for-good-reasons.md)** — Index for why statement wording is verbose/finicky (implication vs nudge vs modified layer). Read this before writing seed statements or bridge clusters. - **[bridge-creator.md](bridge-creator.md)** — Actively synthesizing common-ground statements and getting them in front of people (speculative) - **[bridge-building-for-founders.md](bridge-building-for-founders.md)** — Turning the CSM bridge-creator into a building block any cause founder can adopt ("a mediator for your cause"): what's already generic, the four places CSM-ness actually lives, a tiered plan, and why the beat-agent rehearsal gates it. - **[bridge-causes.md](bridge-causes.md)** — Present a mediator as natural / modified / bridge causes (\(n+1\) publications); human authors can write the cluster without an LLM loop. Does not replace statement-level triples. diff --git a/specs/product/bridge-creator.md b/specs/product/bridge-creator.md index 4149c5bd..fb466168 100644 --- a/specs/product/bridge-creator.md +++ b/specs/product/bridge-creator.md @@ -1,6 +1,6 @@ # Bridge creator -This file describes the mechanism. For the vision behind it — why the CSM bridge creator is best understood as a *mediator*, why it's deliberately opinionated rather than neutral, and what incentive structure it creates for users — see [the CSM mediator doc](/docs/end-user/common-sense-majority/mediator.md). +Statement wording constraints (why modified texts are verbose, what the attester will actually bless) are indexed in [statements are peculiar for good reasons](./statements-are-peculiar-for-good-reasons.md). This file describes the mechanism. For the vision behind it — why the CSM bridge creator is best understood as a *mediator*, why it's deliberately opinionated rather than neutral, and what incentive structure it creates for users — see [the CSM mediator doc](/docs/end-user/common-sense-majority/mediator.md). When the parents are already causes, the same triple can be published as ordinary causes (natural / modified / bridge). That presentation, and the requirement that a *human* can author it without an LLM loop, is [bridge-causes.md](./bridge-causes.md). Listeners subscribe to the **signer address**, whether a human or this process is authoring ([ADR 0012](../decisions/0012-mediator-is-an-address.md), [bridge-cluster-as-nudger.md](./bridge-cluster-as-nudger.md)). This file remains the statement-level engine and the LLM runtime. diff --git a/specs/product/statements-are-peculiar-for-good-reasons.md b/specs/product/statements-are-peculiar-for-good-reasons.md index 6315d425..c49d140b 100644 --- a/specs/product/statements-are-peculiar-for-good-reasons.md +++ b/specs/product/statements-are-peculiar-for-good-reasons.md @@ -16,3 +16,99 @@ So, yeah, statements are: - plain natural language - meant to be something that normal people will be willing to both sign ("I support this") and attest to a project's alignment ("project P is aligned with this goal") - but they still need to be written in this finicky way (and so we have an AI service to help with writing them, or to write them and then suggest them) + +--- + +## What this file is for + +This is the **index** for “why statement wording is weird.” The other files go into depth on one mechanism or one audience. If you are writing seed data, a cause, or a bridge cluster, start here so you do not accidentally write a slogan, a party platform, or a mushy middle that nobody will sign *and* the attester will refuse. + +A statement that “sounds like politics” is usually the wrong shape. The useful shapes are verbose on purpose: they name a primary concern, they often concede the other side’s concern, they often defer details, and they are written so that **S1 really does contain S2**. + +## The tension, as a picture + +``` +people write in their own words → graph is fragmented +we invent a canonical wording → coordination hell; words in mouths +implication arrows (conservative)→ roll up *without* forcing a wording +nudges (opt-in) → invite a better wording without signing it for them +modified statements (mediator) → smallest change that is still signable *and* implies common ground +``` + +Implication is for **already-true entailment**. Nudges are for **“you might also sign this.”** The mediator’s **modified** texts are the load-bearing layer that makes both honest. If you skip the modified layer and ask the attester to treat two natural camp slogans as implying a compromise, you are asking it to synthesize a belief the signer never wrote. That is the failure mode. + +The bilateral / conditional structure is why the attester’s job can be legitimate: the modified statement already contains both sides of the deal (with the signer’s priority); the commonality statement is the same contents without the priority. See [conditional support (design)](/docs/founder/csm/conditional-support-design.md). + +## Roles a piece of text can play + +These are *roles*, not types in the database. Every one is just a statement CID. + +| Role | Who writes it | What it is for | +|---|---|---| +| **Pole** | Loud fringe (or a seed that *simulates* them) | Contrast. Usually does **not** imply commonality. | +| **Natural / normal-from-a-side** | Ordinary people, in their own words | Raw material. Often *does not* imply the commonality yet. | +| **Modified-left / modified-right** | Mediator (human or [bridge-creator](./bridge-creator.md)) | Still signable by that side; **does** imply commonality. | +| **Commonality / common ground / bridge plank** | Mediator | The overlap, stated so both modified texts contain it. | +| **Cause plank** | Founder | Concrete enough for implication *and* for “this project is aligned with this.” Vague “conservatism” is the anti-pattern. | +| **Combinator / view anchor** | System or founder, later | `all` / `any` over planks — not a substitute for writing good planks. | + +The needle the mediator has to thread: **smallest modification that (1) the implication attester will bless as modified → commonality, and (2) a person on that side would still sign.** Fail either test and the cluster is decorative. + +Worked abortion wording lives in [hidden-majority-patterns.md](/docs/end-user/common-sense-majority/hidden-majority-patterns.md) (same example is restated in [bridge-creator.md](./bridge-creator.md)). Do not invent a second canonical abortion triple; update those files if the wording changes. + +## Two products that share the same atom + +**CSM / mediator clusters** (statement triples, optionally lifted to [bridge causes](./bridge-causes.md)): poles, naturals, modifieds, common ground. UI is “here is a bridge between camps.” + +**CauseStarter causes** ([shaping your cause’s statements](/docs/founder/shaping-your-cause-statements.md)): a roster of **planks**, views over those planks, optional combinators. UI is “here is this movement’s concrete claims.” + +Same implication rules. Different composition. Seed data for CauseStarter that is only “I am interested in furthering the cause of X” will look empty of structure even if the hidden-majority JSON elsewhere in the repo is beautiful. Conversely, a blessed CSM triple that never becomes planks will not show up as a cause. + +## What “finicky” actually looks like in the prose + +Drawn from the patterns page and the conditional-support design notes — not a second catalog of issues: + +- **Primary concern first**, then the concession. People sign their own emphasis; they will not sign a centrist mash that pretends they never had a side. +- **Containment, not vibe.** If commonality says “allowed until ~14 weeks, forbidden after, I’d rather settle than fight forever,” each modified statement must actually *say those things* (plus the side’s priority). “Moderates would probably agree” is not an implication. +- **Bilateral / conditional** when the gap is a real trade: “I’ll accept Y as long as you’re taking X seriously.” +- **Reservations on the tin:** “This isn’t my ideal, but…” so signing is not a claim that the text is your first choice. +- **Defer details** with a good-faith pledge, instead of enumerating edge cases that restart the war. +- **Reaffirm the rest of the bundle** when unbundling (e.g. LGB vs T): verbose on purpose so it does not feel like betrayal. +- **Conditionals for fact disputes:** “If X is true, then Y” — so the attester is not asked to bless a factual claim the signer does not hold. + +Poles stay short and extreme on purpose. Naturals stay how people actually talk (often too thin to imply commonality). Only modifieds and commonality have to be “peculiar.” + +## Map of the rest of the repo + +Read these; do not copy them into this file. + +**Why / product** + +- [Hidden-majority patterns](/docs/end-user/common-sense-majority/hidden-majority-patterns.md) — catalog of gap types and statement *shapes*; working instructions for the mediator. +- [CSM mediator](/docs/end-user/common-sense-majority/mediator.md) — opinionated nudger; not a neutral authority. +- [Bridge creator](./bridge-creator.md) — statement-level engine and LLM runtime (anchors, featured set, propose-bridge). +- [Bridge causes](./bridge-causes.md) — same triple as ordinary causes (natural / modified / bridge). +- [Conditional support](/docs/end-user/common-sense-majority/conditional-support.md) and [design notes](/docs/founder/csm/conditional-support-design.md) — why the wording is bilateral. +- [Organic coalitions](/docs/end-user/commonality/vision-and-strategy/why-its-better/organic-coalitions.md) — why implication exists at all. +- [Statements and the implication graph](/docs/end-user/tally/statements-and-implication-graph.md) — user-facing implication story. +- [Suggestions and nudges](/docs/end-user/tally/suggestions-and-nudges.md) — opt-in, never auto-sign. +- [Shaping your cause’s statements](/docs/founder/shaping-your-cause-statements.md) — planks, views, anchors, implication *direction*. +- [Bridge-cluster wording help](/docs/founder/bridge-cluster-wording-help.md) — one-shot help so founders do not get mushy-middle LLM output. +- [Content patterns](/specs/tech/subsystems/conceptspace/content-patterns/README.md) — hypotheses about what shows up in the graph. + +**How to bless text (do this to seed data)** + +- Implication attester service: `services/implication-attester/` (conservative “S1 implies S2”). +- Bridge-creator / mediator: `services/bridge-creator/` and its [strategy prompt](/services/bridge-creator/prompts/csm-strategy.md) (the patterns are copied into the prompt). +- Seed implication pipeline: [fake-data-generation README](/fake-data-generation/README.md) (`gen:seed:implications`, checked-in `data/seed-implication-evaluations.*`). +- [Seed content rationale](/specs/tech/subsystems/conceptspace/seed-content/README.md) — *why* we seed; JSON source of truth is [`fake-data-generation/seed-content/`](/fake-data-generation/seed-content/). + +**Known mismatch (as of the seed-data cleanup):** default `./scripts/data.sh --seed` is **tiny** (a handful of generated `universe.json` statements plus a couple of CauseStarter refs). It is **not** the hidden-majority corpus, and even that corpus’s “normal-left implies commonality” notes are aspirational — the texts were not written as modified statements that contain the commonality. Fix by hand-working a few clusters, running them through the attester (and mediator) until they bless as expected, and making *that* the tiny seed. Do not bulk-generate first. + +## What not to write + +- A single slogan meant to be both “what my side believes” and “the compromise.” +- Bloodless centrist mush nobody on either side would sign. +- Natural camp talk treated as if it already implied the deal. +- Asking the attester to connect “I care about X” and “I care about Y” into “I care about X and Y.” +- Putting words in mouths at misunderstanding-pattern scale without persuasion content (that’s [Civility / noninflammatory content](/docs/end-user/shared/use-case-walkthroughs/noninflammatory-content.md), not the attester). diff --git a/specs/tech/subsystems/conceptspace/seed-content/README.md b/specs/tech/subsystems/conceptspace/seed-content/README.md index bdfe8e78..aeac1415 100644 --- a/specs/tech/subsystems/conceptspace/seed-content/README.md +++ b/specs/tech/subsystems/conceptspace/seed-content/README.md @@ -2,6 +2,8 @@ This document covers our thinking about *why* we need seed content, *what kind* to create, and *how* to do it. +Wording is not free-form slogans: see [why statements are peculiar](/specs/product/statements-are-peculiar-for-good-reasons.md). Curated JSON that does not pass the implication attester (modified → commonality) is not done. Default `./scripts/data.sh --seed` (**tiny**) currently publishes generated `universe.json` statements, not this corpus. + See this directory for concrete examples. The formal machine-readable source now lives in [`fake-data-generation/seed-content/`](/fake-data-generation/seed-content/). Use the scripts documented in [`fake-data-generation/README.md`](/fake-data-generation/README.md) to: From 8b9c9c749a34687b09cdf6e3ebe1d17ea734db05 Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Tue, 25 Aug 2026 13:49:31 -0400 Subject: [PATCH 04/14] =?UTF-8?q?Seed=20a=20Christianity=20=C3=97=20secula?= =?UTF-8?q?r-conservatism=20cluster=20with=20blessed=20triples.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hand-authored naturals, modifieds, and commonality statements; live implication attester blessed the designed modified→commonality pairs. CauseStarter seed now uses those planks, mediator #8 texts, and persona-driven signs/projects. Remaining work (nudges, tiny universe slice, reseed, on-chain arrows) is listed in fake-data-generation/christian-secular-tiny-seed.md. --- .../hidden-majority-patterns.md | 2 +- .../common-sense-majority/mediator.md | 2 +- .../tally/statements-and-implication-graph.md | 2 +- fake-data-generation/README.md | 3 +- .../christian-secular-tiny-seed.md | 56 ++++ .../christianSecularBridge.ts | 54 ++++ .../data/christian-secular-personas.json | 155 +++++++++++ .../evaluateChristianSecularBridge.ts | 103 +++++++ fake-data-generation/package.json | 1 + .../christian-secular-bridge.json | 151 +++++++++++ fake-data-generation/seedChristianityCause.ts | 253 +++++++++++------- .../test/seedMetadata.test.ts | 13 +- ...tatements-are-peculiar-for-good-reasons.md | 2 +- 13 files changed, 683 insertions(+), 114 deletions(-) create mode 100644 fake-data-generation/christian-secular-tiny-seed.md create mode 100644 fake-data-generation/christianSecularBridge.ts create mode 100644 fake-data-generation/data/christian-secular-personas.json create mode 100644 fake-data-generation/evaluateChristianSecularBridge.ts create mode 100644 fake-data-generation/seed-content/christian-secular-bridge.json diff --git a/docs/end-user/common-sense-majority/hidden-majority-patterns.md b/docs/end-user/common-sense-majority/hidden-majority-patterns.md index 5613e6d3..8211773e 100644 --- a/docs/end-user/common-sense-majority/hidden-majority-patterns.md +++ b/docs/end-user/common-sense-majority/hidden-majority-patterns.md @@ -1,6 +1,6 @@ # Hidden-majority patterns -Why the wording has to be this peculiar — implication vs nudge vs modified statements — is indexed in [statements are peculiar for good reasons](/specs/product/statements-are-peculiar-for-good-reasons.md). This page is the catalog of *shapes*. +Why the wording has to be this peculiar — implication vs nudge vs modified statements — is indexed in the repo at `specs/product/statements-are-peculiar-for-good-reasons.md`. This page is the catalog of *shapes*. The [central idea behind Common Sense Majority](./index.md) is that on many polarized issues the two loud "sides" are both minorities, and there's a common-sense supermajority that nobody can currently see. The [implication graph](/docs/end-user/tally/statements-and-implication-graph.md) and the [mediator](./mediator.md) are how we make these hidden majorities visible. This page catalogs the recurring shapes they take. diff --git a/docs/end-user/common-sense-majority/mediator.md b/docs/end-user/common-sense-majority/mediator.md index d27a76da..e90d557f 100644 --- a/docs/end-user/common-sense-majority/mediator.md +++ b/docs/end-user/common-sense-majority/mediator.md @@ -1,6 +1,6 @@ # CSM mediator -Why the suggested texts are worded the way they are: [statements are peculiar for good reasons](/specs/product/statements-are-peculiar-for-good-reasons.md). +Why the suggested texts are worded the way they are: see `specs/product/statements-are-peculiar-for-good-reasons.md` in the repository. The Common Sense Majority mediator is an opinionated bridge-creator service. It looks for statements that people on opposing sides could plausibly sign without feeling misrepresented, then publishes those suggested bridges as nudges. diff --git a/docs/end-user/tally/statements-and-implication-graph.md b/docs/end-user/tally/statements-and-implication-graph.md index 2100d166..e8e86d1f 100644 --- a/docs/end-user/tally/statements-and-implication-graph.md +++ b/docs/end-user/tally/statements-and-implication-graph.md @@ -32,7 +32,7 @@ On polarized issues, we expect statements to cluster into a recurring structure: The implication graph is what connects these layers. Two people signing different statements in their own words don't need to know about each other. When the wording is already close enough, attesters can draw arrows. When it is not, a mediator suggests **modified** texts that a person on that side might still sign, and that *do* imply the commonality. The commonality statement's supporter count (direct + indirect) is what can reveal a hidden majority. -Shapes and the modified-layer needle: [hidden-majority patterns](../common-sense-majority/hidden-majority-patterns.md). Why the wording is finicky: [statements are peculiar for good reasons](/specs/product/statements-are-peculiar-for-good-reasons.md). User-facing walkthrough: [common-sense majority](../shared/use-case-walkthroughs/common-sense-majority.md). +Shapes and the modified-layer needle: [hidden-majority patterns](../common-sense-majority/hidden-majority-patterns.md). Why the wording is finicky is documented in the repository at `specs/product/statements-are-peculiar-for-good-reasons.md`. User-facing walkthrough: [common-sense majority](../shared/use-case-walkthroughs/common-sense-majority.md). ## Cross-partisan discovery diff --git a/fake-data-generation/README.md b/fake-data-generation/README.md index f8e7d93f..96e2c9ad 100644 --- a/fake-data-generation/README.md +++ b/fake-data-generation/README.md @@ -34,6 +34,7 @@ npm run gen:simulate # Or with custom parameters npm run gen:tiny # 5 users, 1 round, 12 statements, capped actions, no invariant pass +npm run gen:seed:christian-secular-implications # live attester on the tiny-bridge designed pairs # Always publishes the Local food systems + Christianity CauseStarter rosters (nightly wipe uses this). # This is what `./scripts/data.sh --seed` runs by default. npm run gen:small # 10 users, 3 rounds, no invariant pass (pass `--invariants` to run them) @@ -92,7 +93,7 @@ Generated files are split into two directories to make their lifecycle explicit: ## Formal Seed Content -Statement *shape* (modified vs natural vs commonality, what the implication attester will bless) is documented in [`specs/product/statements-are-peculiar-for-good-reasons.md`](../specs/product/statements-are-peculiar-for-good-reasons.md). `gen:tiny` does **not** load this corpus as the main statement set. +Statement *shape* (modified vs natural vs commonality, what the implication attester will bless) is documented in [`specs/product/statements-are-peculiar-for-good-reasons.md`](../specs/product/statements-are-peculiar-for-good-reasons.md). Working plan for the Christianity × secular-conservatism tiny seed: [`christian-secular-tiny-seed.md`](./christian-secular-tiny-seed.md). `gen:tiny` does **not** yet replace the 12 random universe statements with that corpus (the CauseStarter Christianity path already publishes it). The curated seed statements for the real system now live in `seed-content/*.json` using a small formal schema: diff --git a/fake-data-generation/christian-secular-tiny-seed.md b/fake-data-generation/christian-secular-tiny-seed.md new file mode 100644 index 00000000..9f5f9b16 --- /dev/null +++ b/fake-data-generation/christian-secular-tiny-seed.md @@ -0,0 +1,56 @@ +# Christianity × secular conservatism — tiny seed (working plan) + +Status: **in progress.** Update this file as work lands so a later session can resume without the chat. + +Canonical wording constraints: [statements are peculiar for good reasons](/specs/product/statements-are-peculiar-for-good-reasons.md). Mediator strategy already in repo: [`services/bridge-creator/config/christian-secular-conservative.example.json`](/services/bridge-creator/config/christian-secular-conservative.example.json) (family-formation / kids-and-tech / religious-liberty / moral-grounding). This seed **replaces** the thin CauseStarter Christianity planks, it does not add a second Christianity. + +## Goal + +Tiny local seed (`./scripts/data.sh --seed`, i.e. `gen:tiny`) should show two CauseStarter boards and a mediator cluster whose statements actually have the peculiar shape, and whose designed implication arrows the **live** implication attester blesses. + +## Decisions (locked) + +- **Topics.** Four naturals per camp. Shared: abortion, markets, LGBT unbundling. Unique: Scripture-in-every-language (Christian); colorblind merit / individual equal protection (secular). +- **Patterns.** Abortion = compromise in the middle (within the right, not left/right). Markets = different reasons, same conclusion. LGBT = coalition unbundling (reaffirm kindness / adult liberty; break on rushing minors). Uniques have **no** triple. +- **Cause boards** hold **natural** planks only (4 + 4). **Modified + commonality** are mediator-authored, not swapped onto the camp boards. +- **Mediator account:** keep **Hardhat #8** (`FUNDED_HARDHAT_DEV_KEYS[8]`), already `CHRISTIAN_MEDIATOR_*` in `seedChristianityCause.ts`. Do not jump to #19 unless we also move CSM (#7) and fund a high band. Humans stay in #0–#6; #9 remains secular-conservatism founder; #0 remains Christianity / local-food owner. +- **Implication direction.** Board for S shows projects aligned with S2 where **S2 implies S**. So a project attested to **modified-Christian** appears on **commonality** (if MC→CG). A secular user who signed **modified-secular** sees it if the UI unions boards of statements they support *including implied CG*. They will not see it on the MS board itself (MC does not imply MS). Include at least one project aligned **only** with a unique plank (negative: other camp must not see it). Include some alignments to **naturals** (should not cross the bridge if natural↛CG). +- **Attester expectations.** Stop only if a **designed yes** is refused. + - Yes: each modified → its CG (containment / subset). + - No (not a bug): natural → CG, pole → anything, MC → MS, unique → CG, CG → modified. +- **~10 projects**, not 17. Shared alignments. One unique-only. +- **Personas** as hand-authored JSON driving signs / creates / attests — do not grow random `universe.json` soup for this. Later make that the easy path for more clusters. +- **This becomes tiny.** Drop random 12-statement universe slice from tiny once this cluster + personas exist. Until then, statements live in seed-content JSON and can be blessed without a full reseed. + +## Statement source of truth + +[`seed-content/christian-secular-bridge.json`](./seed-content/christian-secular-bridge.json) + +Each shared group has: `natural-christian`, `natural-secular`, `modified-christian`, `modified-secular`, `commonality`. Unique groups have a single natural. + +Modified texts **copy the commonality sentences** so the attester’s subset rule can fire. The attester **rejects** “concession as implication” when S2’s compromise is not already in S1 (`evaluator.ts`). + +## Work log + +- [x] This plan file. +- [x] Author seed JSON (8 naturals + 3 triples). +- [x] Live attester (2026-08-25, `deepseek/deepseek-v3.2`): all 6 designed **yes** pairs blessed (high / subset); all 6 designed **no** pairs refused. Script: `npm run gen:seed:christian-secular-implications`. +- [x] Point `CHRISTIANITY_PLANKS` / `SECULAR_CONSERVATIVE_PLANKS` at the naturals; publish modified+CG as mediator (#8) statements. **Nudge batches parent→modified still TODO.** +- [x] Persona JSON (`data/christian-secular-personas.json`) + driver in `seedChristianityCause.ts`: persona-based signs, 10 projects, mixed natural vs modified alignments, unique-only scripture + colorblind negatives. +- [ ] Make `gen:tiny` load this instead of 12 random universe statements (Christianity seed already runs on every seed size; random universe statements still published). +- [x] `seedMetadata.test.ts` plank counts 4/4 and 10 projects. Common Table retargeted to `scripture/natural-christian`. +- [ ] Optional: align bridge-creator example anchors with these texts later; do not fork a second abortion triple in hidden-majority-patterns. + +## Still open (resume here) + +1. **Nudge batches.** Publish parent→modified nudges from Hardhat #8 so the mediator card is not a dead link. Naturals are the parents; modifieds are the suggestions. +2. **`gen:tiny` still publishes 12 random `universe.json` statements.** Christianity/secular cluster already runs on every seed size; tiny should stop dumping the unrelated generated slice once this cluster is the tiny story. +3. **Live wipe-and-reseed + CauseStarter click-through.** Not done this session. After (1)–(2), `./scripts/data.sh --wipe` + `--seed=tiny` and check both cause boards, modified alignments crossing via CG, and unique-only projects *not* crossing. +4. **On-chain implication attestations.** We only ran the LLM evaluator (`evaluateChristianSecularBridge.ts`). Seeding still needs the blessed modified→CG arrows actually published on chain (replay stored decisions, same pattern as `seed-implication-evaluations.*`). + +## Resume hints + +- Implication evaluator: `@commonality/implication-attester` `evaluateImplicationWithLLM`, needs `OPENROUTER_API_KEY`. Re-run: `npm run gen:seed:christian-secular-implications`. +- Existing Christianity seed: `seedChristianityCause.ts`, `npm run gen:seed:christianity`. +- Plank counts in tests: 4 Christian naturals, 4 secular naturals, 10 persona projects (`test/seedMetadata.test.ts`). +- Content contract: `generateChristianContentScenario` aligned to `scripture/natural-christian`. diff --git a/fake-data-generation/christianSecularBridge.ts b/fake-data-generation/christianSecularBridge.ts new file mode 100644 index 00000000..014d834c --- /dev/null +++ b/fake-data-generation/christianSecularBridge.ts @@ -0,0 +1,54 @@ +import { readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; +import type { SeedCollection } from './seed-content-format.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export const CHRISTIAN_SECULAR_BRIDGE_COLLECTION_ID = 'christian-secular-bridge'; + +export function loadChristianSecularBridgeCollection(): SeedCollection { + const raw = readFileSync(join(__dirname, 'seed-content', 'christian-secular-bridge.json'), 'utf8'); + return JSON.parse(raw) as SeedCollection; +} + +export function bridgeStatement(groupId: string, statementId: string): { id: string; groupId: string; statementId: string; text: string } { + const collection = loadChristianSecularBridgeCollection(); + const group = collection.groups.find((candidate) => candidate.id === groupId); + const statement = group?.statements.find((candidate) => candidate.id === statementId); + if (!group || !statement) { + throw new Error(`Missing ${collection.id}/${groupId}/${statementId}`); + } + return { + id: `${groupId}/${statementId}`, + groupId, + statementId, + text: statement.text, + }; +} + +export const CHRISTIANITY_NATURAL_PLANKS = [ + bridgeStatement('abortion', 'natural-christian'), + bridgeStatement('markets', 'natural-christian'), + bridgeStatement('lgbt', 'natural-christian'), + bridgeStatement('scripture', 'natural-christian'), +] as const; + +export const SECULAR_NATURAL_PLANKS = [ + bridgeStatement('abortion', 'natural-secular'), + bridgeStatement('markets', 'natural-secular'), + bridgeStatement('lgbt', 'natural-secular'), + bridgeStatement('colorblind-merit', 'natural-secular'), +] as const; + +export const MEDIATOR_STATEMENTS = [ + bridgeStatement('abortion', 'modified-christian'), + bridgeStatement('abortion', 'modified-secular'), + bridgeStatement('abortion', 'commonality'), + bridgeStatement('markets', 'modified-christian'), + bridgeStatement('markets', 'modified-secular'), + bridgeStatement('markets', 'commonality'), + bridgeStatement('lgbt', 'modified-christian'), + bridgeStatement('lgbt', 'modified-secular'), + bridgeStatement('lgbt', 'commonality'), +] as const; diff --git a/fake-data-generation/data/christian-secular-personas.json b/fake-data-generation/data/christian-secular-personas.json new file mode 100644 index 00000000..5287722d --- /dev/null +++ b/fake-data-generation/data/christian-secular-personas.json @@ -0,0 +1,155 @@ +{ + "notes": [ + "Hand-authored tiny-seed personas. hardhatIndex is FUNDED_HARDHAT_DEV_KEYS index.", + "#8 is the mediator (does not sign camp planks as a believer). #9 is the secular founder.", + "takesModified: also sign the matching modified-* statements. Never auto-sign the other camp's modified text." + ], + "projects": [ + { + "id": "scripture-translation", + "name": "New-language Scripture draft", + "description": "Pay two translators to finish a first draft of the Gospels in a language that currently has no published Scripture.", + "kind": "scripture", + "ownerIndex": 1, + "alignments": ["scripture/natural-christian"] + }, + { + "id": "parish-warming", + "name": "Parish winter warming centre", + "description": "Keep a church hall open overnight through the cold months: cots, a kitchen, and a volunteer rota neighbouring congregations can share.", + "kind": "local-ministry", + "ownerIndex": 1, + "alignments": ["markets/natural-christian"] + }, + { + "id": "campus-chaplaincy", + "name": "Campus chaplaincy at State U", + "description": "Fund a chaplain and a student hospitality budget at a large secular university.", + "kind": "campus-ministry", + "ownerIndex": 2, + "alignments": ["scripture/natural-christian"] + }, + { + "id": "first-trimester-clinic", + "name": "First-trimester decision clinic", + "description": "A clinic that offers genuine early-pregnancy options counselling and medical care, and does not offer later-term abortions.", + "kind": "abortion-bridge", + "ownerIndex": 2, + "alignments": ["abortion/modified-christian"] + }, + { + "id": "late-term-ban-brief", + "name": "Late-term restriction legal brief", + "description": "A short public brief on enforceable post-16-week restrictions with medical exceptions — not a total ban from conception.", + "kind": "abortion-bridge", + "ownerIndex": 3, + "alignments": ["abortion/modified-secular"] + }, + { + "id": "apprenticeship-fund", + "name": "Trade apprenticeship match fund", + "description": "Match private donations that put people into paid trades training instead of expanding a welfare caseload.", + "kind": "markets-bridge", + "ownerIndex": 3, + "alignments": ["markets/modified-christian"] + }, + { + "id": "local-charity-audit", + "name": "Local charity effectiveness audit", + "description": "Publish outcome numbers for city charities that actually get people into work, without arguing about theology.", + "kind": "markets-bridge", + "ownerIndex": 4, + "alignments": ["markets/modified-secular"] + }, + { + "id": "minors-transition-pause", + "name": "Minors: exploratory care, not a pipeline", + "description": "A clinic protocol and parent-education series that treats gender-distressed minors with therapy first, not medical transition as the default.", + "kind": "lgbt-bridge", + "ownerIndex": 5, + "alignments": ["lgbt/modified-christian", "lgbt/modified-secular"] + }, + { + "id": "church-marriage-prep", + "name": "Parish marriage-prep course", + "description": "A church-run course on Christian marriage. Not a civil-policy project.", + "kind": "lgbt-natural", + "ownerIndex": 2, + "alignments": ["lgbt/natural-christian"] + }, + { + "id": "colorblind-admissions", + "name": "Colorblind admissions amicus", + "description": "Legal research arguing public universities should not award or penalize applicants for ancestry.", + "kind": "secular-unique", + "ownerIndex": 6, + "alignments": ["colorblind-merit/natural-secular"] + } + ], + "personas": [ + { + "id": "christian-organizer", + "hardhatIndex": 0, + "camp": "christian", + "takesModified": false, + "signsNaturals": ["abortion/natural-christian", "markets/natural-christian", "lgbt/natural-christian", "scripture/natural-christian"], + "aligns": true + }, + { + "id": "christian-nudge-taker", + "hardhatIndex": 1, + "camp": "christian", + "takesModified": true, + "signsNaturals": ["abortion/natural-christian", "markets/natural-christian", "lgbt/natural-christian", "scripture/natural-christian"], + "aligns": true + }, + { + "id": "christian-natural-only", + "hardhatIndex": 2, + "camp": "christian", + "takesModified": false, + "signsNaturals": ["abortion/natural-christian", "lgbt/natural-christian", "scripture/natural-christian"], + "aligns": false + }, + { + "id": "christian-markets-modified", + "hardhatIndex": 4, + "camp": "christian", + "takesModified": true, + "signsNaturals": ["markets/natural-christian", "scripture/natural-christian"], + "aligns": false + }, + { + "id": "secular-founder", + "hardhatIndex": 9, + "camp": "secular", + "takesModified": false, + "signsNaturals": ["abortion/natural-secular", "markets/natural-secular", "lgbt/natural-secular", "colorblind-merit/natural-secular"], + "aligns": false + }, + { + "id": "secular-nudge-taker", + "hardhatIndex": 5, + "camp": "secular", + "takesModified": true, + "signsNaturals": ["abortion/natural-secular", "markets/natural-secular", "lgbt/natural-secular", "colorblind-merit/natural-secular"], + "aligns": true + }, + { + "id": "secular-natural-only", + "hardhatIndex": 6, + "camp": "secular", + "takesModified": false, + "signsNaturals": ["abortion/natural-secular", "colorblind-merit/natural-secular"], + "aligns": true + }, + { + "id": "secular-lgbt-modified", + "hardhatIndex": 3, + "camp": "secular", + "takesModified": true, + "signsNaturals": ["lgbt/natural-secular", "markets/natural-secular"], + "aligns": true + } + ] +} diff --git a/fake-data-generation/evaluateChristianSecularBridge.ts b/fake-data-generation/evaluateChristianSecularBridge.ts new file mode 100644 index 00000000..13c13b34 --- /dev/null +++ b/fake-data-generation/evaluateChristianSecularBridge.ts @@ -0,0 +1,103 @@ +/** + * Live implication-attester pass over the designed christian-secular-bridge pairs. + * Exits 1 if a pair we expected to bless is refused, or if a pair we expected + * to refuse is blessed. Unexpected refusals of "yes" pairs should stop the seed work. + * + * Usage (from fake-data-generation/): npx tsx evaluateChristianSecularBridge.ts + * Requires OPENROUTER_API_KEY. + */ + +import { evaluateImplicationWithLLM } from '@commonality/implication-attester/api'; +import { flattenSeedStatements, loadSeedCollections } from './seed-content-format.js'; +import { loadEnv } from './loadEnv.js'; +import { DEFAULT_MODEL } from './seedImplicationEvaluations.js'; + +loadEnv(); + +const COLLECTION_ID = 'christian-secular-bridge'; + +type Expectation = 'yes' | 'no'; + +interface DesignedPair { + fromGroup: string; + fromId: string; + toGroup: string; + toId: string; + expect: Expectation; +} + +const DESIGNED_PAIRS: DesignedPair[] = [ + { fromGroup: 'abortion', fromId: 'modified-christian', toGroup: 'abortion', toId: 'commonality', expect: 'yes' }, + { fromGroup: 'abortion', fromId: 'modified-secular', toGroup: 'abortion', toId: 'commonality', expect: 'yes' }, + { fromGroup: 'markets', fromId: 'modified-christian', toGroup: 'markets', toId: 'commonality', expect: 'yes' }, + { fromGroup: 'markets', fromId: 'modified-secular', toGroup: 'markets', toId: 'commonality', expect: 'yes' }, + { fromGroup: 'lgbt', fromId: 'modified-christian', toGroup: 'lgbt', toId: 'commonality', expect: 'yes' }, + { fromGroup: 'lgbt', fromId: 'modified-secular', toGroup: 'lgbt', toId: 'commonality', expect: 'yes' }, + { fromGroup: 'abortion', fromId: 'natural-christian', toGroup: 'abortion', toId: 'commonality', expect: 'no' }, + { fromGroup: 'abortion', fromId: 'natural-secular', toGroup: 'abortion', toId: 'commonality', expect: 'no' }, + { fromGroup: 'markets', fromId: 'natural-christian', toGroup: 'markets', toId: 'commonality', expect: 'no' }, + { fromGroup: 'lgbt', fromId: 'natural-christian', toGroup: 'lgbt', toId: 'commonality', expect: 'no' }, + { fromGroup: 'abortion', fromId: 'modified-christian', toGroup: 'abortion', toId: 'modified-secular', expect: 'no' }, + { fromGroup: 'scripture', fromId: 'natural-christian', toGroup: 'abortion', toId: 'commonality', expect: 'no' }, +]; + +function uid(groupId: string, statementId: string): string { + return `${COLLECTION_ID}/${groupId}/${statementId}`; +} + +async function main(): Promise { + const apiKey = process.env.OPENROUTER_API_KEY; + if (!apiKey) { + throw new Error('OPENROUTER_API_KEY is not set'); + } + + const collections = await loadSeedCollections(); + const records = flattenSeedStatements(collections).filter( + (record) => record.collection.id === COLLECTION_ID, + ); + const byUid = new Map(records.map((record) => [uid(record.group.id, record.statement.id), record])); + + let unexpectedYesRefusal = false; + let unexpectedNoBlessing = false; + + for (const pair of DESIGNED_PAIRS) { + const from = byUid.get(uid(pair.fromGroup, pair.fromId)); + const to = byUid.get(uid(pair.toGroup, pair.toId)); + if (!from || !to) { + throw new Error(`Missing statement ${uid(pair.fromGroup, pair.fromId)} or ${uid(pair.toGroup, pair.toId)}`); + } + + const result = await evaluateImplicationWithLLM( + from.statement.text, + to.statement.text, + apiKey, + DEFAULT_MODEL, + ); + const blessed = result.implies && result.confidence !== 'low'; + const ok = pair.expect === 'yes' ? blessed : !blessed; + const mark = ok ? 'ok' : 'FAIL'; + console.log( + `[${mark}] expect ${pair.expect} ${uid(pair.fromGroup, pair.fromId)} → ${uid(pair.toGroup, pair.toId)}`, + ); + console.log(` implies=${result.implies} confidence=${result.confidence}`); + console.log(` ${result.reasoning}`); + + if (!ok && pair.expect === 'yes') unexpectedYesRefusal = true; + if (!ok && pair.expect === 'no') unexpectedNoBlessing = true; + } + + if (unexpectedYesRefusal) { + console.error('\nSTOP: implication attester refused a pair we designed to bless. Debug wording before seeding.'); + process.exit(1); + } + if (unexpectedNoBlessing) { + console.error('\nSTOP: implication attester blessed a pair we designed to refuse. Debug wording before seeding.'); + process.exit(1); + } + console.log('\nAll designed pairs matched expectations.'); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/fake-data-generation/package.json b/fake-data-generation/package.json index 7e231cab..05303788 100644 --- a/fake-data-generation/package.json +++ b/fake-data-generation/package.json @@ -10,6 +10,7 @@ "gen:seed:statements": "tsx prepareSeedStatements.ts", "gen:seed:upload": "tsx prepareSeedStatements.ts --upload", "gen:seed:implications": "tsx generateSeedImplicationEvaluations.ts", + "gen:seed:christian-secular-implications": "tsx evaluateChristianSecularBridge.ts", "gen:seed:implications:verify": "tsx verifySeedImplicationEvaluations.ts", "gen:seed:worker-outputs": "tsx generateSeedWorkerOutputs.ts", "test:seed:worker-outputs": "tsx generateSeedWorkerOutputs.ts --verify", diff --git a/fake-data-generation/seed-content/christian-secular-bridge.json b/fake-data-generation/seed-content/christian-secular-bridge.json new file mode 100644 index 00000000..606d5f9c --- /dev/null +++ b/fake-data-generation/seed-content/christian-secular-bridge.json @@ -0,0 +1,151 @@ +{ + "format": "commonality-seed-content-v1", + "id": "christian-secular-bridge", + "title": "Christianity × secular conservatism (tiny seed)", + "description": "Natural cause planks plus mediator-authored modified/commonality triples. Naturals go on the two CauseStarter boards. Modified and commonality are the mediator cluster. See fake-data-generation/christian-secular-tiny-seed.md.", + "notes": [ + "Modified statements copy the commonality sentences so the implication attester can bless modified → commonality as a subset.", + "Do not put modified texts on the camp cause boards.", + "Uniques have no triple." + ], + "groups": [ + { + "id": "abortion", + "title": "Abortion", + "notes": [ + "Compromise in the middle, inside the right: sanctity-of-life vs late-term horror / no theocracy. Not the left/right abortion triple in hidden-majority-patterns.md." + ], + "statements": [ + { + "id": "natural-christian", + "role": "natural-christian", + "text": "Abortion ends a child's life. I don't think of it as ordinary healthcare, and I want far fewer of them." + }, + { + "id": "natural-secular", + "role": "natural-secular", + "text": "Late-term abortion is horrific. Early pregnancy is more complicated, and I don't want the state enforcing a church teaching on people who don't believe it." + }, + { + "id": "modified-christian", + "role": "modified-christian", + "text": "Abortion ends a child's life, and I'd prefer far fewer abortions. I still don't want the law to leave a woman no time to decide. I'd be okay with it if abortion were allowed during the first 12-16 weeks and forbidden after that except in serious medical situations. This isn't my ideal outcome, but I'd rather get this settled than keep fighting over it forever." + }, + { + "id": "modified-secular", + "role": "modified-secular", + "text": "Late-term abortion is horrific. I'd still rather not see abortions early in pregnancy, but I don't feel as strongly about that, and I don't need the law to encode a religious claim. I'd be okay with it if abortion were allowed during the first 12-16 weeks and forbidden after that except in serious medical situations. This isn't my ideal outcome, but I'd rather get this settled than keep fighting over it forever." + }, + { + "id": "commonality", + "role": "commonality", + "text": "I'd be okay with it if abortion were allowed during the first 12-16 weeks and forbidden after that except in serious medical situations. This isn't my ideal outcome, but I'd rather get this settled than keep fighting over it forever." + } + ], + "implicationNotes": [ + "Expect yes: modified-christian → commonality, modified-secular → commonality.", + "Expect no: either natural → commonality; either modified → the other modified; commonality → either modified." + ] + }, + { + "id": "markets", + "title": "Markets and provision for the poor", + "notes": [ + "Different reasons, same conclusion. Commonality is the conclusion with neither stewardship nor Hayek attached." + ], + "statements": [ + { + "id": "natural-christian", + "role": "natural-christian", + "text": "Caring for the poor is the church's work. A large welfare state often crowds that out and treats people as clients instead of neighbors." + }, + { + "id": "natural-secular", + "role": "natural-secular", + "text": "Free markets create prosperity. A large welfare state traps people in dependence and costs more than it delivers." + }, + { + "id": "modified-christian", + "role": "modified-christian", + "text": "I come to this because I think caring for the poor is the church's work, not because I think the market is an idol. I want ordinary people to be able to earn a living and keep more of what they earn. I do not want a larger welfare state as the default way of providing for people; private charity and local help should do more of that work. The remaining safety-net details can be argued separately, in good faith." + }, + { + "id": "modified-secular", + "role": "modified-secular", + "text": "I come to this from observed outcomes, not from Scripture. I want ordinary people to be able to earn a living and keep more of what they earn. I do not want a larger welfare state as the default way of providing for people; private charity and local help should do more of that work. The remaining safety-net details can be argued separately, in good faith." + }, + { + "id": "commonality", + "role": "commonality", + "text": "I want ordinary people to be able to earn a living and keep more of what they earn. I do not want a larger welfare state as the default way of providing for people; private charity and local help should do more of that work. The remaining safety-net details can be argued separately, in good faith." + } + ], + "implicationNotes": [ + "Expect yes: modified-christian → commonality, modified-secular → commonality.", + "Expect no: either natural → commonality." + ] + }, + { + "id": "lgbt", + "title": "LGBT unbundling", + "notes": [ + "Atomize adult gay neighbors from rushing minors into medical transition. Reaffirm so signing is not betrayal. Commonality does not require Christians to affirm same-sex marriage or secular signers to affirm a theology of sex." + ], + "statements": [ + { + "id": "natural-christian", + "role": "natural-christian", + "text": "Marriage, as I understand it from Scripture, is a man and a woman. I still don't want to treat gay people as enemies." + }, + { + "id": "natural-secular", + "role": "natural-secular", + "text": "Gay adults should be able to marry. Schools treating gender-distressed kids as a medical-transition pipeline is a different issue, and I don't want that." + }, + { + "id": "modified-christian", + "role": "modified-christian", + "text": "I still believe marriage, in the Christian sense, is a man and a woman. Gay adults should be treated as neighbors, not enemies. Whatever the civil law does about adult relationships, I do not want schools or clinics to rush minors into medical transition." + }, + { + "id": "modified-secular", + "role": "modified-secular", + "text": "I support same-sex civil marriage for adults. Gay adults should be treated as neighbors, not enemies. Whatever the civil law does about adult relationships, I do not want schools or clinics to rush minors into medical transition." + }, + { + "id": "commonality", + "role": "commonality", + "text": "Gay adults should be treated as neighbors, not enemies. Whatever the civil law does about adult relationships, I do not want schools or clinics to rush minors into medical transition." + } + ], + "implicationNotes": [ + "Expect yes: modified-christian → commonality, modified-secular → commonality.", + "Expect no: natural-christian → commonality (does not mention minors / medical transition); modified-christian → modified-secular (adds SSM)." + ] + }, + { + "id": "scripture", + "title": "Scripture available (Christian unique)", + "notes": ["No bridge triple."], + "statements": [ + { + "id": "natural-christian", + "role": "natural-christian", + "text": "Everyone should be able to read Scripture in their own language, including people who currently have no translation." + } + ] + }, + { + "id": "colorblind-merit", + "title": "Colorblind merit (secular unique)", + "notes": ["No bridge triple."], + "statements": [ + { + "id": "natural-secular", + "role": "natural-secular", + "text": "The law should treat people as individuals, not as racial blocs. Hiring and admissions should not award or penalize people for their ancestry." + } + ] + } + ] +} diff --git a/fake-data-generation/seedChristianityCause.ts b/fake-data-generation/seedChristianityCause.ts index ddac25b2..f96c3409 100644 --- a/fake-data-generation/seedChristianityCause.ts +++ b/fake-data-generation/seedChristianityCause.ts @@ -44,6 +44,13 @@ import { serializeSeedCauseBookmarkList, type SeedCauseRosterFields, } from './seedCauseRoster.js'; +import { + CHRISTIANITY_NATURAL_PLANKS, + MEDIATOR_STATEMENTS, + SECULAR_NATURAL_PLANKS, +} from './christianSecularBridge.js'; +import { readFileSync } from 'fs'; +import { dirname, join } from 'path'; loadEnv(); @@ -76,7 +83,7 @@ const MONTH_SECONDS = 30n * 24n * 60n * 60n; export const CHRISTIANITY_CAUSE_SLUG = 'christianity'; export const CHRISTIANITY_CAUSE_TITLE = 'Christianity'; export const CHRISTIANITY_CAUSE_SUMMARY = - 'Shared works practising Christians can fund with neighbours who arrive at the same conclusions for different reasons. Seed includes a Christian / secular-conservative mediator, a winter warming centre, campus chaplaincy, a family-formation housing brief, and a Common Table essay fund.'; + 'Practising Christians, in their own words: abortion, provision for the poor, sex and marriage, and making Scripture available. A mediator (Hardhat #8) publishes modified wordings toward secular conservatives; those modified texts are not these planks.'; /** Hardhat #8 — distinct from the CSM bridge-creator default (#7). */ export const CHRISTIAN_MEDIATOR_PRIVATE_KEY = FUNDED_HARDHAT_DEV_KEYS[8]!; @@ -90,47 +97,7 @@ export function seedChristianMediatorServiceUrl(): string { return (process.env.SEED_CHRISTIAN_MEDIATOR_URL ?? 'http://127.0.0.1:3011').replace(/\/+$/, ''); } -export const CHRISTIANITY_PLANKS = [ - { - id: 'shared-local-works', - text: 'Christians and their neighbours should be able to fund shared local works — warming centres, chaplaincy, disaster relief — without first resolving why they want the same thing.', - }, - { - id: 'family-formation', - text: 'It should be easier than it currently is for people to marry and raise children — housing, cost, and working hours included.', - }, - { - id: 'kids-and-tech', - text: 'Children should not have unrestricted access to pornography or to platforms engineered to be maximally addictive.', - }, -] as const; - -export const CHRISTIANITY_PROJECTS = [ - { - name: 'Parish winter warming centre', - description: - 'Keep a church hall open overnight through the cold months: cots, a kitchen, and a volunteer rota that neighbouring congregations can share without merging anything.', - kind: 'local-ministry', - plankId: 'shared-local-works', - ownerIndex: 1, - }, - { - name: 'Campus chaplaincy at State U', - description: - 'Fund a chaplain and a student hospitality budget at a large secular university — the kind of presence that has no denomination-wide treasury behind it.', - kind: 'campus-ministry', - plankId: 'shared-local-works', - ownerIndex: 2, - }, - { - name: 'Family-formation housing brief', - description: - 'Commission a short, public brief on what actually makes it possible for ordinary people to marry and raise children in this city — zoning, hours, and childcare costs, not a culture-war pamphlet.', - kind: 'family-formation', - plankId: 'family-formation', - ownerIndex: 3, - }, -] as const; +export const CHRISTIANITY_PLANKS = CHRISTIANITY_NATURAL_PLANKS; /** Hardhat #9 — a distinct founder so the other camp is not the Christianity owner. */ export const SECULAR_CONSERVATIVE_OWNER_KEY = FUNDED_HARDHAT_DEV_KEYS[9]!; @@ -138,17 +105,37 @@ export const SECULAR_CONSERVATIVE_OWNER_ADDRESS = privateKeyToAccount(SECULAR_CO export const SECULAR_CONSERVATIVE_CAUSE_SLUG = 'secular-conservatism'; export const SECULAR_CONSERVATIVE_CAUSE_TITLE = 'Secular conservatism'; export const SECULAR_CONSERVATIVE_CAUSE_SUMMARY = - 'Order, family formation, and measured outcomes without a theological premise. Seed roster so a mediator can point at a real other cause, not only invent a stand-in.'; -export const SECULAR_CONSERVATIVE_PLANKS = [ - { - id: 'two-parent-outcomes', - text: 'Kids do better with two committed parents, and a country that has stopped forming families is storing up a problem it cannot buy its way out of.', - }, - { - id: 'family-formation-priority', - text: 'Making family formation affordable and normal should be a public priority even if people disagree about why families matter.', - }, -] as const; + 'Secular conservatives, in their own words: abortion, markets, sex and marriage, and colorblind merit. Modified wordings live on the mediator cluster, not on this roster.'; +export const SECULAR_CONSERVATIVE_PLANKS = SECULAR_NATURAL_PLANKS; + +interface PersonaProject { + id: string; + name: string; + description: string; + kind: string; + ownerIndex: number; + alignments: string[]; +} + +interface Persona { + id: string; + hardhatIndex: number; + camp: 'christian' | 'secular'; + takesModified: boolean; + signsNaturals: string[]; + aligns: boolean; +} + +function loadPersonaFile(): { projects: PersonaProject[]; personas: Persona[] } { + const path = join(dirname(fileURLToPath(import.meta.url)), 'data', 'christian-secular-personas.json'); + return JSON.parse(readFileSync(path, 'utf8')) as { projects: PersonaProject[]; personas: Persona[] }; +} + +function loadPersonaProjects(): PersonaProject[] { + return loadPersonaFile().projects; +} + +export const CHRISTIANITY_PROJECTS = loadPersonaProjects(); function createClients(privateKey: `0x${string}`) { return createSeedClients(privateKey, RPC_URL); @@ -181,38 +168,67 @@ async function fundPaymentToken(to: `0x${string}`, amount: bigint): Promise> { +async function publishStatementSet( + statements: readonly { id: string; text: string }[], + domain: string, + publisherKey: `0x${string}`, + cids: Map, +): Promise { const publishedData = CONTRACT_ADDRESSES.publishedData as `0x${string}` | undefined; - const owner = createClients(HARDHAT_PRIVATE_KEYS[0]!); + const owner = createClients(publisherKey); const ipfsConfig = createIPFSConfigInNodeJSFromTheUsualEnvVars(); - const cids = new Map(); - for (const plank of CHRISTIANITY_PLANKS) { + for (const plank of statements) { const cid = await publishGeneratedStatement( ipfsConfig, - { text: plank.text, domain: 'christianity', position: plank.id }, - 'christianity', + { text: plank.text, domain, position: plank.id }, + domain, plank.id, 'simple', { clients: owner as WriteClients, publishedDataAddress: publishedData }, ); cids.set(plank.id, cid); - console.log(` Published plank ${plank.id} → ${cid}`); + console.log(` Published ${domain} ${plank.id} → ${cid}`); } +} + +async function publishPlanks(): Promise> { + const cids = new Map(); + await publishStatementSet(CHRISTIANITY_PLANKS, 'christianity', HARDHAT_PRIVATE_KEYS[0]!, cids); + await publishStatementSet(SECULAR_CONSERVATIVE_PLANKS, 'secular-conservatism', SECULAR_CONSERVATIVE_OWNER_KEY, cids); + await publishStatementSet(MEDIATOR_STATEMENTS, 'christian-secular-bridge', CHRISTIAN_MEDIATOR_PRIVATE_KEY, cids); return cids; } +function modifiedIdForNatural(naturalId: string, camp: 'christian' | 'secular'): string | null { + const [group] = naturalId.split('/'); + if (!group || group === 'scripture' || group === 'colorblind-merit') return null; + return `${group}/modified-${camp}`; +} + async function signPlanks(cids: Map): Promise { const beliefs = CONTRACT_ADDRESSES.beliefs as `0x${string}` | undefined; if (!beliefs) { console.warn('Beliefs contract not configured — skipping plank signatures.'); return; } - const signerIndexes = [0, 1, 2, 4, 5, 6]; - for (const index of signerIndexes) { - const key = FUNDED_HARDHAT_DEV_KEYS[index]; + for (const persona of loadPersonaFile().personas) { + const key = FUNDED_HARDHAT_DEV_KEYS[persona.hardhatIndex]; if (!key) continue; + const toSign = [...persona.signsNaturals]; + if (persona.takesModified) { + for (const naturalId of persona.signsNaturals) { + const modifiedId = modifiedIdForNatural(naturalId, persona.camp); + if (modifiedId) toSign.push(modifiedId); + } + } const clients = createClients(key); - for (const cid of cids.values()) { + let signed = 0; + for (const statementId of toSign) { + const cid = cids.get(statementId); + if (!cid) { + console.warn(` Missing CID for ${statementId} (persona ${persona.id})`); + continue; + } const hash = await clients.walletClient.writeContract({ address: beliefs, abi: BeliefsAbi, @@ -222,8 +238,9 @@ async function signPlanks(cids: Map): Promise { account: clients.walletClient.account, }); await clients.publicClient.waitForTransactionReceipt({ hash }); + signed += 1; } - console.log(` ✓ HH#${index} signed ${cids.size} Christianity planks`); + console.log(` ✓ HH#${persona.hardhatIndex} (${persona.id}) signed ${signed} statements`); } } @@ -236,7 +253,7 @@ interface CreatedChristianProject { prices: string[]; } -async function createProjects(plankCids: Map): Promise { +async function createProjects(statementCids: Map): Promise { const factory = CONTRACT_ADDRESSES.projectFactory as `0x${string}` | undefined; const publishedData = CONTRACT_ADDRESSES.publishedData as `0x${string}` | undefined; const paymentToken = process.env.PAYMENT_TOKEN_ADDRESS as `0x${string}` | undefined; @@ -257,7 +274,7 @@ async function createProjects(plankCids: Map): Promise): Promise { + const [groupId, statementId] = alignment.split('/'); + return { collectionId: 'christian-secular-bridge', groupId, statementId }; + }); const publication = await store.publish(createDisplayableDocument({ format: 'markdown-restricted', content: template.description, @@ -276,7 +297,7 @@ async function createProjects(plankCids: Map): Promise): Promise): Promise persona.aligns); + if (alignment) { + for (const alignmentId of template.alignments) { + const statementCid = statementCids.get(alignmentId); + if (!statementCid) { + console.warn(` Missing CID for alignment ${alignmentId}`); + continue; + } + const attesterPersona = aligners[0]; + const attesterKey = attesterPersona + ? FUNDED_HARDHAT_DEV_KEYS[attesterPersona.hardhatIndex] + : FUNDED_HARDHAT_DEV_KEYS[0]; + if (!attesterKey) continue; + const attester = createClients(attesterKey); + const hash = await attestAlignment( + attester as WriteClients, + { address: alignment, abi: AlignmentAttestationsAbi }, + toSubjectId(projectDetails.assuranceContractAddress), + statementCid, + PROJECT_ALIGNMENT_TOPIC, + ); + await attester.publicClient.waitForTransactionReceipt({ hash }); + } } } return created; @@ -361,17 +394,17 @@ async function buyAndPledge(projects: CreatedChristianProject[], plankCids: Map< const recurringPledges = CONTRACT_ADDRESSES.recurringPledges as `0x${string}` | undefined; const notes = CONTRACT_ADDRESSES.delegatableNotes as `0x${string}` | undefined; - const sharedWorks = plankCids.get('shared-local-works'); - const family = plankCids.get('family-formation'); - if (!recurringPledges || !notes || !sharedWorks) { + const scripture = plankCids.get('scripture/natural-christian'); + const marketsModified = plankCids.get('markets/modified-christian'); + if (!recurringPledges || !notes || !scripture) { console.warn('Recurring pledges not configured — skipping Christianity monthly pledges.'); return; } const pledges = [ - { accountIndex: 4, cid: sharedWorks, amount: '20' }, - { accountIndex: 5, cid: sharedWorks, amount: '8' }, - { accountIndex: 6, cid: family ?? sharedWorks, amount: '12' }, + { accountIndex: 4, cid: scripture, amount: '20' }, + { accountIndex: 5, cid: marketsModified ?? scripture, amount: '8' }, + { accountIndex: 6, cid: scripture, amount: '12' }, ]; const delegateTo = privateKeyToAccount(FUNDED_HARDHAT_DEV_KEYS[0]!).address; for (const pledge of pledges) { @@ -506,12 +539,12 @@ export async function publishSeedChristianityCause(): Promise<{ publishedData: CONTRACT_ADDRESSES.publishedData, alignmentAttestations: CONTRACT_ADDRESSES.alignmentAttestations, }; - const sharedWorks = plankMap.get('shared-local-works'); + const scripture = plankMap.get('scripture/natural-christian'); if ( cfAddresses.channelRegistry && cfAddresses.channelVerifier && cfAddresses.creatorContractFactory - && sharedWorks + && scripture ) { try { await generateChristianContentScenario( @@ -526,15 +559,18 @@ export async function publishSeedChristianityCause(): Promise<{ privateKey, address: privateKeyToAccount(privateKey).address, })), - { statementCid: sharedWorks }, + { statementCid: scripture }, ); } catch (error) { console.warn('Christianity content contract failed (channel may already exist):', error); } } - const rosterCid = await publishRoster([...plankMap.values()]); - await publishSeedSecularConservativeCause(); + const christianPlankCids = CHRISTIANITY_PLANKS.map((plank) => plankMap.get(plank.id)).filter( + (cid): cid is IpfsCidV1 => Boolean(cid), + ); + const rosterCid = await publishRoster(christianPlankCids); + await publishSeedSecularConservativeCause(plankMap); return { slug: CHRISTIANITY_CAUSE_SLUG, rosterCid, @@ -551,7 +587,9 @@ export function secularConservativeRosterFields(plankCids: string[]): SeedCauseR }; } -export async function publishSeedSecularConservativeCause(): Promise<{ +export async function publishSeedSecularConservativeCause( + existingCids?: Map, +): Promise<{ slug: string; rosterCid: string | null; plankCids: string[]; @@ -564,19 +602,26 @@ export async function publishSeedSecularConservativeCause(): Promise<{ } console.log('\n=== Publishing seed CauseStarter roster (secular conservatism) ===\n'); const owner = createClients(SECULAR_CONSERVATIVE_OWNER_KEY); - const ipfsConfig = createIPFSConfigInNodeJSFromTheUsualEnvVars(); const plankCids: string[] = []; - for (const plank of SECULAR_CONSERVATIVE_PLANKS) { - const cid = await publishGeneratedStatement( - ipfsConfig, - { text: plank.text, domain: 'secular-conservatism', position: plank.id }, - 'secular-conservatism', - plank.id, - 'simple', - { clients: owner as WriteClients, publishedDataAddress: publishedData }, - ); - plankCids.push(cid); - console.log(` Published plank ${plank.id} → ${cid}`); + if (existingCids) { + for (const plank of SECULAR_CONSERVATIVE_PLANKS) { + const cid = existingCids.get(plank.id); + if (cid) plankCids.push(cid); + } + } else { + const ipfsConfig = createIPFSConfigInNodeJSFromTheUsualEnvVars(); + for (const plank of SECULAR_CONSERVATIVE_PLANKS) { + const cid = await publishGeneratedStatement( + ipfsConfig, + { text: plank.text, domain: 'secular-conservatism', position: plank.id }, + 'secular-conservatism', + plank.id, + 'simple', + { clients: owner as WriteClients, publishedDataAddress: publishedData }, + ); + plankCids.push(cid); + console.log(` Published plank ${plank.id} → ${cid}`); + } } const fields = secularConservativeRosterFields(plankCids); const doc = buildSeedRosterDocument(fields); diff --git a/fake-data-generation/test/seedMetadata.test.ts b/fake-data-generation/test/seedMetadata.test.ts index c3ce24d4..01c9cac3 100644 --- a/fake-data-generation/test/seedMetadata.test.ts +++ b/fake-data-generation/test/seedMetadata.test.ts @@ -172,14 +172,17 @@ test('seed cause roster is a CauseStarter document owned by Hardhat #0', () => { }); test('christianity seed roster includes the example mediator and distinct planks', () => { - const plankCids = ['bafkreiplank1', 'bafkreiplank2', 'bafkreiplank3']; + const plankCids = ['bafkreiplank1', 'bafkreiplank2', 'bafkreiplank3', 'bafkreiplank4']; const fields = christianityRosterFields(plankCids); const doc = buildSeedRosterDocument(fields); assert.equal(CHRISTIANITY_CAUSE_SLUG, 'christianity'); assert.equal(fields.title, 'Christianity'); - assert.equal(CHRISTIANITY_PLANKS.length, 3); - assert.equal(CHRISTIANITY_PROJECTS.length, 3); + assert.equal(CHRISTIANITY_PLANKS.length, 4); + assert.equal(CHRISTIANITY_PROJECTS.length, 10); assert.ok(CHRISTIANITY_PROJECTS.some((project) => project.kind === 'campus-ministry')); + assert.ok(CHRISTIANITY_PROJECTS.some((project) => project.alignments.includes('abortion/modified-christian'))); + assert.ok(CHRISTIANITY_PROJECTS.some((project) => project.alignments.includes('scripture/natural-christian'))); + assert.ok(CHRISTIANITY_PROJECTS.some((project) => project.alignments.includes('colorblind-merit/natural-secular'))); assert.match(fields.mediatorBlurb, /secular-conservative/i); assert.equal(fields.mediator?.name, CHRISTIAN_MEDIATOR_NAME); assert.equal(fields.mediator?.address.toLowerCase(), CHRISTIAN_MEDIATOR_ADDRESS.toLowerCase()); @@ -190,10 +193,10 @@ test('christianity seed roster includes the example mediator and distinct planks }); test('secular-conservative seed roster is a distinct founder cause', () => { - const plankCids = ['bafkreiplankA', 'bafkreiplankB']; + const plankCids = ['bafkreiplankA', 'bafkreiplankB', 'bafkreiplankC', 'bafkreiplankD']; const fields = secularConservativeRosterFields(plankCids); assert.equal(SECULAR_CONSERVATIVE_CAUSE_SLUG, 'secular-conservatism'); assert.equal(fields.title, 'Secular conservatism'); - assert.equal(SECULAR_CONSERVATIVE_PLANKS.length, 2); + assert.equal(SECULAR_CONSERVATIVE_PLANKS.length, 4); assert.equal(fields.mediatorBlurb, ''); }); diff --git a/specs/product/statements-are-peculiar-for-good-reasons.md b/specs/product/statements-are-peculiar-for-good-reasons.md index c49d140b..4e45a9cc 100644 --- a/specs/product/statements-are-peculiar-for-good-reasons.md +++ b/specs/product/statements-are-peculiar-for-good-reasons.md @@ -103,7 +103,7 @@ Read these; do not copy them into this file. - Seed implication pipeline: [fake-data-generation README](/fake-data-generation/README.md) (`gen:seed:implications`, checked-in `data/seed-implication-evaluations.*`). - [Seed content rationale](/specs/tech/subsystems/conceptspace/seed-content/README.md) — *why* we seed; JSON source of truth is [`fake-data-generation/seed-content/`](/fake-data-generation/seed-content/). -**Known mismatch (as of the seed-data cleanup):** default `./scripts/data.sh --seed` is **tiny** (a handful of generated `universe.json` statements plus a couple of CauseStarter refs). It is **not** the hidden-majority corpus, and even that corpus’s “normal-left implies commonality” notes are aspirational — the texts were not written as modified statements that contain the commonality. Fix by hand-working a few clusters, running them through the attester (and mediator) until they bless as expected, and making *that* the tiny seed. Do not bulk-generate first. +**Tiny-seed work in progress:** [christian-secular-tiny-seed.md](/fake-data-generation/christian-secular-tiny-seed.md) — hand-worked Christianity × secular-conservatism cluster; live attester has blessed the designed modified→commonality pairs. ## What not to write From 65486cf095309973416ad0755100007d6167531a Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Tue, 25 Aug 2026 14:00:21 -0400 Subject: [PATCH 05/14] Make tiny seed the Christian/secular cluster, with mediator nudges and blessed arrows. gen:tiny no longer publishes random universe.json statements. Hardhat #8 publishes parent-natural to modified nudges, and the local implication attester replays the six modified-to-commonality arrows on chain. --- fake-data-generation/README.md | 4 +- .../christian-secular-tiny-seed.md | 11 ++- .../christianSecularBridge.ts | 20 +++++ fake-data-generation/package.json | 2 +- fake-data-generation/runSimulation.ts | 7 ++ fake-data-generation/seedChristianityCause.ts | 88 ++++++++++++++++++- .../test/seedMetadata.test.ts | 17 ++++ scripts/data.sh | 2 +- .../conceptspace/seed-content/README.md | 2 +- workflow/local-development.md | 2 +- 10 files changed, 142 insertions(+), 13 deletions(-) diff --git a/fake-data-generation/README.md b/fake-data-generation/README.md index 96e2c9ad..969ba09c 100644 --- a/fake-data-generation/README.md +++ b/fake-data-generation/README.md @@ -33,7 +33,7 @@ npm install npm run gen:simulate # Or with custom parameters -npm run gen:tiny # 5 users, 1 round, 12 statements, capped actions, no invariant pass +npm run gen:tiny # 5 users, 1 round, no random universe statements, capped actions, no invariant pass npm run gen:seed:christian-secular-implications # live attester on the tiny-bridge designed pairs # Always publishes the Local food systems + Christianity CauseStarter rosters (nightly wipe uses this). # This is what `./scripts/data.sh --seed` runs by default. @@ -93,7 +93,7 @@ Generated files are split into two directories to make their lifecycle explicit: ## Formal Seed Content -Statement *shape* (modified vs natural vs commonality, what the implication attester will bless) is documented in [`specs/product/statements-are-peculiar-for-good-reasons.md`](../specs/product/statements-are-peculiar-for-good-reasons.md). Working plan for the Christianity × secular-conservatism tiny seed: [`christian-secular-tiny-seed.md`](./christian-secular-tiny-seed.md). `gen:tiny` does **not** yet replace the 12 random universe statements with that corpus (the CauseStarter Christianity path already publishes it). +Statement *shape* (modified vs natural vs commonality, what the implication attester will bless) is documented in [`specs/product/statements-are-peculiar-for-good-reasons.md`](../specs/product/statements-are-peculiar-for-good-reasons.md). Working plan for the Christianity × secular-conservatism tiny seed: [`christian-secular-tiny-seed.md`](./christian-secular-tiny-seed.md). `gen:tiny` does **not** publish the random 12-statement `universe.json` slice; CauseStarter Christianity + secular-conservatism (and local-food) are the tiny story. The curated seed statements for the real system now live in `seed-content/*.json` using a small formal schema: diff --git a/fake-data-generation/christian-secular-tiny-seed.md b/fake-data-generation/christian-secular-tiny-seed.md index 9f5f9b16..c5bbffe3 100644 --- a/fake-data-generation/christian-secular-tiny-seed.md +++ b/fake-data-generation/christian-secular-tiny-seed.md @@ -35,18 +35,17 @@ Modified texts **copy the commonality sentences** so the attester’s subset rul - [x] This plan file. - [x] Author seed JSON (8 naturals + 3 triples). - [x] Live attester (2026-08-25, `deepseek/deepseek-v3.2`): all 6 designed **yes** pairs blessed (high / subset); all 6 designed **no** pairs refused. Script: `npm run gen:seed:christian-secular-implications`. -- [x] Point `CHRISTIANITY_PLANKS` / `SECULAR_CONSERVATIVE_PLANKS` at the naturals; publish modified+CG as mediator (#8) statements. **Nudge batches parent→modified still TODO.** +- [x] Point `CHRISTIANITY_PLANKS` / `SECULAR_CONSERVATIVE_PLANKS` at the naturals; publish modified+CG as mediator (#8) statements. - [x] Persona JSON (`data/christian-secular-personas.json`) + driver in `seedChristianityCause.ts`: persona-based signs, 10 projects, mixed natural vs modified alignments, unique-only scripture + colorblind negatives. -- [ ] Make `gen:tiny` load this instead of 12 random universe statements (Christianity seed already runs on every seed size; random universe statements still published). +- [x] Make `gen:tiny` skip the 12 random `universe.json` statements (`--statement-limit=0`; Christianity/secular + local-food still seed). - [x] `seedMetadata.test.ts` plank counts 4/4 and 10 projects. Common Table retargeted to `scripture/natural-christian`. +- [x] Nudge batches: Hardhat #8 publishes 6 parent-natural → modified suggestions (`NATURAL_TO_MODIFIED_NUDGES`). +- [x] On-chain implications: local implication attester replays 6 blessed modified→CG arrows (`BLESSED_MODIFIED_TO_COMMONALITY`). - [ ] Optional: align bridge-creator example anchors with these texts later; do not fork a second abortion triple in hidden-majority-patterns. ## Still open (resume here) -1. **Nudge batches.** Publish parent→modified nudges from Hardhat #8 so the mediator card is not a dead link. Naturals are the parents; modifieds are the suggestions. -2. **`gen:tiny` still publishes 12 random `universe.json` statements.** Christianity/secular cluster already runs on every seed size; tiny should stop dumping the unrelated generated slice once this cluster is the tiny story. -3. **Live wipe-and-reseed + CauseStarter click-through.** Not done this session. After (1)–(2), `./scripts/data.sh --wipe` + `--seed=tiny` and check both cause boards, modified alignments crossing via CG, and unique-only projects *not* crossing. -4. **On-chain implication attestations.** We only ran the LLM evaluator (`evaluateChristianSecularBridge.ts`). Seeding still needs the blessed modified→CG arrows actually published on chain (replay stored decisions, same pattern as `seed-implication-evaluations.*`). +1. **CauseStarter click-through.** Full wipe+reseed done 2026-08-25 (`./scripts/stop-wipe-restart.sh --seed`, ~2m33s). Tiny published 0 random universe statements, 6 mediator nudges, 6 modified→CG arrows, both cause boards. UI walk (CG crossing, unique-only not crossing) not done in that pass. Prospective-round content scenario still failed (`Failed to find ProspectiveRoundCreated event`); local-food + Christianity continued. ## Resume hints diff --git a/fake-data-generation/christianSecularBridge.ts b/fake-data-generation/christianSecularBridge.ts index 014d834c..87c17d77 100644 --- a/fake-data-generation/christianSecularBridge.ts +++ b/fake-data-generation/christianSecularBridge.ts @@ -52,3 +52,23 @@ export const MEDIATOR_STATEMENTS = [ bridgeStatement('lgbt', 'modified-secular'), bridgeStatement('lgbt', 'commonality'), ] as const; + +/** Parent natural → mediator-authored modified (nudges from Hardhat #8). */ +export const NATURAL_TO_MODIFIED_NUDGES: ReadonlyArray<{ target: string; suggested: string }> = [ + { target: 'abortion/natural-christian', suggested: 'abortion/modified-christian' }, + { target: 'abortion/natural-secular', suggested: 'abortion/modified-secular' }, + { target: 'markets/natural-christian', suggested: 'markets/modified-christian' }, + { target: 'markets/natural-secular', suggested: 'markets/modified-secular' }, + { target: 'lgbt/natural-christian', suggested: 'lgbt/modified-christian' }, + { target: 'lgbt/natural-secular', suggested: 'lgbt/modified-secular' }, +]; + +/** Designed-yes arrows the live attester already blessed (modified → commonality). */ +export const BLESSED_MODIFIED_TO_COMMONALITY: ReadonlyArray<{ from: string; to: string }> = [ + { from: 'abortion/modified-christian', to: 'abortion/commonality' }, + { from: 'abortion/modified-secular', to: 'abortion/commonality' }, + { from: 'markets/modified-christian', to: 'markets/commonality' }, + { from: 'markets/modified-secular', to: 'markets/commonality' }, + { from: 'lgbt/modified-christian', to: 'lgbt/commonality' }, + { from: 'lgbt/modified-secular', to: 'lgbt/commonality' }, +]; diff --git a/fake-data-generation/package.json b/fake-data-generation/package.json index 05303788..094b908e 100644 --- a/fake-data-generation/package.json +++ b/fake-data-generation/package.json @@ -19,7 +19,7 @@ "gen:attesters": "tsx generateAttesters.ts", "gen:attestations": "tsx generateAttestations.ts", "gen:simulate": "tsx runSimulation.ts", - "gen:tiny": "tsx runSimulation.ts 5 1 --statement-limit=12 --max-actions-per-user=2 --skip-invariants", + "gen:tiny": "tsx runSimulation.ts 5 1 --statement-limit=0 --max-actions-per-user=2 --skip-invariants", "gen:small": "tsx runSimulation.ts 10 3 --skip-invariants", "gen:seed:local": "npm run gen:seed:universe -- --exclude-proliferation && tsx runSimulation.ts 12 3 --universe=output/seed-universe.json --publish-seed-worker-outputs", "gen:seed:leaderboard": "tsx seedLeaderboardActivity.ts", diff --git a/fake-data-generation/runSimulation.ts b/fake-data-generation/runSimulation.ts index 1c6bc82f..a9e0fdd4 100644 --- a/fake-data-generation/runSimulation.ts +++ b/fake-data-generation/runSimulation.ts @@ -487,6 +487,13 @@ class SimulationRunner { } async performAction(actionType: string, user: User): Promise { + const needsStatements = actionType === 'setBelief' + || actionType === 'setBeliefsInBatch' + || actionType === 'attestImplication'; + if (needsStatements && this.statements.length === 0) { + return; + } + const clients = this.getClientsForUser(user); const publicClient = clients.publicClient; diff --git a/fake-data-generation/seedChristianityCause.ts b/fake-data-generation/seedChristianityCause.ts index f96c3409..254eec37 100644 --- a/fake-data-generation/seedChristianityCause.ts +++ b/fake-data-generation/seedChristianityCause.ts @@ -13,7 +13,9 @@ import { AlignmentAttestationsAbi, AssuranceContractAbi, BeliefsAbi, + ImplicationsAbi, MutableRefUpdaterAbi, + NudgePublicationsAbi, ProjectFactoryAbi, PublishedDataAbi, RecurringPledgesAbi, @@ -28,7 +30,7 @@ import { buyProjectTokens, createProject as sdkCreateProject } from '@commonalit import { createSDKMachinery } from '@commonality/sdk/machinery'; import { getRef, updateRef } from '@commonality/sdk/mutable-refs'; import { createIPFSConfigInNodeJSFromTheUsualEnvVars } from '@commonality/sdk/node'; -import { cidToBytes32, type IpfsCidV1, type WriteClients } from '@commonality/sdk/utils'; +import { cidToBytes32, type IpfsCidV1, uploadToIPFS, type WriteClients } from '@commonality/sdk/utils'; import { publishGeneratedStatement } from './generateStatements.js'; import { HARDHAT_PRIVATE_KEYS } from './generateUsers.js'; import { CONTRACT_ADDRESSES, loadEnv, RPC_URL } from './loadEnv.js'; @@ -45,8 +47,10 @@ import { type SeedCauseRosterFields, } from './seedCauseRoster.js'; import { + BLESSED_MODIFIED_TO_COMMONALITY, CHRISTIANITY_NATURAL_PLANKS, MEDIATOR_STATEMENTS, + NATURAL_TO_MODIFIED_NUDGES, SECULAR_NATURAL_PLANKS, } from './christianSecularBridge.js'; import { readFileSync } from 'fs'; @@ -199,6 +203,86 @@ async function publishPlanks(): Promise> { return cids; } +async function publishMediatorNudges(cids: Map): Promise { + const nudgePublications = process.env.NUDGE_PUBLICATIONS_CONTRACT_ADDRESS as `0x${string}` | undefined; + if (!nudgePublications) { + console.warn('NUDGE_PUBLICATIONS_CONTRACT_ADDRESS not configured — skipping mediator nudges.'); + return; + } + const nudges = []; + for (const pair of NATURAL_TO_MODIFIED_NUDGES) { + const target = cids.get(pair.target); + const suggested = cids.get(pair.suggested); + if (!target || !suggested) { + console.warn(` Missing CID for nudge ${pair.target} → ${pair.suggested}`); + continue; + } + nudges.push({ + targetStatementCid: target, + suggestedStatementCid: suggested, + reason: 'Mediator wording that keeps your reasons while naming the overlapping claim.', + confidence: 0.9, + }); + } + if (nudges.length === 0) return; + + const mediator = createClients(CHRISTIAN_MEDIATOR_PRIVATE_KEY); + const ipfsConfig = createIPFSConfigInNodeJSFromTheUsualEnvVars(); + const batch = { + kind: 'nudge-batch', + schemaVersion: 1, + nudger: mediator.account, + publishedAt: Math.floor(Date.now() / 1000), + nudges, + revocations: [], + }; + const batchCid = await uploadToIPFS(ipfsConfig, batch); + const hash = await mediator.walletClient.writeContract({ + address: nudgePublications, + abi: NudgePublicationsAbi, + functionName: 'publishNudgeBatch', + args: [cidToBytes32(batchCid)], + chain: mediator.walletClient.chain, + account: mediator.walletClient.account, + }); + await mediator.publicClient.waitForTransactionReceipt({ hash }); + console.log(` ✓ Mediator nudge batch (${nudges.length} parent→modified): ${batchCid}`); +} + +async function attestBlessedImplications(cids: Map): Promise { + const implications = CONTRACT_ADDRESSES.implications as `0x${string}` | undefined; + const attesterKey = process.env.IMPLICATION_ATTESTER_PRIVATE_KEY as `0x${string}` | undefined; + if (!implications || !attesterKey) { + console.warn('Implications contract or IMPLICATION_ATTESTER_PRIVATE_KEY missing — skipping designed arrows.'); + return; + } + const clients = createClients(attesterKey); + let attested = 0; + for (const pair of BLESSED_MODIFIED_TO_COMMONALITY) { + const from = cids.get(pair.from); + const to = cids.get(pair.to); + if (!from || !to) { + console.warn(` Missing CID for implication ${pair.from} → ${pair.to}`); + continue; + } + const hash = await clients.walletClient.writeContract({ + address: implications, + abi: ImplicationsAbi, + functionName: 'attestImplication', + args: [ + cidToBytes32(from), + cidToBytes32(to), + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + chain: clients.walletClient.chain, + account: clients.walletClient.account, + }); + await clients.publicClient.waitForTransactionReceipt({ hash }); + attested += 1; + } + console.log(` ✓ Replayed ${attested} blessed modified→commonality implications`); +} + function modifiedIdForNatural(naturalId: string, camp: 'christian' | 'secular'): string | null { const [group] = naturalId.split('/'); if (!group || group === 'scripture' || group === 'colorblind-merit') return null; @@ -528,6 +612,8 @@ export async function publishSeedChristianityCause(): Promise<{ } | null> { console.log('\n=== Publishing seed CauseStarter roster (Christianity + mediator) ===\n'); const plankMap = await publishPlanks(); + await publishMediatorNudges(plankMap); + await attestBlessedImplications(plankMap); await signPlanks(plankMap); const projects = await createProjects(plankMap); await buyAndPledge(projects, plankMap); diff --git a/fake-data-generation/test/seedMetadata.test.ts b/fake-data-generation/test/seedMetadata.test.ts index 01c9cac3..64b5190b 100644 --- a/fake-data-generation/test/seedMetadata.test.ts +++ b/fake-data-generation/test/seedMetadata.test.ts @@ -35,6 +35,10 @@ import { CHRISTIAN_MEDIATOR_NAME, christianityRosterFields, } from '../seedChristianityCause.js'; +import { + BLESSED_MODIFIED_TO_COMMONALITY, + NATURAL_TO_MODIFIED_NUDGES, +} from '../christianSecularBridge.js'; import { seedChristianContentAlignmentCanonicalIds } from '../contentFundingActions.js'; import { createStatementDocumentFromSeed, flattenSeedStatements, loadSeedCollections } from '../seed-content-format.js'; @@ -200,3 +204,16 @@ test('secular-conservative seed roster is a distinct founder cause', () => { assert.equal(SECULAR_CONSERVATIVE_PLANKS.length, 4); assert.equal(fields.mediatorBlurb, ''); }); + +test('christian-secular bridge has parent→modified nudges and blessed modified→CG arrows', () => { + assert.equal(NATURAL_TO_MODIFIED_NUDGES.length, 6); + assert.equal(BLESSED_MODIFIED_TO_COMMONALITY.length, 6); + for (const pair of NATURAL_TO_MODIFIED_NUDGES) { + assert.match(pair.target, /\/natural-/); + assert.match(pair.suggested, /\/modified-/); + } + for (const pair of BLESSED_MODIFIED_TO_COMMONALITY) { + assert.match(pair.from, /\/modified-/); + assert.match(pair.to, /\/commonality$/); + } +}); diff --git a/scripts/data.sh b/scripts/data.sh index 7a5e764a..a39652b3 100755 --- a/scripts/data.sh +++ b/scripts/data.sh @@ -6,7 +6,7 @@ # ./scripts/data.sh --wipe # Wipe data directory (stops services first) # ./scripts/data.sh --seed # Tiny dataset (default; fast local UI) # ./scripts/stop-wipe-restart.sh --seed # Wipe + start + seed in one go -# ./scripts/data.sh --seed=tiny # Tiny dataset (5 users, 1 round, capped statements/actions) +# ./scripts/data.sh --seed=tiny # Tiny dataset (5 users, 1 round, no random universe statements) # ./scripts/data.sh --seed=small # Small dataset (10 users, 3 rounds) # ./scripts/data.sh --seed=medium # Medium dataset (50 users, 5 rounds) # ./scripts/data.sh --seed=demo # Seed-content demo dataset plus Alignment Explorer/nudge fixtures diff --git a/specs/tech/subsystems/conceptspace/seed-content/README.md b/specs/tech/subsystems/conceptspace/seed-content/README.md index aeac1415..3ea0e7b7 100644 --- a/specs/tech/subsystems/conceptspace/seed-content/README.md +++ b/specs/tech/subsystems/conceptspace/seed-content/README.md @@ -2,7 +2,7 @@ This document covers our thinking about *why* we need seed content, *what kind* to create, and *how* to do it. -Wording is not free-form slogans: see [why statements are peculiar](/specs/product/statements-are-peculiar-for-good-reasons.md). Curated JSON that does not pass the implication attester (modified → commonality) is not done. Default `./scripts/data.sh --seed` (**tiny**) currently publishes generated `universe.json` statements, not this corpus. +Wording is not free-form slogans: see [why statements are peculiar](/specs/product/statements-are-peculiar-for-good-reasons.md). Curated JSON that does not pass the implication attester (modified → commonality) is not done. Default `./scripts/data.sh --seed` (**tiny**) publishes the Christianity × secular-conservatism CauseStarter cluster plus local-food, not a random `universe.json` slice. See this directory for concrete examples. diff --git a/workflow/local-development.md b/workflow/local-development.md index b3698267..adbcec11 100644 --- a/workflow/local-development.md +++ b/workflow/local-development.md @@ -34,7 +34,7 @@ That's it. This uses Docker Compose to start a local Hardhat blockchain, deploys The same env var is read by `scripts/deploy-causestarter.sh`. The allow-list lives in `scripts/ui-domains.mjs` (`resolveLocalPublishDomains`). CauseStarter's dedicated SPA on `:8090` is always started and is independent of this list. -A local UI gateway then gives each **published** IPFS bundle a stable URL such as `http://causestarter.localhost:8088/#/`. Bookmark `http://localhost:8088/admin` for links to whatever was published. The latest CIDs, raw IPFS gateway URLs, and stable local URLs are written to `./data/ui-ipfs//`. You can re-print the stable URLs any time with `./scripts/services.sh --url`. After that, run `./scripts/data.sh --seed` to populate the chain with fake data. The default is `--seed=tiny` (5 users, 1 round, 12 statements, no invariant pass). Use `--seed=small` for the older 10-user / 3-round set. +A local UI gateway then gives each **published** IPFS bundle a stable URL such as `http://causestarter.localhost:8088/#/`. Bookmark `http://localhost:8088/admin` for links to whatever was published. The latest CIDs, raw IPFS gateway URLs, and stable local URLs are written to `./data/ui-ipfs//`. You can re-print the stable URLs any time with `./scripts/services.sh --url`. After that, run `./scripts/data.sh --seed` to populate the chain with fake data. The default is `--seed=tiny` (5 users, 1 round, no random universe statements, no invariant pass). Use `--seed=small` for the older 10-user / 3-round set. For a clean local reset, use: From a22b382fd45aa2f6de1ad2a87e94d313ea2422a5 Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Tue, 25 Aug 2026 14:36:16 -0400 Subject: [PATCH 06/14] Seed the Christian/secular CauseStarter bridge cluster under Hardhat #8. Tiny seed now publishes modified + bridge rosters and a cluster document so the camp boards can cite it. Stop the published cluster page from refetching forever by memoizing the route parse. --- .../christian-secular-tiny-seed.md | 48 +++- fake-data-generation/seedCauseRoster.ts | 97 +++++++++ fake-data-generation/seedChristianityCause.ts | 206 +++++++++++++++++- .../test/seedMetadata.test.ts | 64 ++++++ .../causestarter/pages/BridgeClusterPage.tsx | 5 +- 5 files changed, 409 insertions(+), 11 deletions(-) diff --git a/fake-data-generation/christian-secular-tiny-seed.md b/fake-data-generation/christian-secular-tiny-seed.md index c5bbffe3..159aa6b5 100644 --- a/fake-data-generation/christian-secular-tiny-seed.md +++ b/fake-data-generation/christian-secular-tiny-seed.md @@ -41,11 +41,57 @@ Modified texts **copy the commonality sentences** so the attester’s subset rul - [x] `seedMetadata.test.ts` plank counts 4/4 and 10 projects. Common Table retargeted to `scripture/natural-christian`. - [x] Nudge batches: Hardhat #8 publishes 6 parent-natural → modified suggestions (`NATURAL_TO_MODIFIED_NUDGES`). - [x] On-chain implications: local implication attester replays 6 blessed modified→CG arrows (`BLESSED_MODIFIED_TO_COMMONALITY`). +- [x] CauseStarter click-through on the 2026-08-25 tiny seed (no reseed this pass). - [ ] Optional: align bridge-creator example anchors with these texts later; do not fork a second abortion triple in hidden-majority-patterns. +- [x] CauseStarter **bridge cluster** under #8 (`christian-secular`): two modified rosters + bridge roster + cluster document. Tiny seed publishes it; `--cluster-only` republishes against an existing chain. +- [ ] Prospective-round content scenario still failed at last seed (`Failed to find ProspectiveRoundCreated event`); local-food + Christianity continued. Unrelated to this cluster unless it starts blocking tiny. ## Still open (resume here) -1. **CauseStarter click-through.** Full wipe+reseed done 2026-08-25 (`./scripts/stop-wipe-restart.sh --seed`, ~2m33s). Tiny published 0 random universe statements, 6 mediator nudges, 6 modified→CG arrows, both cause boards. UI walk (CG crossing, unique-only not crossing) not done in that pass. Prospective-round content scenario still failed (`Failed to find ProspectiveRoundCreated event`); local-food + Christianity continued. +Nothing blocking this seed. Optional items above. + +## Bridge cluster (2026-08-25) + +Hardhat #8 refs: + +- Cluster: `/bridge/0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f/christian-secular` +- Modified Christianity: `/cause/0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f/christian-secular-christianity-modified` (3 modified-christian planks) +- Modified secular: `/cause/0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f/christian-secular-secular-conservatism-modified` +- Bridge cause: `/cause/0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f/christian-secular-bridge` (3 commonality planks) +- Six recorded pairs, all `modified-to-bridge` (same as `BLESSED_MODIFIED_TO_COMMONALITY`). Uniques are not in the cluster. + +Natural parent pages list the cluster **after this client has opened the cluster URL** (ADR 0011: remember opened citations; no crawl). Fresh browsers still say “No bridges yet” on Christianity/secular until that visit. + +`BridgeClusterPage` used to refetch forever (`routeRef` object identity in the load effect). Memoize `parseClusterRouteParams` on `owner`/`slugPart`. Docker/IPFS CauseStarter (`:8090`, `:8088`) still has the old bundle until republished; Vite (`npm run causestarter:dev`) shows the page. + +## CauseStarter UI walk (2026-08-25, existing local seed) + +CauseStarter at `http://causestarter.localhost:8088/#/`. Hardhat picker works. + +**Camp boards (naturals only)** + +- Christianity (`#0` / `christianity`): 4 natural planks. Fundable: Common Table, Parish winter warming, Parish marriage-prep, New-language Scripture draft, Campus chaplaincy. **No** first-trimester clinic, **no** colorblind amicus. +- Secular conservatism (`#9` / `secular-conservatism`): 4 natural planks. Fundable: **only** Colorblind admissions amicus. **No** scripture draft, **no** modified-aligned bridge projects. Natural abortion/markets/lgbt planks show 0 projects (alignments sit on modified, not naturals). + +**Commonality crossing (modified → CG arrows live; attester `0x021b3C90931CAdDa12C0dCaB0407A622d717b02C` is trusted)** + +- Abortion CG (`bafybeihku3omeh5tkiwyvlmvy36ec6fr2vasgw3ld4qqtxtij2oqqydzum`): First-trimester decision clinic **and** Late-term restriction legal brief, both **Indirect**. Signers: 2 indirect (the two modified signers). +- Markets CG: Trade apprenticeship match fund + Local charity effectiveness audit, Indirect. +- LGBT CG: Minors: exploratory care, not a pipeline, Indirect. +- Modified-christian abortion board: clinic **Direct** only (not the secular brief). +- Modified-secular abortion board: late-term brief **Direct** only (not the clinic). MC does not imply MS. + +**Unique-only (must not cross)** + +- Scripture natural board: Common Table, Scripture draft, Campus chaplaincy. Not on the secular cause board. Hardhat #5 (secular nudge-taker) fundable list has no scripture project. +- Colorblind amicus: only on the secular unique plank / secular cause. Not on Christianity. + +**Persona dashboards** + +- Hardhat #1 (christian, takesModified): 8 fundable = camp naturals + MC-aligned bridge projects. No colorblind, no MS-only late-term brief. +- Hardhat #5 (secular, takesModified): 4 fundable = colorblind + MS-aligned + dual LGBT. No scripture unique, no MC-only clinic. Personal “fundable” is the union of **signed** statement boards (direct), not an extra union of implied CG. Crossing for the other camp’s modified-aligned project is on the **CG statement page**, which is the locked implication-direction rule. + +**Nudges.** Dashboard still shows no suggestions for subscribers of `#0`. Mediator `#8` published the parent→modified batch; the home “Suggesters” strip is subscribed to `#0`, not `#8`. Not a seed-content bug. ## Resume hints diff --git a/fake-data-generation/seedCauseRoster.ts b/fake-data-generation/seedCauseRoster.ts index b93c378e..2c833a74 100644 --- a/fake-data-generation/seedCauseRoster.ts +++ b/fake-data-generation/seedCauseRoster.ts @@ -24,6 +24,8 @@ import { createSeedClients } from './seedRpc.js'; export const ROSTER_KIND = 'causestarter.roster' as const; export const ROSTER_SCHEMA_VERSION = 1 as const; +export const BRIDGE_CLUSTER_KIND = 'causestarter.bridge-cluster' as const; +export const BRIDGE_CLUSTER_SCHEMA_VERSION = 1 as const; export const CAUSE_BOOKMARKS_REF = 'bookmarked-causes'; export const CAUSE_BOOKMARKS_SCHEMA_VERSION = 1 as const; @@ -57,12 +59,22 @@ export interface SeedCauseMediator { serviceUrl: string; } +export interface SeedRosterBridgeLink { + clusterOwner: string; + clusterSlug: string; + role: 'modified' | 'bridge'; + parentOwner?: string; + parentSlug?: string; +} + export interface SeedCauseRosterFields { title: string; summary: string; plankCids: string[]; mediatorBlurb: string; mediator?: SeedCauseMediator; + /** Present on mediator-owned modified/bridge rosters, never on natural camp boards. */ + bridgeCluster?: SeedRosterBridgeLink; } export function seedCauseRosterFields(plankCid: string): SeedCauseRosterFields { @@ -104,6 +116,91 @@ export function buildSeedRosterDocument(fields: SeedCauseRosterFields) { plankCids: [...fields.plankCids], mediatorBlurb: fields.mediatorBlurb, ...(fields.mediator ? { mediator: fields.mediator } : {}), + ...(fields.bridgeCluster ? { bridgeCluster: normalizeSeedBridgeCluster(fields.bridgeCluster) } : {}), + }, + }); +} + +function normalizeSeedBridgeCluster(link: SeedRosterBridgeLink): SeedRosterBridgeLink { + return { + clusterOwner: link.clusterOwner.toLowerCase(), + clusterSlug: link.clusterSlug, + role: link.role, + ...(link.parentOwner && link.parentSlug + ? { parentOwner: link.parentOwner.toLowerCase(), parentSlug: link.parentSlug } + : {}), + }; +} + +export interface SeedClusterPair { + fromCid: string; + toCid: string; + role: 'modified-to-bridge' | 'modified-to-parent' | 'parent-to-bridge'; +} + +export interface SeedBridgeClusterFields { + mediatorName: string; + mediatorNote: string; + mediatorAddress: string; + parents: Array<{ owner: string; slug: string }>; + modified: Array<{ owner: string; slug: string; parentOwner: string; parentSlug: string }>; + bridge: { owner: string; slug: string }; + pairs: SeedClusterPair[]; +} + +export function renderSeedClusterContent(fields: SeedBridgeClusterFields): string { + const lines = [ + '# Bridge cluster', + '', + `Mediator: ${fields.mediatorName.trim()}`, + ]; + if (fields.mediatorNote.trim()) { + lines.push('', fields.mediatorNote.trim()); + } + lines.push('', '## Natural parents'); + for (const parent of fields.parents) { + lines.push(`- ${parent.owner.toLowerCase()}/${parent.slug}`); + } + lines.push('', '## Modified causes'); + for (const modified of fields.modified) { + lines.push( + `- ${modified.owner.toLowerCase()}/${modified.slug} (from ${modified.parentOwner.toLowerCase()}/${modified.parentSlug})`, + ); + } + lines.push('', '## Bridge cause', `- ${fields.bridge.owner.toLowerCase()}/${fields.bridge.slug}`); + lines.push('', '## Intended plank pairs'); + for (const pair of fields.pairs) { + lines.push(`- ${pair.fromCid} → ${pair.toCid} (${pair.role})`); + } + return lines.join('\n'); +} + +export function buildSeedClusterDocument(fields: SeedBridgeClusterFields) { + const mediatorAddress = fields.mediatorAddress.toLowerCase(); + return createDisplayableDocument({ + format: 'markdown-restricted', + content: renderSeedClusterContent(fields), + extras: { + kind: BRIDGE_CLUSTER_KIND, + version: BRIDGE_CLUSTER_SCHEMA_VERSION, + mediatorName: fields.mediatorName.trim(), + mediatorNote: fields.mediatorNote.trim(), + mediatorAddress, + parents: fields.parents.map((parent) => ({ + owner: parent.owner.toLowerCase(), + slug: parent.slug, + })), + modified: fields.modified.map((modified) => ({ + owner: modified.owner.toLowerCase(), + slug: modified.slug, + parentOwner: modified.parentOwner.toLowerCase(), + parentSlug: modified.parentSlug, + })), + bridge: { + owner: fields.bridge.owner.toLowerCase(), + slug: fields.bridge.slug, + }, + pairs: fields.pairs.map((pair) => ({ ...pair })), }, }); } diff --git a/fake-data-generation/seedChristianityCause.ts b/fake-data-generation/seedChristianityCause.ts index 254eec37..ee73ec24 100644 --- a/fake-data-generation/seedChristianityCause.ts +++ b/fake-data-generation/seedChristianityCause.ts @@ -38,6 +38,7 @@ import { createSeedClients } from './seedRpc.js'; import { parsePaymentTokenUnits } from './paymentTokenUnits.js'; import { generateChristianContentScenario } from './contentFundingActions.js'; import { + buildSeedClusterDocument, buildSeedRosterDocument, CAUSE_BOOKMARKS_REF, FUNDED_HARDHAT_DEV_KEYS, @@ -112,6 +113,15 @@ export const SECULAR_CONSERVATIVE_CAUSE_SUMMARY = 'Secular conservatives, in their own words: abortion, markets, sex and marriage, and colorblind merit. Modified wordings live on the mediator cluster, not on this roster.'; export const SECULAR_CONSERVATIVE_PLANKS = SECULAR_NATURAL_PLANKS; +/** Mutable-ref slug for the published cluster document (Hardhat #8). */ +export const CHRISTIAN_SECULAR_CLUSTER_SLUG = 'christian-secular'; +export const CHRISTIAN_MODIFIED_CAUSE_SLUG = 'christian-secular-christianity-modified'; +export const SECULAR_MODIFIED_CAUSE_SLUG = 'christian-secular-secular-conservatism-modified'; +export const CHRISTIAN_SECULAR_BRIDGE_CAUSE_SLUG = 'christian-secular-bridge'; + +export const CHRISTIAN_SECULAR_CLUSTER_NOTE = + 'Mediator-authored modified wordings and shared planks. Natural camp boards stay as the camps wrote them. Scripture-in-every-language and colorblind merit are unique planks and are not in this cluster.'; + interface PersonaProject { id: string; name: string; @@ -544,6 +554,9 @@ async function mergeBookmarks(): Promise { { owner: SEED_CAUSE_OWNER_ADDRESS, slug: SEED_CAUSE_SLUG }, { owner: SEED_CAUSE_OWNER_ADDRESS, slug: CHRISTIANITY_CAUSE_SLUG }, { owner: SECULAR_CONSERVATIVE_OWNER_ADDRESS, slug: SECULAR_CONSERVATIVE_CAUSE_SLUG }, + { owner: CHRISTIAN_MEDIATOR_ADDRESS, slug: CHRISTIAN_MODIFIED_CAUSE_SLUG }, + { owner: CHRISTIAN_MEDIATOR_ADDRESS, slug: SECULAR_MODIFIED_CAUSE_SLUG }, + { owner: CHRISTIAN_MEDIATOR_ADDRESS, slug: CHRISTIAN_SECULAR_BRIDGE_CAUSE_SLUG }, ]; const seen = new Set(existing.map((item) => `${item.owner.toLowerCase()}/${item.slug}`)); const merged = [...existing]; @@ -574,16 +587,106 @@ export function christianityRosterFields(plankCids: string[]): SeedCauseRosterFi }; } -async function publishRoster(plankCids: string[]): Promise { +function clusterOwner(): `0x${string}` { + return CHRISTIAN_MEDIATOR_ADDRESS.toLowerCase() as `0x${string}`; +} + +export function christianModifiedRosterFields(plankCids: string[]): SeedCauseRosterFields { + return { + title: 'Christianity (modified, mediator wording)', + summary: + 'Mediator wording of practising-Christian planks that still imply the shared abortion, markets, and LGBT claims. Not an official revision of the Christianity cause.', + plankCids, + mediatorBlurb: '', + bridgeCluster: { + clusterOwner: clusterOwner(), + clusterSlug: CHRISTIAN_SECULAR_CLUSTER_SLUG, + role: 'modified', + parentOwner: SEED_CAUSE_OWNER_ADDRESS, + parentSlug: CHRISTIANITY_CAUSE_SLUG, + }, + }; +} + +export function secularModifiedRosterFields(plankCids: string[]): SeedCauseRosterFields { + return { + title: 'Secular conservatism (modified, mediator wording)', + summary: + 'Mediator wording of secular-conservative planks that still imply the shared abortion, markets, and LGBT claims. Not an official revision of the secular conservatism cause.', + plankCids, + mediatorBlurb: '', + bridgeCluster: { + clusterOwner: clusterOwner(), + clusterSlug: CHRISTIAN_SECULAR_CLUSTER_SLUG, + role: 'modified', + parentOwner: SECULAR_CONSERVATIVE_OWNER_ADDRESS, + parentSlug: SECULAR_CONSERVATIVE_CAUSE_SLUG, + }, + }; +} + +export function christianSecularBridgeRosterFields(plankCids: string[]): SeedCauseRosterFields { + return { + title: 'Christian / secular shared ground', + summary: + 'Bridge cause whose featured planks are implied by each modified wording. Unique camp planks (Scripture; colorblind merit) are not here.', + plankCids, + mediatorBlurb: '', + bridgeCluster: { + clusterOwner: clusterOwner(), + clusterSlug: CHRISTIAN_SECULAR_CLUSTER_SLUG, + role: 'bridge', + }, + }; +} + +export function christianSecularClusterFields(cids: Map) { + const owner = clusterOwner(); + const pairs = BLESSED_MODIFIED_TO_COMMONALITY.flatMap((pair) => { + const fromCid = cids.get(pair.from); + const toCid = cids.get(pair.to); + if (!fromCid || !toCid) return []; + return [{ fromCid, toCid, role: 'modified-to-bridge' as const }]; + }); + return { + mediatorName: CHRISTIAN_MEDIATOR_NAME, + mediatorNote: CHRISTIAN_SECULAR_CLUSTER_NOTE, + mediatorAddress: owner, + parents: [ + { owner: SEED_CAUSE_OWNER_ADDRESS, slug: CHRISTIANITY_CAUSE_SLUG }, + { owner: SECULAR_CONSERVATIVE_OWNER_ADDRESS, slug: SECULAR_CONSERVATIVE_CAUSE_SLUG }, + ], + modified: [ + { + owner, + slug: CHRISTIAN_MODIFIED_CAUSE_SLUG, + parentOwner: SEED_CAUSE_OWNER_ADDRESS, + parentSlug: CHRISTIANITY_CAUSE_SLUG, + }, + { + owner, + slug: SECULAR_MODIFIED_CAUSE_SLUG, + parentOwner: SECULAR_CONSERVATIVE_OWNER_ADDRESS, + parentSlug: SECULAR_CONSERVATIVE_CAUSE_SLUG, + }, + ], + bridge: { owner, slug: CHRISTIAN_SECULAR_BRIDGE_CAUSE_SLUG }, + pairs, + }; +} + +async function publishDocumentAs( + publisherKey: `0x${string}`, + slug: string, + doc: ReturnType, +): Promise { const publishedData = CONTRACT_ADDRESSES.publishedData as `0x${string}` | undefined; const mutableRef = CONTRACT_ADDRESSES.mutableRefUpdater as `0x${string}` | undefined; if (!publishedData || !mutableRef) { - console.warn('PublishedData or MutableRefUpdater missing — skipping Christianity roster.'); + console.warn('PublishedData or MutableRefUpdater missing — skipping', slug); return null; } - const owner = createClients(HARDHAT_PRIVATE_KEYS[0]!); - const fields = christianityRosterFields(plankCids); - const doc = buildSeedRosterDocument(fields); + const owner = createClients(publisherKey); const store = createDefaultDocumentStore( createSDKMachinery({ ipfsConfig: createIPFSConfigInNodeJSFromTheUsualEnvVars() }), { @@ -595,14 +698,94 @@ async function publishRoster(plankCids: string[]): Promise { await updateRef( owner as WriteClients, { address: mutableRef, abi: MutableRefUpdaterAbi }, - CHRISTIANITY_CAUSE_SLUG, + slug, publication.cid, ); + return publication.cid; +} + +async function publishRoster(plankCids: string[]): Promise { + const owner = createClients(HARDHAT_PRIVATE_KEYS[0]!); + const cid = await publishDocumentAs( + HARDHAT_PRIVATE_KEYS[0]!, + CHRISTIANITY_CAUSE_SLUG, + buildSeedRosterDocument(christianityRosterFields(plankCids)), + ); + if (!cid) return null; await mergeBookmarks(); console.log( - ` ✓ Cause ${CHRISTIANITY_CAUSE_SLUG} → ${publication.cid}\n Open /cause/${owner.account}/${CHRISTIANITY_CAUSE_SLUG}`, + ` ✓ Cause ${CHRISTIANITY_CAUSE_SLUG} → ${cid}\n Open /cause/${owner.account}/${CHRISTIANITY_CAUSE_SLUG}`, ); - return publication.cid; + return cid; +} + +function cidsFor(ids: readonly string[], cids: Map): IpfsCidV1[] { + return ids.map((id) => cids.get(id)).filter((cid): cid is IpfsCidV1 => Boolean(cid)); +} + +export async function publishChristianSecularBridgeCluster( + cids: Map, +): Promise<{ clusterCid: string | null; rosterCids: string[] }> { + console.log('\n=== Publishing Christian × secular CauseStarter bridge cluster ===\n'); + const christianModified = cidsFor( + ['abortion/modified-christian', 'markets/modified-christian', 'lgbt/modified-christian'], + cids, + ); + const secularModified = cidsFor( + ['abortion/modified-secular', 'markets/modified-secular', 'lgbt/modified-secular'], + cids, + ); + const bridgePlanks = cidsFor( + ['abortion/commonality', 'markets/commonality', 'lgbt/commonality'], + cids, + ); + const rosterCids: string[] = []; + const christianModifiedCid = await publishDocumentAs( + CHRISTIAN_MEDIATOR_PRIVATE_KEY, + CHRISTIAN_MODIFIED_CAUSE_SLUG, + buildSeedRosterDocument(christianModifiedRosterFields(christianModified)), + ); + if (christianModifiedCid) { + rosterCids.push(christianModifiedCid); + console.log(` ✓ Modified Christianity ${CHRISTIAN_MODIFIED_CAUSE_SLUG} → ${christianModifiedCid}`); + } + const secularModifiedCid = await publishDocumentAs( + CHRISTIAN_MEDIATOR_PRIVATE_KEY, + SECULAR_MODIFIED_CAUSE_SLUG, + buildSeedRosterDocument(secularModifiedRosterFields(secularModified)), + ); + if (secularModifiedCid) { + rosterCids.push(secularModifiedCid); + console.log(` ✓ Modified secular ${SECULAR_MODIFIED_CAUSE_SLUG} → ${secularModifiedCid}`); + } + const bridgeRosterCid = await publishDocumentAs( + CHRISTIAN_MEDIATOR_PRIVATE_KEY, + CHRISTIAN_SECULAR_BRIDGE_CAUSE_SLUG, + buildSeedRosterDocument(christianSecularBridgeRosterFields(bridgePlanks)), + ); + if (bridgeRosterCid) { + rosterCids.push(bridgeRosterCid); + console.log(` ✓ Bridge cause ${CHRISTIAN_SECULAR_BRIDGE_CAUSE_SLUG} → ${bridgeRosterCid}`); + } + + const clusterFields = christianSecularClusterFields(cids); + if (clusterFields.pairs.length !== BLESSED_MODIFIED_TO_COMMONALITY.length) { + console.warn( + ` Cluster has ${clusterFields.pairs.length}/${BLESSED_MODIFIED_TO_COMMONALITY.length} modified→CG pairs (missing CIDs).`, + ); + } + const clusterCid = await publishDocumentAs( + CHRISTIAN_MEDIATOR_PRIVATE_KEY, + CHRISTIAN_SECULAR_CLUSTER_SLUG, + buildSeedClusterDocument(clusterFields), + ); + await mergeBookmarks(); + if (clusterCid) { + console.log( + ` ✓ Cluster ${CHRISTIAN_SECULAR_CLUSTER_SLUG} → ${clusterCid}\n Open /bridge/${clusterOwner()}/${CHRISTIAN_SECULAR_CLUSTER_SLUG}`, + ); + } + return { clusterCid, rosterCids }; } export async function publishSeedChristianityCause(): Promise<{ @@ -657,6 +840,7 @@ export async function publishSeedChristianityCause(): Promise<{ ); const rosterCid = await publishRoster(christianPlankCids); await publishSeedSecularConservativeCause(plankMap); + await publishChristianSecularBridgeCluster(plankMap); return { slug: CHRISTIANITY_CAUSE_SLUG, rosterCid, @@ -737,7 +921,11 @@ export async function publishSeedSecularConservativeCause( } if (process.argv[1] === fileURLToPath(import.meta.url)) { - publishSeedChristianityCause() + const clusterOnly = process.argv.includes('--cluster-only'); + const run = clusterOnly + ? publishPlanks().then((cids) => publishChristianSecularBridgeCluster(cids)) + : publishSeedChristianityCause(); + run .then(() => process.exit(0)) .catch((error) => { console.error(error); diff --git a/fake-data-generation/test/seedMetadata.test.ts b/fake-data-generation/test/seedMetadata.test.ts index 64b5190b..781c25a1 100644 --- a/fake-data-generation/test/seedMetadata.test.ts +++ b/fake-data-generation/test/seedMetadata.test.ts @@ -15,6 +15,9 @@ import { seedMixedContentAlignmentCanonicalIds, } from '../contentFundingActions.js'; import { + BRIDGE_CLUSTER_KIND, + BRIDGE_CLUSTER_SCHEMA_VERSION, + buildSeedClusterDocument, buildSeedRosterDocument, CAUSE_BOOKMARKS_SCHEMA_VERSION, ROSTER_KIND, @@ -34,6 +37,14 @@ import { CHRISTIAN_MEDIATOR_ADDRESS, CHRISTIAN_MEDIATOR_NAME, christianityRosterFields, + CHRISTIAN_SECULAR_CLUSTER_SLUG, + CHRISTIAN_MODIFIED_CAUSE_SLUG, + SECULAR_MODIFIED_CAUSE_SLUG, + CHRISTIAN_SECULAR_BRIDGE_CAUSE_SLUG, + christianModifiedRosterFields, + secularModifiedRosterFields, + christianSecularBridgeRosterFields, + christianSecularClusterFields, } from '../seedChristianityCause.js'; import { BLESSED_MODIFIED_TO_COMMONALITY, @@ -205,6 +216,59 @@ test('secular-conservative seed roster is a distinct founder cause', () => { assert.equal(fields.mediatorBlurb, ''); }); +test('christian-secular seed cluster documents match CauseStarter extras', () => { + const modifiedCids = ['bafymc1', 'bafymc2', 'bafymc3']; + const modified = christianModifiedRosterFields(modifiedCids); + const modifiedDoc = buildSeedRosterDocument(modified); + assert.equal(modified.bridgeCluster?.role, 'modified'); + assert.equal(modified.bridgeCluster?.clusterSlug, CHRISTIAN_SECULAR_CLUSTER_SLUG); + assert.equal(modified.bridgeCluster?.parentSlug, CHRISTIANITY_CAUSE_SLUG); + assert.equal(modifiedDoc.extras?.kind, ROSTER_KIND); + assert.deepEqual(modifiedDoc.extras?.bridgeCluster, { + clusterOwner: CHRISTIAN_MEDIATOR_ADDRESS.toLowerCase(), + clusterSlug: CHRISTIAN_SECULAR_CLUSTER_SLUG, + role: 'modified', + parentOwner: SEED_CAUSE_OWNER_ADDRESS.toLowerCase(), + parentSlug: CHRISTIANITY_CAUSE_SLUG, + }); + + const secularModified = secularModifiedRosterFields(['bafyms1']); + assert.equal(secularModified.bridgeCluster?.parentSlug, SECULAR_CONSERVATIVE_CAUSE_SLUG); + assert.equal(SECULAR_MODIFIED_CAUSE_SLUG, 'christian-secular-secular-conservatism-modified'); + + const bridge = christianSecularBridgeRosterFields(['bafycg1']); + const bridgeDoc = buildSeedRosterDocument(bridge); + assert.equal(bridge.bridgeCluster?.role, 'bridge'); + assert.equal(bridge.bridgeCluster?.parentSlug, undefined); + assert.deepEqual(bridgeDoc.extras?.bridgeCluster, { + clusterOwner: CHRISTIAN_MEDIATOR_ADDRESS.toLowerCase(), + clusterSlug: CHRISTIAN_SECULAR_CLUSTER_SLUG, + role: 'bridge', + }); + assert.equal(CHRISTIAN_MODIFIED_CAUSE_SLUG.length <= 64, true); + assert.equal(CHRISTIAN_SECULAR_BRIDGE_CAUSE_SLUG, 'christian-secular-bridge'); + + const cids = new Map([ + ['abortion/modified-christian', 'bafymcab'], + ['abortion/commonality', 'bafycgab'], + ['abortion/modified-secular', 'bafymsab'], + ['markets/modified-christian', 'bafymcmc'], + ['markets/commonality', 'bafycgmc'], + ['markets/modified-secular', 'bafymsmc'], + ['lgbt/modified-christian', 'bafymclg'], + ['lgbt/commonality', 'bafycglg'], + ['lgbt/modified-secular', 'bafymslg'], + ]); + const cluster = christianSecularClusterFields(cids); + assert.equal(cluster.pairs.length, 6); + assert.ok(cluster.pairs.every((pair) => pair.role === 'modified-to-bridge')); + const clusterDoc = buildSeedClusterDocument(cluster); + assert.equal(clusterDoc.extras?.kind, BRIDGE_CLUSTER_KIND); + assert.equal(clusterDoc.extras?.version, BRIDGE_CLUSTER_SCHEMA_VERSION); + assert.equal(clusterDoc.extras?.mediatorAddress, CHRISTIAN_MEDIATOR_ADDRESS.toLowerCase()); + assert.match(clusterDoc.content, /Natural parents/); +}); + test('christian-secular bridge has parent→modified nudges and blessed modified→CG arrows', () => { assert.equal(NATURAL_TO_MODIFIED_NUDGES.length, 6); assert.equal(BLESSED_MODIFIED_TO_COMMONALITY.length, 6); diff --git a/ui/src/causestarter/pages/BridgeClusterPage.tsx b/ui/src/causestarter/pages/BridgeClusterPage.tsx index 3732ea00..a6f3b48e 100644 --- a/ui/src/causestarter/pages/BridgeClusterPage.tsx +++ b/ui/src/causestarter/pages/BridgeClusterPage.tsx @@ -104,7 +104,10 @@ export function BridgeClusterPage() { const { address, isConnected } = useAccount() const writeClients = useWriteClients(address) - const routeRef = parseClusterRouteParams(params.owner, params.slugPart) + const routeRef = useMemo( + () => parseClusterRouteParams(params.owner, params.slugPart), + [params.owner, params.slugPart], + ) const localDraftId = params.draftId && !params.owner ? params.draftId : undefined const [draft, setDraft] = useState(null) From e4608e14ce8654fc0a18978ad9f3f889bf03765b Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Tue, 25 Aug 2026 14:57:06 -0400 Subject: [PATCH 07/14] Fix prospective-round seed when the factory address has no bytecode. Empty-account calls succeed with no logs, which looked like a missing ProspectiveRoundCreated event. Skip that scenario without code, fail the SDK with a deploy hint, and require the factory in local config sync. --- fake-data-generation/christian-secular-tiny-seed.md | 4 ++-- fake-data-generation/contentFundingActions.ts | 9 +++++++++ scripts/check-local-config-sync.mjs | 2 ++ sdk/src/subsystems/content-funding/actions.ts | 13 ++++++++++++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/fake-data-generation/christian-secular-tiny-seed.md b/fake-data-generation/christian-secular-tiny-seed.md index 159aa6b5..97241346 100644 --- a/fake-data-generation/christian-secular-tiny-seed.md +++ b/fake-data-generation/christian-secular-tiny-seed.md @@ -44,11 +44,11 @@ Modified texts **copy the commonality sentences** so the attester’s subset rul - [x] CauseStarter click-through on the 2026-08-25 tiny seed (no reseed this pass). - [ ] Optional: align bridge-creator example anchors with these texts later; do not fork a second abortion triple in hidden-majority-patterns. - [x] CauseStarter **bridge cluster** under #8 (`christian-secular`): two modified rosters + bridge roster + cluster document. Tiny seed publishes it; `--cluster-only` republishes against an existing chain. -- [ ] Prospective-round content scenario still failed at last seed (`Failed to find ProspectiveRoundCreated event`); local-food + Christianity continued. Unrelated to this cluster unless it starts blocking tiny. +- [x] Prospective-round content scenario: `Failed to find ProspectiveRoundCreated` was a call to a **no-bytecode** factory address left in `.env` after a chain that never deployed `ProspectiveContentRoundFactory` (empty-account txs succeed with no logs). Seed now skips when `getCode` is empty; SDK `createProspectiveRound` reports missing bytecode instead of a missing event. Local config sync requires `PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS`. 2026-08-25 tiny reseed: open YouTube round `0x147D1dB74c2878E08a6Ac648818421b3d77e90E3`; materialized Substack `0xEa26F3615fd3A84eB5dD24a00E7B4bEc06D63206` → `0xF8ADc47E258b9a56a8E0A717572dB3F1Cb1b4cc4`. ## Still open (resume here) -Nothing blocking this seed. Optional items above. +Optional: align bridge-creator example anchors later. Prospective-round seed is fixed. ## Bridge cluster (2026-08-25) diff --git a/fake-data-generation/contentFundingActions.ts b/fake-data-generation/contentFundingActions.ts index ddd5cf18..7886ca7b 100644 --- a/fake-data-generation/contentFundingActions.ts +++ b/fake-data-generation/contentFundingActions.ts @@ -871,6 +871,15 @@ export async function generateProspectiveContentRoundScenarios( console.warn(' Prospective content round factory not configured — skipping prospective/materialized rounds.'); return; } + { + const factoryCode = await createClients(users[2].privateKey).publicClient.getCode({ address: factory }); + if (!factoryCode || factoryCode === '0x') { + console.warn( + ` Prospective content round factory ${factory} has no bytecode — skipping prospective/materialized rounds. Redeploy with ./scripts/deploy-contracts.sh localhost.`, + ); + return; + } + } if (users.length < 4) { console.warn(' Need at least 4 users for prospective content rounds — skipping.'); return; diff --git a/scripts/check-local-config-sync.mjs b/scripts/check-local-config-sync.mjs index cc6cedc7..0681bfb3 100755 --- a/scripts/check-local-config-sync.mjs +++ b/scripts/check-local-config-sync.mjs @@ -55,6 +55,7 @@ const REQUIRED_ROOT_KEYS = [ 'TRUST_REGISTRY_ADDRESS', 'DELEGATABLE_NOTES_CONTRACT_ADDRESS', 'NOTE_INTENT_ADDRESS', + 'PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS', ]; /** Map root deploy names → VITE_* names expected by SPAs. */ @@ -72,6 +73,7 @@ const ROOT_TO_VITE = { DELEGATABLE_NOTES_CONTRACT_ADDRESS: 'VITE_DELEGATABLE_NOTES_CONTRACT_ADDRESS', NOTE_INTENT_ADDRESS: 'VITE_NOTE_INTENT_CONTRACT_ADDRESS', NUDGE_PUBLICATIONS_CONTRACT_ADDRESS: 'VITE_NUDGE_PUBLICATIONS_CONTRACT_ADDRESS', + PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS: 'VITE_PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS', }; const RUNTIME_CONFIG_URLS = [ diff --git a/sdk/src/subsystems/content-funding/actions.ts b/sdk/src/subsystems/content-funding/actions.ts index 4c6018bb..a1143b3c 100644 --- a/sdk/src/subsystems/content-funding/actions.ts +++ b/sdk/src/subsystems/content-funding/actions.ts @@ -306,8 +306,19 @@ export async function createProspectiveRound( account: clients.walletClient.account!, }); const receipt = await clients.publicClient.waitForTransactionReceipt({ hash }); + if (receipt.status !== 'success') { + throw new Error(`createProspectiveRound reverted (tx ${hash})`); + } const [event] = parseEventLogs({ abi: ProspectiveContentRoundFactoryAbi, eventName: 'ProspectiveRoundCreated', logs: receipt.logs }); - if (!event) throw new Error('Failed to find ProspectiveRoundCreated event in transaction receipt'); + if (!event) { + const code = await clients.publicClient.getCode({ address: factoryAddress }); + if (!code || code === '0x') { + throw new Error( + `No ProspectiveContentRoundFactory bytecode at ${factoryAddress}. Redeploy contracts (./scripts/deploy-contracts.sh localhost) so PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS is live.`, + ); + } + throw new Error('Failed to find ProspectiveRoundCreated event in transaction receipt'); + } return { hash, roundAddress: event.args.round, receiptTokenAddress: event.args.receiptToken, conditionAddress: event.args.condition }; } From 064caab482f98c5109bb502081107b2d988bfdae Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Tue, 25 Aug 2026 15:58:47 -0400 Subject: [PATCH 08/14] Fiddling with the seed statements, though I'm still not happy. --- .../christian-secular-tiny-seed.md | 2 +- .../data/christian-secular-personas.json | 12 +++---- .../christian-secular-bridge.json | 34 +++++++++---------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/fake-data-generation/christian-secular-tiny-seed.md b/fake-data-generation/christian-secular-tiny-seed.md index 97241346..c905e9af 100644 --- a/fake-data-generation/christian-secular-tiny-seed.md +++ b/fake-data-generation/christian-secular-tiny-seed.md @@ -11,7 +11,7 @@ Tiny local seed (`./scripts/data.sh --seed`, i.e. `gen:tiny`) should show two Ca ## Decisions (locked) - **Topics.** Four naturals per camp. Shared: abortion, markets, LGBT unbundling. Unique: Scripture-in-every-language (Christian); colorblind merit / individual equal protection (secular). -- **Patterns.** Abortion = compromise in the middle (within the right, not left/right). Markets = different reasons, same conclusion. LGBT = coalition unbundling (reaffirm kindness / adult liberty; break on rushing minors). Uniques have **no** triple. +- **Patterns.** Abortion = different phrasing, same conclusion (religious vs secular; no 12–16 week deal). Markets = different reasons, same conclusion. LGBT = unbundle gay adults (not enemies; secular also SSM/monogamy) from DQSH, exhibitionist Pride, and the youth medical pipeline. Uniques have **no** triple. - **Cause boards** hold **natural** planks only (4 + 4). **Modified + commonality** are mediator-authored, not swapped onto the camp boards. - **Mediator account:** keep **Hardhat #8** (`FUNDED_HARDHAT_DEV_KEYS[8]`), already `CHRISTIAN_MEDIATOR_*` in `seedChristianityCause.ts`. Do not jump to #19 unless we also move CSM (#7) and fund a high band. Humans stay in #0–#6; #9 remains secular-conservatism founder; #0 remains Christianity / local-food owner. - **Implication direction.** Board for S shows projects aligned with S2 where **S2 implies S**. So a project attested to **modified-Christian** appears on **commonality** (if MC→CG). A secular user who signed **modified-secular** sees it if the UI unions boards of statements they support *including implied CG*. They will not see it on the MS board itself (MC does not imply MS). Include at least one project aligned **only** with a unique plank (negative: other camp must not see it). Include some alignments to **naturals** (should not cross the bridge if natural↛CG). diff --git a/fake-data-generation/data/christian-secular-personas.json b/fake-data-generation/data/christian-secular-personas.json index 5287722d..cdb37eda 100644 --- a/fake-data-generation/data/christian-secular-personas.json +++ b/fake-data-generation/data/christian-secular-personas.json @@ -31,16 +31,16 @@ }, { "id": "first-trimester-clinic", - "name": "First-trimester decision clinic", - "description": "A clinic that offers genuine early-pregnancy options counselling and medical care, and does not offer later-term abortions.", + "name": "Crisis pregnancy help, not an abortuary", + "description": "A clinic that offers ultrasounds, material help, and genuine alternatives to abortion. It does not treat abortion as health care.", "kind": "abortion-bridge", "ownerIndex": 2, "alignments": ["abortion/modified-christian"] }, { "id": "late-term-ban-brief", - "name": "Late-term restriction legal brief", - "description": "A short public brief on enforceable post-16-week restrictions with medical exceptions — not a total ban from conception.", + "name": "Elective-abortion restriction brief", + "description": "A short public brief arguing that elective abortion is not health care and that life-of-the-mother exceptions are not a license for an undo button.", "kind": "abortion-bridge", "ownerIndex": 3, "alignments": ["abortion/modified-secular"] @@ -63,8 +63,8 @@ }, { "id": "minors-transition-pause", - "name": "Minors: exploratory care, not a pipeline", - "description": "A clinic protocol and parent-education series that treats gender-distressed minors with therapy first, not medical transition as the default.", + "name": "Keep schools and libraries off the pipeline", + "description": "Parent-education and school-board work against Drag Queen Story Hour, exhibitionist Pride events around kids, and treating gender-distressed minors as a medical-transition pipeline.", "kind": "lgbt-bridge", "ownerIndex": 5, "alignments": ["lgbt/modified-christian", "lgbt/modified-secular"] diff --git a/fake-data-generation/seed-content/christian-secular-bridge.json b/fake-data-generation/seed-content/christian-secular-bridge.json index 606d5f9c..6ea549fb 100644 --- a/fake-data-generation/seed-content/christian-secular-bridge.json +++ b/fake-data-generation/seed-content/christian-secular-bridge.json @@ -4,7 +4,7 @@ "title": "Christianity × secular conservatism (tiny seed)", "description": "Natural cause planks plus mediator-authored modified/commonality triples. Naturals go on the two CauseStarter boards. Modified and commonality are the mediator cluster. See fake-data-generation/christian-secular-tiny-seed.md.", "notes": [ - "Modified statements copy the commonality sentences so the implication attester can bless modified → commonality as a subset.", + "Commonality is the overlap both modifieds already assert, so the attester can bless modified → commonality as a subset. Camp voice (soul/murder, undo-button, SSM, church vs theory) stays on the modifieds, not on commonality.", "Do not put modified texts on the camp cause boards.", "Uniques have no triple." ], @@ -13,33 +13,33 @@ "id": "abortion", "title": "Abortion", "notes": [ - "Compromise in the middle, inside the right: sanctity-of-life vs late-term horror / no theocracy. Not the left/right abortion triple in hidden-majority-patterns.md." + "Different phrasing, same conclusion — not a gestational-line compromise. Christians use soul/murder language; secular conservatives do not. Not the left/right abortion triple in hidden-majority-patterns.md." ], "statements": [ { "id": "natural-christian", "role": "natural-christian", - "text": "Abortion ends a child's life. I don't think of it as ordinary healthcare, and I want far fewer of them." + "text": "An unborn child still has a soul. Unless the mother's life is at stake, abortion isn't health care. Thou shalt not murder." }, { "id": "natural-secular", "role": "natural-secular", - "text": "Late-term abortion is horrific. Early pregnancy is more complicated, and I don't want the state enforcing a church teaching on people who don't believe it." + "text": "Abortion ends a child's life. Maybe there are reasonable exceptions for mother's health and rape, but the overwhelming majority of abortions are done by irresponsible people who simply want an undo button." }, { "id": "modified-christian", "role": "modified-christian", - "text": "Abortion ends a child's life, and I'd prefer far fewer abortions. I still don't want the law to leave a woman no time to decide. I'd be okay with it if abortion were allowed during the first 12-16 weeks and forbidden after that except in serious medical situations. This isn't my ideal outcome, but I'd rather get this settled than keep fighting over it forever." + "text": "An unborn child still has a soul. Abortion ends a child's life. Unless the mother's life is at stake, abortion isn't health care. Thou shalt not murder." }, { "id": "modified-secular", "role": "modified-secular", - "text": "Late-term abortion is horrific. I'd still rather not see abortions early in pregnancy, but I don't feel as strongly about that, and I don't need the law to encode a religious claim. I'd be okay with it if abortion were allowed during the first 12-16 weeks and forbidden after that except in serious medical situations. This isn't my ideal outcome, but I'd rather get this settled than keep fighting over it forever." + "text": "Abortion ends a child's life. Unless the mother's life is at stake, abortion isn't health care. Maybe there are reasonable exceptions for mother's health and rape, but the overwhelming majority of abortions are done by irresponsible people who simply want an undo button." }, { "id": "commonality", "role": "commonality", - "text": "I'd be okay with it if abortion were allowed during the first 12-16 weeks and forbidden after that except in serious medical situations. This isn't my ideal outcome, but I'd rather get this settled than keep fighting over it forever." + "text": "Abortion ends a child's life. Unless the mother's life is at stake, abortion isn't health care." } ], "implicationNotes": [ @@ -67,17 +67,17 @@ { "id": "modified-christian", "role": "modified-christian", - "text": "I come to this because I think caring for the poor is the church's work, not because I think the market is an idol. I want ordinary people to be able to earn a living and keep more of what they earn. I do not want a larger welfare state as the default way of providing for people; private charity and local help should do more of that work. The remaining safety-net details can be argued separately, in good faith." + "text": "I come to this because I think caring for the poor is the church's work, not because I think the market is an idol. Still, markets do generally create prosperity, and I want ordinary people to be able to earn a living and keep more of what they earn. I do not want a larger welfare state as the default way of providing for poor people; private charity and local help, including the church, should do more of that work." }, { "id": "modified-secular", "role": "modified-secular", - "text": "I come to this from observed outcomes, not from Scripture. I want ordinary people to be able to earn a living and keep more of what they earn. I do not want a larger welfare state as the default way of providing for people; private charity and local help should do more of that work. The remaining safety-net details can be argued separately, in good faith." + "text": "I come to this from observed outcomes and economic theory, not for religious reasons. Markets do generally create prosperity, and I want ordinary people to be able to earn a living and keep more of what they earn. I do not want a larger welfare state as the default way of providing for poor people; private charity and local help, including religious organizations, should do more of that work." }, { "id": "commonality", "role": "commonality", - "text": "I want ordinary people to be able to earn a living and keep more of what they earn. I do not want a larger welfare state as the default way of providing for people; private charity and local help should do more of that work. The remaining safety-net details can be argued separately, in good faith." + "text": "Markets do generally create prosperity, and I want ordinary people to be able to earn a living and keep more of what they earn. I do not want a larger welfare state as the default way of providing for poor people; private charity and local help should do more of that work." } ], "implicationNotes": [ @@ -89,38 +89,38 @@ "id": "lgbt", "title": "LGBT unbundling", "notes": [ - "Atomize adult gay neighbors from rushing minors into medical transition. Reaffirm so signing is not betrayal. Commonality does not require Christians to affirm same-sex marriage or secular signers to affirm a theology of sex." + "Unbundle gay adults from public sexualization and from rushing minors into medical transition. Commonality does not require Christians to affirm same-sex marriage or secular signers to call homosexual acts a sin." ], "statements": [ { "id": "natural-christian", "role": "natural-christian", - "text": "Marriage, as I understand it from Scripture, is a man and a woman. I still don't want to treat gay people as enemies." + "text": "Scripture says marriage is between a man and a woman. Gay people are not my enemy, but I do believe that what they're doing is a sin." }, { "id": "natural-secular", "role": "natural-secular", - "text": "Gay adults should be able to marry. Schools treating gender-distressed kids as a medical-transition pipeline is a different issue, and I don't want that." + "text": "Gay adults should be able to marry; they're participating as best they're able in upholding healthy societal norms of monogamy. That is very different from Drag Queen Story Hour, or \"Pride\" parades where scantily-clad men prance around in broad daylight, or schools treating gender-distressed kids as a medical-transition pipeline; those are destructive." }, { "id": "modified-christian", "role": "modified-christian", - "text": "I still believe marriage, in the Christian sense, is a man and a woman. Gay adults should be treated as neighbors, not enemies. Whatever the civil law does about adult relationships, I do not want schools or clinics to rush minors into medical transition." + "text": "Scripture says marriage is between a man and a woman. I do believe that what they're doing is a sin. Gay people are not my enemy, but Drag Queen Story Hour, \"Pride\" parades where scantily-clad men prance around in broad daylight, and schools treating gender-distressed kids as a medical-transition pipeline are destructive." }, { "id": "modified-secular", "role": "modified-secular", - "text": "I support same-sex civil marriage for adults. Gay adults should be treated as neighbors, not enemies. Whatever the civil law does about adult relationships, I do not want schools or clinics to rush minors into medical transition." + "text": "Gay adults should be able to marry; they're participating as best they're able in upholding healthy societal norms of monogamy. Gay people are not my enemy, but Drag Queen Story Hour, \"Pride\" parades where scantily-clad men prance around in broad daylight, and schools treating gender-distressed kids as a medical-transition pipeline are destructive." }, { "id": "commonality", "role": "commonality", - "text": "Gay adults should be treated as neighbors, not enemies. Whatever the civil law does about adult relationships, I do not want schools or clinics to rush minors into medical transition." + "text": "Gay people are not my enemy, but Drag Queen Story Hour, \"Pride\" parades where scantily-clad men prance around in broad daylight, and schools treating gender-distressed kids as a medical-transition pipeline are destructive." } ], "implicationNotes": [ "Expect yes: modified-christian → commonality, modified-secular → commonality.", - "Expect no: natural-christian → commonality (does not mention minors / medical transition); modified-christian → modified-secular (adds SSM)." + "Expect no: natural-christian → commonality (does not mention DQSH / Pride / the pipeline); modified-christian → modified-secular (adds SSM and drops sin)." ] }, { From 9c2e17b243c37441ccce7a1f55e7240b75332a55 Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Tue, 25 Aug 2026 16:02:08 -0400 Subject: [PATCH 09/14] Added localhost contract manifest to .gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 19ef2e42..485ca174 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ services/implication-attester/dist published-data-ipfs-mirror/dist/ hardhat/deployments/ deployments/localhost.env +deployments/localhost.contracts-manifest.json deployments/operator-addresses.env /data sdk/src/generated/ From 2ed636a36f0e843bda92f60cbd0610aa5e099425 Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Tue, 25 Aug 2026 16:18:57 -0400 Subject: [PATCH 10/14] Added env var. --- causestarter/docker-entrypoint.d/40-causestarter-config.sh | 1 + docker-compose.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/causestarter/docker-entrypoint.d/40-causestarter-config.sh b/causestarter/docker-entrypoint.d/40-causestarter-config.sh index ab0f3b45..58c11974 100755 --- a/causestarter/docker-entrypoint.d/40-causestarter-config.sh +++ b/causestarter/docker-entrypoint.d/40-causestarter-config.sh @@ -49,6 +49,7 @@ write_kv VITE_CONTENT_REGISTRY_ADDRESS "${VITE_CONTENT_REGISTRY_ADDRESS:-}" write_kv VITE_CHANNEL_REGISTRY_ADDRESS "${VITE_CHANNEL_REGISTRY_ADDRESS:-}" write_kv VITE_CHANNEL_ESCROW_ADDRESS "${VITE_CHANNEL_ESCROW_ADDRESS:-}" write_kv VITE_CREATOR_CONTRACT_FACTORY_ADDRESS "${VITE_CREATOR_CONTRACT_FACTORY_ADDRESS:-}" +write_kv VITE_PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS "${VITE_PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS:-}" write_kv VITE_PROJECT_FACTORY_CONTRACT_ADDRESS "${VITE_PROJECT_FACTORY_CONTRACT_ADDRESS:-}" write_kv VITE_PAYMENT_TOKEN_ADDRESS "${VITE_PAYMENT_TOKEN_ADDRESS:-}" write_kv VITE_CHAIN_ID "${VITE_CHAIN_ID:-}" diff --git a/docker-compose.yml b/docker-compose.yml index 0983d76c..8f8b2810 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -582,6 +582,7 @@ services: VITE_CHANNEL_REGISTRY_ADDRESS: ${VITE_CHANNEL_REGISTRY_ADDRESS:-} VITE_CHANNEL_ESCROW_ADDRESS: ${VITE_CHANNEL_ESCROW_ADDRESS:-} VITE_CREATOR_CONTRACT_FACTORY_ADDRESS: ${VITE_CREATOR_CONTRACT_FACTORY_ADDRESS:-} + VITE_PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS: ${VITE_PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS:-} VITE_PROJECT_FACTORY_CONTRACT_ADDRESS: ${VITE_PROJECT_FACTORY_CONTRACT_ADDRESS:-} VITE_PAYMENT_TOKEN_ADDRESS: ${VITE_PAYMENT_TOKEN_ADDRESS:-} VITE_CHAIN_ID: ${VITE_CHAIN_ID:-31337} From 36f6e4d1ffe17cd36b9bc63a1f317ee38ac10b38 Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Tue, 25 Aug 2026 17:05:51 -0400 Subject: [PATCH 11/14] Still working on the tiny seed data, but I'm realizing that this is the wrong example to start with. --- .../christian-secular-tiny-seed.md | 6 +- .../christian-secular-bridge.json | 47 ++++++++-------- ...tatements-are-peculiar-for-good-reasons.md | 55 +++++++++++++++++++ 3 files changed, 84 insertions(+), 24 deletions(-) diff --git a/fake-data-generation/christian-secular-tiny-seed.md b/fake-data-generation/christian-secular-tiny-seed.md index c905e9af..6653750b 100644 --- a/fake-data-generation/christian-secular-tiny-seed.md +++ b/fake-data-generation/christian-secular-tiny-seed.md @@ -34,7 +34,7 @@ Modified texts **copy the commonality sentences** so the attester’s subset rul - [x] This plan file. - [x] Author seed JSON (8 naturals + 3 triples). -- [x] Live attester (2026-08-25, `deepseek/deepseek-v3.2`): all 6 designed **yes** pairs blessed (high / subset); all 6 designed **no** pairs refused. Script: `npm run gen:seed:christian-secular-implications`. +- [x] Rewrite seed JSON off subset-concatenation (2026-08-25): naturals as speech; modifieds keep *why* + limiting principle; commonality last. Live attester: all 6 designed **yes** blessed (high / subset); all 6 designed **no** refused. Script: `npm run gen:seed:christian-secular-implications`. CIDs changed — tiny reseed still needed for the running chain. - [x] Point `CHRISTIANITY_PLANKS` / `SECULAR_CONSERVATIVE_PLANKS` at the naturals; publish modified+CG as mediator (#8) statements. - [x] Persona JSON (`data/christian-secular-personas.json`) + driver in `seedChristianityCause.ts`: persona-based signs, 10 projects, mixed natural vs modified alignments, unique-only scripture + colorblind negatives. - [x] Make `gen:tiny` skip the 12 random `universe.json` statements (`--statement-limit=0`; Christianity/secular + local-food still seed). @@ -50,6 +50,10 @@ Modified texts **copy the commonality sentences** so the attester’s subset rul Optional: align bridge-creator example anchors later. Prospective-round seed is fixed. +**This pairing is a weak first exercise of the implication system (2026-08-25).** Christian × secular-conservative is a real alliance type (groups already close; they agree on the *policy*; they mistrust each other’s *why*). For that pattern the honest commonality *is* just the policy. That is why the prose kept collapsing: slogan-glue, then “I don’t need your reasons,” then “we come from different places,” then the policy twice. Nothing left to peculiar-ize. Fine as a CauseStarter demo of two nearby camps. **Bad as the tiny seed’s only test of modifieds, nudges, and the attester**, which exist to handle a deal one side would not write on their own (overlap-zone compromise, bilateral assurance, unbundling that costs something, a conditional on a fact fight). Locked topic list above mixed those jobs. Do not keep polishing this triple as if more wording will make it a compromise-in-the-middle. Next: either (a) keep Christianity / secular boards and add a *second* cluster that is actually a policy gap (canonical left/right abortion/immigration — reuse hidden-majority-patterns, do not fork a second abortion *wording*), or (b) replace the featured tiny-seed bridge with that gap and keep this pairing as optional later. Uniques (scripture, colorblind) stay useful either way. + +**Prose rewrite (2026-08-25).** Draft-order rewrite, then drop coalition-narration. Commonality is the civic conclusion only. Live attester 6 yes / 6 no. CIDs change — tiny reseed still needed if this JSON is what you publish. + ## Bridge cluster (2026-08-25) Hardhat #8 refs: diff --git a/fake-data-generation/seed-content/christian-secular-bridge.json b/fake-data-generation/seed-content/christian-secular-bridge.json index 6ea549fb..cafb871d 100644 --- a/fake-data-generation/seed-content/christian-secular-bridge.json +++ b/fake-data-generation/seed-content/christian-secular-bridge.json @@ -4,54 +4,55 @@ "title": "Christianity × secular conservatism (tiny seed)", "description": "Natural cause planks plus mediator-authored modified/commonality triples. Naturals go on the two CauseStarter boards. Modified and commonality are the mediator cluster. See fake-data-generation/christian-secular-tiny-seed.md.", "notes": [ - "Commonality is the overlap both modifieds already assert, so the attester can bless modified → commonality as a subset. Camp voice (soul/murder, undo-button, SSM, church vs theory) stays on the modifieds, not on commonality.", - "Do not put modified texts on the camp cause boards.", - "Uniques have no triple." + "Draft order: gap named, naturals as speech, modifieds as smallest belief-change still in that camp's voice, commonality last, then check that each modified actually claims what the commonality claims. Camp *why* stays on the modifieds.", + "This pairing is mostly different-reasons-same-conclusion plus limiting principle — not a left/right gestational compromise. Do not put modified texts on the camp cause boards.", + "Uniques have no triple.", + "Prose target: family-formation / kids-and-tech voice in services/bridge-creator/config/christian-secular-conservative.example.json. Containment is a check, not copy-paste." ], "groups": [ { "id": "abortion", "title": "Abortion", "notes": [ - "Different phrasing, same conclusion — not a gestational-line compromise. Christians use soul/murder language; secular conservatives do not. Not the left/right abortion triple in hidden-majority-patterns.md." + "Different reasons, same conclusion — not the left/right 12–16 week deal. Naturals stay in camp voice. Modifieds keep the why and a first-person limit (not a theocracy). Commonality is only the civic pair — no 'we come from different places' narrator." ], "statements": [ { "id": "natural-christian", "role": "natural-christian", - "text": "An unborn child still has a soul. Unless the mother's life is at stake, abortion isn't health care. Thou shalt not murder." + "text": "An unborn child still has a soul. Taking that life is murder." }, { "id": "natural-secular", "role": "natural-secular", - "text": "Abortion ends a child's life. Maybe there are reasonable exceptions for mother's health and rape, but the overwhelming majority of abortions are done by irresponsible people who simply want an undo button." + "text": "Abortion ends a child's life. Maybe rape and the mother's health are real edge cases, but the overwhelming majority are people who simply want an undo button." }, { "id": "modified-christian", "role": "modified-christian", - "text": "An unborn child still has a soul. Abortion ends a child's life. Unless the mother's life is at stake, abortion isn't health care. Thou shalt not murder." + "text": "An unborn child still has a soul, and taking that life is murder — that's why this matters to me. Elective abortion should not be treated as ordinary health care. A threat to the mother's life is not a license for an undo button. I am not asking the state to make anyone pray." }, { "id": "modified-secular", "role": "modified-secular", - "text": "Abortion ends a child's life. Unless the mother's life is at stake, abortion isn't health care. Maybe there are reasonable exceptions for mother's health and rape, but the overwhelming majority of abortions are done by irresponsible people who simply want an undo button." + "text": "Abortion ends a child's life; what I see in the ordinary case is an undo button, not medicine. Elective abortion should not be treated as ordinary health care. A threat to the mother's life is not a license for an undo button." }, { "id": "commonality", "role": "commonality", - "text": "Abortion ends a child's life. Unless the mother's life is at stake, abortion isn't health care." + "text": "Elective abortion should not be treated as ordinary health care. A threat to the mother's life is not a license for an undo button." } ], "implicationNotes": [ "Expect yes: modified-christian → commonality, modified-secular → commonality.", - "Expect no: either natural → commonality; either modified → the other modified; commonality → either modified." + "Expect no: either natural → commonality (naturals never state the civic pair); either modified → the other modified; commonality → either modified." ] }, { "id": "markets", "title": "Markets and provision for the poor", "notes": [ - "Different reasons, same conclusion. Commonality is the conclusion with neither stewardship nor Hayek attached." + "Different reasons, same conclusion. Commonality is only the conclusion — neither stewardship, nor Hayek, nor a comment on whose why." ], "statements": [ { @@ -67,29 +68,29 @@ { "id": "modified-christian", "role": "modified-christian", - "text": "I come to this because I think caring for the poor is the church's work, not because I think the market is an idol. Still, markets do generally create prosperity, and I want ordinary people to be able to earn a living and keep more of what they earn. I do not want a larger welfare state as the default way of providing for poor people; private charity and local help, including the church, should do more of that work." + "text": "Caring for the poor is the church's work — neighbors, not clients of an office. Markets generally let ordinary people earn a living and keep more of what they earn. Private charity and local help, including the church, should do more of providing for poor people than a larger welfare state." }, { "id": "modified-secular", "role": "modified-secular", - "text": "I come to this from observed outcomes and economic theory, not for religious reasons. Markets do generally create prosperity, and I want ordinary people to be able to earn a living and keep more of what they earn. I do not want a larger welfare state as the default way of providing for poor people; private charity and local help, including religious organizations, should do more of that work." + "text": "The dependence numbers and the growth numbers are enough for me. Markets generally let ordinary people earn a living and keep more of what they earn. Private charity and local help, including churches I don't sit in, should do more of providing for poor people than a larger welfare state." }, { "id": "commonality", "role": "commonality", - "text": "Markets do generally create prosperity, and I want ordinary people to be able to earn a living and keep more of what they earn. I do not want a larger welfare state as the default way of providing for poor people; private charity and local help should do more of that work." + "text": "Markets generally let ordinary people earn a living and keep more of what they earn. Private charity and local help should do more of providing for poor people than a larger welfare state." } ], "implicationNotes": [ "Expect yes: modified-christian → commonality, modified-secular → commonality.", - "Expect no: either natural → commonality." + "Expect no: either natural → commonality (no shared civic formulation)." ] }, { "id": "lgbt", "title": "LGBT unbundling", "notes": [ - "Unbundle gay adults from public sexualization and from rushing minors into medical transition. Commonality does not require Christians to affirm same-sex marriage or secular signers to call homosexual acts a sin." + "Unbundle gay adults from sexualizing children and from rushing minors into medical transition. Christian natural stays marriage/sin/'not my enemy' and does not already name the civic list. Modified-christian reaffirms the faith bundle, then states the civic piece so signing is not a conversion. Commonality does not require SSM or 'this is a sin.'" ], "statements": [ { @@ -100,33 +101,33 @@ { "id": "natural-secular", "role": "natural-secular", - "text": "Gay adults should be able to marry; they're participating as best they're able in upholding healthy societal norms of monogamy. That is very different from Drag Queen Story Hour, or \"Pride\" parades where scantily-clad men prance around in broad daylight, or schools treating gender-distressed kids as a medical-transition pipeline; those are destructive." + "text": "Gay adults should be able to marry; they're participating as best they're able in upholding healthy societal norms of monogamy. That is very different from putting children in sexualized public events, or from schools treating gender-distressed kids as a medical-transition pipeline." }, { "id": "modified-christian", "role": "modified-christian", - "text": "Scripture says marriage is between a man and a woman. I do believe that what they're doing is a sin. Gay people are not my enemy, but Drag Queen Story Hour, \"Pride\" parades where scantily-clad men prance around in broad daylight, and schools treating gender-distressed kids as a medical-transition pipeline are destructive." + "text": "Scripture still says marriage is between a man and a woman, and I still believe homosexual acts are a sin — I am not signing this as a way of taking that back. Gay adults are not my enemies. Children should not be put in sexualized public events, including drag story hours and exhibitionist Pride in front of kids, and schools should not treat gender-distressed minors as a medical-transition pipeline. I can hold all of that without pretending I now bless same-sex marriage, and without asking anyone else to call it sin." }, { "id": "modified-secular", "role": "modified-secular", - "text": "Gay adults should be able to marry; they're participating as best they're able in upholding healthy societal norms of monogamy. Gay people are not my enemy, but Drag Queen Story Hour, \"Pride\" parades where scantily-clad men prance around in broad daylight, and schools treating gender-distressed kids as a medical-transition pipeline are destructive." + "text": "Gay adults should be able to marry; they're participating as best they're able in upholding healthy societal norms of monogamy, and I am not taking that back. Gay adults are not my enemies. Children should not be put in sexualized public events, including drag story hours and exhibitionist Pride in front of kids, and schools should not treat gender-distressed minors as a medical-transition pipeline. I can hold that without attending church, and without asking Christians to bless the marriages." }, { "id": "commonality", "role": "commonality", - "text": "Gay people are not my enemy, but Drag Queen Story Hour, \"Pride\" parades where scantily-clad men prance around in broad daylight, and schools treating gender-distressed kids as a medical-transition pipeline are destructive." + "text": "Gay adults are not my enemies. Children should not be put in sexualized public events, including drag story hours and exhibitionist Pride in front of kids, and schools should not treat gender-distressed minors as a medical-transition pipeline." } ], "implicationNotes": [ "Expect yes: modified-christian → commonality, modified-secular → commonality.", - "Expect no: natural-christian → commonality (does not mention DQSH / Pride / the pipeline); modified-christian → modified-secular (adds SSM and drops sin)." + "Expect no: natural-christian → commonality (does not name the civic list); modified-christian → modified-secular (adds SSM and drops sin)." ] }, { "id": "scripture", "title": "Scripture available (Christian unique)", - "notes": ["No bridge triple."], + "notes": ["No bridge triple. Ordinary single-issue plank; does not need peculiar syntax."], "statements": [ { "id": "natural-christian", @@ -138,7 +139,7 @@ { "id": "colorblind-merit", "title": "Colorblind merit (secular unique)", - "notes": ["No bridge triple."], + "notes": ["No bridge triple. Ordinary single-issue plank; does not need peculiar syntax."], "statements": [ { "id": "natural-secular", diff --git a/specs/product/statements-are-peculiar-for-good-reasons.md b/specs/product/statements-are-peculiar-for-good-reasons.md index 4e45a9cc..b0ec52da 100644 --- a/specs/product/statements-are-peculiar-for-good-reasons.md +++ b/specs/product/statements-are-peculiar-for-good-reasons.md @@ -78,6 +78,55 @@ Drawn from the patterns page and the conditional-support design notes — not a Poles stay short and extreme on purpose. Naturals stay how people actually talk (often too thin to imply commonality). Only modifieds and commonality have to be “peculiar.” +## How to draft (containment is a check, not a method) + +The implication attester is conservative on **subset / entailment**. That tempts authors to *assemble* a commonality from sentences, then paste those sentences into each modified text so the bless is guaranteed. That is how you get a graph that is technically correct and statements nobody would sign. + +Write in this order: + +1. **Name the gap**, using the [hidden-majority patterns](/docs/end-user/common-sense-majority/hidden-majority-patterns.md). The pattern decides the *shape* of the commonality (deal in the overlap; obvious consensus; “if X then Y”; corrective; unbundled piece; policy with neither *why*). If you cannot name the gap, you are not ready to write the triple. +2. **Write the naturals as speech.** How would this person actually talk? Do not withhold a sentence from the natural just so the modified can “add” it and win a bless. If the natural already implies the commonality, that is a fact about the issue (often “no major controversy”) — maybe you do not need a triple, or the modified’s job is a *limiting principle*, not extra slogans. +3. **Write each modified as a person still on that side**, with the smallest change in **belief content** that makes the commonality already true in their mouth. String-diff can be small while the belief jump is huge (a Christian natural about marriage-as-sin that suddenly lists Drag Queen Story Hour, Pride, and the youth medical pipeline is not a small modification). +4. **Write the commonality last**, as something both modifieds already said, with camp *whys* stripped. Then **check** containment. If you can only get a bless by copy-pasting identical clauses, the commonality is too slogan-like or the modifieds are too vague — rewrite the prose, do not glue. + +Read each text aloud as a signature. A parishioner, a Reason-reader, a tired moderate — would they put their name on this *paragraph*, not on the topic? + +### One voice, one job + +- **One register per statement.** Do not concatenate King James, a tweet, and an essay. Three slogans stacked is not a statement. +- **No mediator meta.** Openings like “I come to this because I think X, not because Y” are the graph talking. So is a commonality that announces the coalition: “we come to this from different places,” “we don’t have to settle why first.” A signer is not writing a caption for a bridge diagram. Keep each side’s *why* on the modified; strip it from the commonality by **omission**. If you need a limit, write it in the first person on that side’s modified (“I am not asking the state to make anyone pray”), not as a comment on whose reasons are in play. +- **Commonality is not a rant with the theology sanded off.** If the shared text is still one camp’s cadence plus a defensive throat-clearing (“not my enemy, but [list of things I hate]”), you have not found the overlap; you have selected a culture-war shopping list and called it a bridge. +- **Uniques can stay ordinary.** A cause plank that is not in a triple does not need peculiar syntax. “Everyone should be able to read Scripture in their own language” is the right shape for a unique. + +### When the two sides are already allies + +Christianity × secular conservatism is **not** a left/right fight. They often already share the conclusion; they mistrust each other’s *reasons* and imagined maximalism. The [bridge-creator example strategy](/services/bridge-creator/config/christian-secular-conservative.example.json) is the right brief: different reasons, same conclusion; make the limiting principle explicit; do not smuggle God-given into a secular signature or reduce faith to “studies show.” + +That pattern is real, and the honest commonality **is** just the policy. Do not invent peculiar syntax to pretend there is a deal. **Do not use it as the first (or only) exercise of the implication system.** Tiny seed asked this pairing to both populate two CauseStarter boards *and* demonstrate modifieds / attester / nudges; the second job needs a gap where the commonality is something neither natural would say (compromise in the overlap, bilateral assurance, a costly unbundle, a fact-conditional). See [christian-secular-tiny-seed.md](/fake-data-generation/christian-secular-tiny-seed.md) § Still open. + +For that pairing, a triple whose commonality is just two campaign slogans (and whose modifieds are those slogans glued onto the naturals) is decorative. The load-bearing extra is usually: + +- keep each side’s *why* on the modified only; +- state the shared conclusion without either foundation (that omission *is* the protection against adopting the other camp’s metaphysics — you do not need a sentence that says so); +- on the modified, say where you **stop**, in the first person (not a theocracy; not waiting for the churches to die). + +A cooperative closer (“we come from different places”) is the same function as “I don’t need your reasons,” only politer, and it still reads as the statement knowing it is a bridge. Prefer not to use it. The family-formation anchors in the example config still have that closer; treat them as a reasons-kept / conclusion-shared *shape*, not as a license to narrate the coalition. + +Compare the family-formation anchors in that example config (a person talking, reasons kept, conclusion shared) with the 2026-08-25 tiny-seed abortion triple (slogan concatenation). The attester blessed both shapes. Only one is a demonstration of the product. + +### Tiny seed (what failed, then the rewrite) + +The 2026-08-25 [`christian-secular-bridge.json`](/fake-data-generation/seed-content/christian-secular-bridge.json) passed live attester designed-yes/designed-no by **subset-concatenation**. That is necessary and not sufficient. Failures of *shape*: + +| Group | What went wrong | +|---|---| +| Abortion | Pattern was “same conclusion, different language,” executed as copy-paste. Natural Christian already had “isn’t health care”; modified only inserts “ends a child’s life” so subset can fire. Commonality is two slogans, no reservation, no limiting principle. | +| Markets | Closest to “different problems, same solution,” then opens with mediator-meta (“I come to this because…”). | +| LGBT | Unbundling, but the Christian natural never mentioned the public-sexualization / youth-pipeline piece; modified *adds a program* without the verbose “I am not converting.” Commonality is “not my enemy, but” + a vivid hate-list. | +| Uniques | These already sounded signable. That was a hint: the triples overfit the attester. | + +The later rewrite in that JSON follows the draft order above. Copy the *roles* (naturals on camp boards, modified+CG from the mediator, uniques with no triple). Style target remains the family-formation / kids-and-tech / religious-liberty anchors in the example mediator config. + ## Map of the rest of the repo Read these; do not copy them into this file. @@ -112,3 +161,9 @@ Read these; do not copy them into this file. - Natural camp talk treated as if it already implied the deal. - Asking the attester to connect “I care about X” and “I care about Y” into “I care about X and Y.” - Putting words in mouths at misunderstanding-pattern scale without persuasion content (that’s [Civility / noninflammatory content](/docs/end-user/shared/use-case-walkthroughs/noninflammatory-content.md), not the attester). +- **Subset-by-concatenation:** commonality sentences pasted into each modified so the attester’s subset rule fires. A bless is a check on a draft, not a drafting algorithm. +- **Withholding a line from the natural** so the modified can add it. Naturals are how people talk; they are not a puzzle box. +- **Mediator-meta openings** (“I come to this because… not because…”). +- **A commonality that still belongs to one camp’s rant**, with the other camp’s theology deleted. +- **Belief jumps disguised as small edits** (unbundling that introduces issues the natural never held, without the verbose reaffirmation *and* without treating that as persuasion). +- **Equidistant-by-default.** The commonality sits where the supermajority actually is, including when that is “extreme.” From 66523557ed6cdbd714f1804376c930e35e63203d Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Wed, 26 Aug 2026 13:33:57 -0400 Subject: [PATCH 12/14] Document implication-vs-nudge routing and wire it into writer checks. Add the reasonable-annoyance test to the peculiar-statements spec as a pair checker, point nudger/seed/CSM docs at it, and teach cause-assist critique and statement guidance to apply it without loosening the attester. --- cause-assist/README.md | 2 +- cause-assist/src/bridgeClusterAssist.test.ts | 1 + cause-assist/src/bridgeClusterAssist.ts | 4 +++- cause-assist/src/statementGuidance.ts | 1 + .../hidden-majority-patterns.md | 2 ++ docs/end-user/tally/suggestions-and-nudges.md | 4 ++-- docs/founder/bridge-cluster-wording-help.md | 2 +- .../christian-secular-tiny-seed.md | 4 +++- .../bridge-creator/prompts/csm-strategy.md | 2 ++ ...statements-are-peculiar-for-good-reasons.md | 18 ++++++++++++++++++ .../conceptspace/seed-content/README.md | 5 +++-- specs/tech/subsystems/nudger/README.md | 6 +++--- 12 files changed, 40 insertions(+), 11 deletions(-) diff --git a/cause-assist/README.md b/cause-assist/README.md index 7f18e11f..bacdefce 100644 --- a/cause-assist/README.md +++ b/cause-assist/README.md @@ -35,7 +35,7 @@ See `src/statementGuidance.ts` and the Implication Attester evaluator prompt for | POST | `/draft-modified-plank` | `{ parentPlanks[], currentDraft?, sideLabel?, mustNotConcede?, complaint? }` | One modified-plank proposal for a human-authored bridge cluster. Not a chat turn. Refuses empty parents. | | POST | `/draft-stand-in-sliver` | `{ sideLabel, bullets?, mustNotCaricature?, complaint?, currentDraft? }` | Thin roster for a camp with no published cause. Not a modified-plank call. | | POST | `/draft-bridge-plank` | `{ modifiedSides[{ label?, planks[] }], currentDraft?, complaint? }` | One shared-platform plank from ≥2 modified sides. Strips justifications. | -| POST | `/critique-triple` | `{ modifiedPlanks[], bridgePlank }` | Objections and justification-leak warnings only — no rewrite | +| POST | `/critique-triple` | `{ modifiedPlanks[], bridgePlank }` | Objections (including implication-vs-nudge `routing:`), justification-leak warnings — no rewrite | | POST | `/check-implications` | `{ mainStatement, supportingStatements[] }` | Per-pair implies / confidence / reasoning | | POST | `/safety-check` | `{ items: [{ text, fieldLabel? }] }` | Per-item allow/deny + user-facing explanation | | POST | `/check-coherence` | `{ rosterCid, title, summary, planks[], mediatorBlurb? }` | Positive-only construction check for a would-be roster CID (preview; no chain write; may use heuristic without an API key) | diff --git a/cause-assist/src/bridgeClusterAssist.test.ts b/cause-assist/src/bridgeClusterAssist.test.ts index cbb05ac4..1518f0cf 100644 --- a/cause-assist/src/bridgeClusterAssist.test.ts +++ b/cause-assist/src/bridgeClusterAssist.test.ts @@ -48,6 +48,7 @@ describe('bridge cluster wording verbs', () => { bridgePlank: 'Marriage is a gift from God and also the data says so.', }, config, async (request: LlmJsonRequest) => { assert.match(request.systemPrompt, /Do not rewrite/) + assert.match(request.systemPrompt, /routing:/) return { objections: ['Shared plank requires a theological premise.'], leakWarnings: ['God-talk leaked into the bridge plank.'], diff --git a/cause-assist/src/bridgeClusterAssist.ts b/cause-assist/src/bridgeClusterAssist.ts index 3d9fbe20..7fc06e46 100644 --- a/cause-assist/src/bridgeClusterAssist.ts +++ b/cause-assist/src/bridgeClusterAssist.ts @@ -144,9 +144,11 @@ export const critiqueTripleStrategy: StatementStrategy< name: 'cause-assist-critique-triple', systemPrompt: `You critique a proposed bridge triple. Do not rewrite. List objections a fair-minded person on each side would raise, and flag any justification leak into the shared plank (theology in a secular-signable claim, or reducing a faith claim to "studies show"). +Also apply the implication-vs-nudge routing test. For each modified plank → bridge plank: if a reasonable signer of the modified would be annoyed at being asked to explicitly sign the bridge ("I already said that"), the pair should be an implication (containment). If they would not be annoyed, the modified does not contain the shared claim yet — object. If they would be annoyed but a different reasonable person would see a real extra claim in the bridge, do not treat that as containment; object that the pair is a nudge (or that the wording hides the delta), not an implication. Unreasonable annoyance is not a reason to bless an arrow. + ${MEDIATION_RULES} -Return JSON only: {"objections":["..."],"leakWarnings":["..."]}. Empty arrays mean you found nothing load-bearing to flag.`, +Return JSON only: {"objections":["..."],"leakWarnings":["..."]}. Empty arrays mean you found nothing load-bearing to flag. Prefix routing failures with "routing:" (e.g. "routing: modified does not contain the bridge; a signer would not find a separate signature redundant").`, renderInput: (input) => ({ modified_planks: input.modifiedPlanks, bridge_plank: input.bridgePlank, diff --git a/cause-assist/src/statementGuidance.ts b/cause-assist/src/statementGuidance.ts index 31f94030..2958cdad 100644 --- a/cause-assist/src/statementGuidance.ts +++ b/cause-assist/src/statementGuidance.ts @@ -19,4 +19,5 @@ Implication rule for supporting statements (critical): - Reject additions of policy, acceptance, concessions, reservations, bilateral commitments, or ambiguous speech acts; changes of strength or quantifier; and claims that depend on guessed context. - Do not reject merely because S2 is broad, permits multiple implementations, or leaves details unsettled. - Implication is stronger than topical relatedness. Do not draft "drivers," "principles," or "why it matters" extras unless they are already entailed by the main wording. +- A supporting statement should feel redundant to sign separately: a reasonable signer of the main statement would be annoyed at being asked to also sign it ("I already said that"). If they would not be annoyed, you invented a new claim — drop it or keep it off the implication path. - When in doubt, do not suggest the supporting statement.` diff --git a/docs/end-user/common-sense-majority/hidden-majority-patterns.md b/docs/end-user/common-sense-majority/hidden-majority-patterns.md index 8211773e..da3c8a94 100644 --- a/docs/end-user/common-sense-majority/hidden-majority-patterns.md +++ b/docs/end-user/common-sense-majority/hidden-majority-patterns.md @@ -28,6 +28,8 @@ The modified statements are the load-bearing part of the work — and the subtle If a modification buys the implication but no one on that side would sign it, the mediator has failed. If it's signable but the implication doesn't actually hold, the mediator has failed. Threading that needle — the smallest modification that satisfies both — is the heart of the job. +A quick routing check on **modified → commonality**: if a signer of the modified would reasonably be annoyed at being *asked* to also sign the commonality ("I already said that"), that pair is an implication — and the attester must bless it. If they would *not* be annoyed (the commonality still feels like a new ask), the modified does not contain the deal yet. Natural → modified is usually the opposite: extra content, so a nudge, not an arrow. See `specs/product/statements-are-peculiar-for-good-reasons.md`. + The point isn't that "moderate" or "compromise" positions are always right. In fact, on some issues the common-sense supermajority position may be a rather extreme one. (e.g. Free speech: "just let people say what they want, minus some very specific exceptions like defamation and shouting 'fire' in a crowded theatre" is a pretty extreme position that I suspect is held by most of the population.) The point is that we are *not* actually divided 50-50 into two camps that can't possibly find common ground; if we stopped letting the poles dominate the discourse, the remaining supermajority of normal people wouldn't have that much trouble getting along. ## The sub-patterns diff --git a/docs/end-user/tally/suggestions-and-nudges.md b/docs/end-user/tally/suggestions-and-nudges.md index ee1562fc..c46f4fe1 100644 --- a/docs/end-user/tally/suggestions-and-nudges.md +++ b/docs/end-user/tally/suggestions-and-nudges.md @@ -2,7 +2,7 @@ Tally can suggest statements you might want to consider. For example: if you signed a very specific statement, it might suggest a clearer or more widely used statement nearby in the implication graph. -These suggestions are called **nudges**. They are meant to help you discover statements you might already believe — not to pressure you into agreeing. +These suggestions are called **nudges**. They are meant to help you discover statements you might also want to sign — a clearer wording, a related claim, or a modified text that goes a step further — not to restate something you already obviously signed, and not to pressure you into agreeing. If S2 is already contained in S1, that connection should be an [implication](./statements-and-implication-graph.md), not a nudge. (Why that split is finicky: `specs/product/statements-are-peculiar-for-good-reasons.md`.) ## What a nudge is @@ -29,4 +29,4 @@ The important rule is: **your signature is yours.** A suggestion does not count ## Why suggestions are useful -Tally lets people write statements in their own words. That freedom is valuable, but it can make the graph feel fragmented: many people may believe nearly the same thing without using the same sentence. Suggestions help you find nearby wording, broader claims, or related statements that make your position more visible without forcing you to compromise on what you originally wrote. +Tally lets people write statements in their own words. That freedom is valuable, but it can make the graph feel fragmented: many people may believe nearly the same thing without using the same sentence. Suggestions help you find nearby wording, broader claims, or related statements that make your position more visible without forcing you to compromise on what you originally wrote. They should not ask you to separately sign a weaker claim that your original statement already contained. diff --git a/docs/founder/bridge-cluster-wording-help.md b/docs/founder/bridge-cluster-wording-help.md index 715572f1..13721dfb 100644 --- a/docs/founder/bridge-cluster-wording-help.md +++ b/docs/founder/bridge-cluster-wording-help.md @@ -56,7 +56,7 @@ cause-assist endpoints — proposals, never auto-applied, never a standing strat | `POST /draft-modified-plank` | One modified plank from parent texts + optional “must not concede” / complaint. Refuses empty parents. | | `POST /draft-stand-in-sliver` | Thin roster for a camp that has no published cause yet (title, summary, planks). Not a modified-plank call. | | `POST /draft-bridge-plank` | One shared plank from ≥2 sides (modified wording, or stand-in planks when modified is skipped); strip justifications | -| `POST /critique-triple` | Objections and justification-leak warnings only — no rewrite | +| `POST /critique-triple` | Objections (including implication-vs-nudge routing) and justification-leak warnings only — no rewrite | UI: `causestarter/src/components/BridgeClusterAssist.tsx`. Implementation: `cause-assist/src/bridgeClusterAssist.ts`. diff --git a/fake-data-generation/christian-secular-tiny-seed.md b/fake-data-generation/christian-secular-tiny-seed.md index 6653750b..a8a12d24 100644 --- a/fake-data-generation/christian-secular-tiny-seed.md +++ b/fake-data-generation/christian-secular-tiny-seed.md @@ -15,7 +15,7 @@ Tiny local seed (`./scripts/data.sh --seed`, i.e. `gen:tiny`) should show two Ca - **Cause boards** hold **natural** planks only (4 + 4). **Modified + commonality** are mediator-authored, not swapped onto the camp boards. - **Mediator account:** keep **Hardhat #8** (`FUNDED_HARDHAT_DEV_KEYS[8]`), already `CHRISTIAN_MEDIATOR_*` in `seedChristianityCause.ts`. Do not jump to #19 unless we also move CSM (#7) and fund a high band. Humans stay in #0–#6; #9 remains secular-conservatism founder; #0 remains Christianity / local-food owner. - **Implication direction.** Board for S shows projects aligned with S2 where **S2 implies S**. So a project attested to **modified-Christian** appears on **commonality** (if MC→CG). A secular user who signed **modified-secular** sees it if the UI unions boards of statements they support *including implied CG*. They will not see it on the MS board itself (MC does not imply MS). Include at least one project aligned **only** with a unique plank (negative: other camp must not see it). Include some alignments to **naturals** (should not cross the bridge if natural↛CG). -- **Attester expectations.** Stop only if a **designed yes** is refused. +- **Attester expectations.** Stop only if a **designed yes** is refused. After a bless, still run the **routing check** ([peculiar statements](/specs/product/statements-are-peculiar-for-good-reasons.md) § How to check a pair): modified → CG should feel redundant to sign separately; natural → modified should not. - Yes: each modified → its CG (containment / subset). - No (not a bug): natural → CG, pole → anything, MC → MS, unique → CG, CG → modified. - **~10 projects**, not 17. Shared alignments. One unique-only. @@ -54,6 +54,8 @@ Optional: align bridge-creator example anchors later. Prospective-round seed is **Prose rewrite (2026-08-25).** Draft-order rewrite, then drop coalition-narration. Commonality is the civic conclusion only. Live attester 6 yes / 6 no. CIDs change — tiny reseed still needed if this JSON is what you publish. +**Checker loop for later clusters (and LLM bulk seed).** Do not optimize only for attester bless. For each pair: (1) designed-yes/no vs live attester, (2) routing — would a reasonable signer of S1 be annoyed at being asked to sign S2? If yes and designed implication, good (attester still must bless). If yes but S2 adds a claim another reasonable person would see, that is unreasonable annoyance — keep it a nudge, or put the extra into a modified. If no and you wanted implication, thicken S1. Generate → attester → routing; iterate. Cause-assist `POST /critique-triple` is told to emit `routing:` objections on that test. + ## Bridge cluster (2026-08-25) Hardhat #8 refs: diff --git a/services/bridge-creator/prompts/csm-strategy.md b/services/bridge-creator/prompts/csm-strategy.md index a5195e81..39afc5db 100644 --- a/services/bridge-creator/prompts/csm-strategy.md +++ b/services/bridge-creator/prompts/csm-strategy.md @@ -26,6 +26,8 @@ The modified statements are the load-bearing part of the work — and the subtle If a modification buys the implication but no one on that side would sign it, you've failed. If it's signable but the implication doesn't actually hold, you've failed. Threading that needle — the smallest modification that satisfies both — is the heart of the job. +Routing: a signer of a modified statement should already believe the commonality. If they would reasonably be annoyed at being *suggested* the commonality as a separate signature ("yes obviously, I already signed the modified"), the commonality is an implication, not a nudge. If they would not be annoyed, the modified does not yet contain the overlap — rewrite the modified, do not treat the commonality as a follow-up ask. Naturals that do not contain the deal are nudge targets (modified texts), not implication sources. + **The commonality is not always the mushy middle.** Do not reflexively reach for "moderate" or "split the difference." On some issues the supermajority position is in fact an extreme one. (e.g. Free speech: "just let people say what they want, minus narrow exceptions like defamation or shouting 'fire' in a crowded theatre" is an extreme position probably held by most of the population.) The goal is the position the supermajority actually holds, wherever it sits — not a position equidistant between the two poles. Equidistant-by-default is a failure mode. ### Inputs you read each tick diff --git a/specs/product/statements-are-peculiar-for-good-reasons.md b/specs/product/statements-are-peculiar-for-good-reasons.md index b0ec52da..b760b9a4 100644 --- a/specs/product/statements-are-peculiar-for-good-reasons.md +++ b/specs/product/statements-are-peculiar-for-good-reasons.md @@ -12,6 +12,8 @@ So: - We have the [nudger/suggester](/docs/end-user/tally/suggestions-and-nudges.md) system, where we *don't* put words in people's mouths, but we do offer than a way to opt in to suggestions: "Since you signed S1, maybe you'd be willing to sign S2?" - And we have various [patterns](/docs/end-user/common-sense-majority/hidden-majority-patterns.md) that should be helpful in writing statements that play nicely with the implication attester and with the suggester system. +One point that might help us clarify what the rules ought to be regarding implications vs suggestions is: if a human has signed S1 and would reasonably be annoyed at having S2 suggested to him as something he might want to explicitly sign, because "yes obviously I believe S2, I already signed S1", then S2 should be an implication rather than a suggestion. (Of course a human might be *unreasonably* annoyed, maybe because he doesn't realize that there are important differences that a different human might *not* consider obviously implied. So this is a judgment call. But still, that's roughly the standard we're aiming for: we don't want to piss people off by putting words in their mouth, but we also don't want to piss people off by requiring them to explicitly sign stuff that they obviously already agree with.) + So, yeah, statements are: - plain natural language - meant to be something that normal people will be willing to both sign ("I support this") and attest to a project's alignment ("project P is aligned with this goal") @@ -37,6 +39,22 @@ modified statements (mediator) → smallest change that is still signable *an Implication is for **already-true entailment**. Nudges are for **“you might also sign this.”** The mediator’s **modified** texts are the load-bearing layer that makes both honest. If you skip the modified layer and ask the attester to treat two natural camp slogans as implying a compromise, you are asking it to synthesize a belief the signer never wrote. That is the failure mode. +## How to check a pair (writer loop) + +It is easier to *check* a pair than to draft it. Run both checks; neither is enough alone. + +**1. Attester (containment).** Conservative: S1 already contains S2, no new claim. A bless is necessary for an implication arrow. It is not sufficient (subset-by-concatenation can bless junk). Do **not** treat “the signer would be annoyed at a suggestion” as a reason for the attester to say yes — some people are unreasonably annoyed. + +**2. Routing (implication vs nudge).** Imagine a human who signed S1 is shown S2 as something they might want to *explicitly sign*. + +| If they would… | Then… | +|---|---| +| **Reasonably** be annoyed (“yes obviously, I already signed S1”) | The pair belongs on the **implication** path. Then the attester must still bless. If it refuses, improve **S1** so it actually contains S2. Do not fatten S2. | +| **Not** be annoyed (S2 is a real extra: concession, limit, different emphasis, clearer reusable wording) | **Nudge**, not implication. If hasty readers still think S2 is the same claim, improve **S2** (and/or S1) so the delta is on the tin. | +| Be **unreasonably** annoyed (they treat a real extra claim as already implied; another reasonable person would not) | Still **not** an implication. Do not mint the arrow to soothe them. | + +Typical triple: **natural → modified** is a nudge (extra belief content). **Modified → commonality** is an implication (they should already have said it). Full standard: the paragraph above in this file’s opening. + The bilateral / conditional structure is why the attester’s job can be legitimate: the modified statement already contains both sides of the deal (with the signer’s priority); the commonality statement is the same contents without the priority. See [conditional support (design)](/docs/founder/csm/conditional-support-design.md). ## Roles a piece of text can play diff --git a/specs/tech/subsystems/conceptspace/seed-content/README.md b/specs/tech/subsystems/conceptspace/seed-content/README.md index 3ea0e7b7..3d41999d 100644 --- a/specs/tech/subsystems/conceptspace/seed-content/README.md +++ b/specs/tech/subsystems/conceptspace/seed-content/README.md @@ -62,8 +62,9 @@ When populating the system pre-launch: 1. **Convert** each seed statement into a displayable document (markdown-restricted format, appropriate extras) 2. **Upload** to IPFS 3. **Have a seed signer account** sign each one (so signer counts are at least 1) -4. **Run the implication attester** on pre-generated implication link pairs (see [hidden-majority.md](./hidden-majority.md) for the specific links) -5. The Aligning/Fundable Project Explorer AI can then use these as starting points for cause exploration +4. **Run the implication attester** on pre-generated implication link pairs (see [hidden-majority.md](./hidden-majority.md) for the specific links). Designed-yes pairs must bless; designed-no must refuse. A bless is not enough. +5. **Routing check** (implication vs nudge): for each designed implication, a reasonable signer of S1 should find a *suggestion* to also sign S2 annoying ("I already said that"). If they would not, S1 does not contain S2 yet — rewrite S1, do not ship it as a nudge. For designed *nudge* pairs (e.g. natural → modified), the opposite: S2 must be a real extra so a separate signature is fair. Unreasonable annoyance does not mint an arrow. Loop: generate → attester yes/no → routing check. Details: [why statements are peculiar](/specs/product/statements-are-peculiar-for-good-reasons.md). +6. The Aligning/Fundable Project Explorer AI can then use these as starting points for cause exploration The fake-data system in `universe.json` uses a different set of statements optimized for testing mechanics. The formal seed-content JSON can now be converted into the same shape, so the simulations can gradually move toward these more realistic statements without hand-copying them. diff --git a/specs/tech/subsystems/nudger/README.md b/specs/tech/subsystems/nudger/README.md index 7d82c6b1..7fc48392 100644 --- a/specs/tech/subsystems/nudger/README.md +++ b/specs/tech/subsystems/nudger/README.md @@ -203,18 +203,18 @@ The framework is general: any nudger can plug in whatever heuristics or AI promp ### 1. Implication-graph nudger -The simple case: watch the implication graph for statements that are implied by (or imply) statements the user has signed, filtered to those with more supporters. "You signed S1, and S2 is more popular and implies S1 — maybe you'd like to sign S2 too." +The simple case: watch the implication graph for statements that are implied by (or imply) statements the user has signed, filtered to those with more supporters. "You signed S1, and S2 is more popular and implies S1 — maybe you'd like to sign S2 too." That is suggesting a **parent** (or a clearer reusable wording), not asking them to also sign a **weaker S2 that S1 already contains**. The latter is an implication job: if a reasonable signer of S1 would be annoyed at being asked to sign S2 because they already said it, do **not** nudge — attest S1 → S2 instead (when the attester blesses). Routing test: [statements are peculiar](../../../product/statements-are-peculiar-for-good-reasons.md). This nudger can also do a closely related job: help users move from graph-poor statements to graph-usable ones. If a statement is too ambiguous or context-dependent to connect safely via implication attestations, the nudger may publish a clarification nudge suggesting a clearer statement that captures the likely intended meaning in a way that can participate in the graph. -This is still a nudge, not an implication. The claim is not "S1 logically implies S2"; it is "if S2 is what you meant, it may be a better statement to sign because it is clearer and more reusable." +This is still a nudge, not an implication. The claim is not "S1 logically implies S2"; it is "if S2 is what you meant, it may be a better statement to sign because it is clearer and more reusable," or "here is a more popular statement that implies yours." Do not use this channel to collect a second signature on an obvious subset of S1. This is essentially what `getStatementSuggestions` ([sdk/src/subsystems/conceptspace/queries.ts:754](../../../../sdk/src/subsystems/conceptspace/queries.ts)) and the `StatementSuggestions` component ([ui/src/conceptspace/components/StatementSuggestions.tsx](../../../../ui/src/conceptspace/components/StatementSuggestions.tsx)) already do — but currently embedded in the SDK/UI rather than running as an off-chain service. This strategy can be extracted into a proper nudger service and serve as the reference implementation. The implication-graph nudger runs as a background worker: it scans all statements periodically, generates nudges for each, and publishes them as `nudge-batch` publications. Two common sub-modes: -- **Direct graph nudge** — suggest an already-connected statement related by existing implication edges. +- **Direct graph nudge** — suggest an already-connected statement that is *not* an obvious subset of what they signed (typically a more popular parent that implies their statement, or a sibling they might also mean). - **Clarification nudge** — suggest a clearer, more context-explicit statement when the original one is too ambiguous to connect safely. When possible, the nudger should prefer an already-existing, well-supported clear statement over synthesizing a new one. Synthesizing a fresh statement is appropriate only when there is no good existing statement to point at. From 82a2d6feb952a20cd5302e36e9c09e3c77256cf6 Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Wed, 26 Aug 2026 14:38:07 -0400 Subject: [PATCH 13/14] Teach writers not to paste-glue triples, and catch that in critique. Split bridge wording guidance from ordinary planks, pass parent/intended-bridge into the cluster verbs, refuse coalition captions on shared text, and add attester examples for captions and tighter policy specs without rejecting honest subset. --- cause-assist/README.md | 6 ++--- cause-assist/src/app.ts | 12 ++++++++++ cause-assist/src/bridgeClusterAssist.test.ts | 7 ++++++ cause-assist/src/bridgeClusterAssist.ts | 22 +++++++++++++++---- cause-assist/src/statementGuidance.ts | 13 ++++++++++- cause-assist/src/types.ts | 4 ++++ docs/founder/bridge-cluster-wording-help.md | 10 ++++----- ...hristian-secular-conservative.example.json | 2 +- ...christian-secular-conservative-strategy.md | 12 ++++++++++ .../bridge-creator/prompts/csm-strategy.md | 4 ++++ .../implication-attester/src/evaluator.ts | 10 ++++++++- .../test/evaluator-corpus.test.ts | 1 + .../test/semantic-corpus.ts | 14 ++++++++++++ ...tatements-are-peculiar-for-good-reasons.md | 8 +++++++ .../components/BridgeClusterAssist.tsx | 8 ++++++- .../lib/bridgeAssistBrief.test.ts | 2 ++ ui/src/causestarter/lib/bridgeAssistBrief.ts | 12 +++++++--- ui/src/causestarter/lib/causeAssistClient.ts | 2 ++ 18 files changed, 130 insertions(+), 19 deletions(-) diff --git a/cause-assist/README.md b/cause-assist/README.md index bacdefce..e22c15e1 100644 --- a/cause-assist/README.md +++ b/cause-assist/README.md @@ -32,10 +32,10 @@ See `src/statementGuidance.ts` and the Implication Attester evaluator prompt for | POST | `/sharpen-plank` | `{ plank, causeDescription? }` | Critique + optional reword against the attestable + signable bar (callers should treat `plank` as a suggestion, not auto-apply) | | POST | `/draft-anchor` | `{ planks[] }` | Deterministic disjunctive anchor with verbatim planks and plank→anchor check payloads | | POST | `/suggest-mediator-scaffold` | `{ foundingStatement, name? }` | Editable mediator identity, side labels, and complete starting anchor triples; never a strategy prompt | -| POST | `/draft-modified-plank` | `{ parentPlanks[], currentDraft?, sideLabel?, mustNotConcede?, complaint? }` | One modified-plank proposal for a human-authored bridge cluster. Not a chat turn. Refuses empty parents. | +| POST | `/draft-modified-plank` | `{ parentPlanks[], currentDraft?, sideLabel?, mustNotConcede?, complaint?, intendedBridge? }` | One modified-plank proposal for a human-authored bridge cluster. Not a chat turn. Refuses empty parents. | | POST | `/draft-stand-in-sliver` | `{ sideLabel, bullets?, mustNotCaricature?, complaint?, currentDraft? }` | Thin roster for a camp with no published cause. Not a modified-plank call. | -| POST | `/draft-bridge-plank` | `{ modifiedSides[{ label?, planks[] }], currentDraft?, complaint? }` | One shared-platform plank from ≥2 modified sides. Strips justifications. | -| POST | `/critique-triple` | `{ modifiedPlanks[], bridgePlank }` | Objections (including implication-vs-nudge `routing:`), justification-leak warnings — no rewrite | +| POST | `/draft-bridge-plank` | `{ modifiedSides[{ label?, planks[] }], currentDraft?, complaint? }` | One shared-platform plank from ≥2 modified sides. Strips justifications and coalition captions. | +| POST | `/critique-triple` | `{ modifiedPlanks[], bridgePlank, parentPlanks? }` | Objections (including `routing:` and `shape:`), justification-leak warnings — no rewrite | | POST | `/check-implications` | `{ mainStatement, supportingStatements[] }` | Per-pair implies / confidence / reasoning | | POST | `/safety-check` | `{ items: [{ text, fieldLabel? }] }` | Per-item allow/deny + user-facing explanation | | POST | `/check-coherence` | `{ rosterCid, title, summary, planks[], mediatorBlurb? }` | Positive-only construction check for a would-be roster CID (preview; no chain write; may use heuristic without an API key) | diff --git a/cause-assist/src/app.ts b/cause-assist/src/app.ts index 63031d9b..0a963131 100644 --- a/cause-assist/src/app.ts +++ b/cause-assist/src/app.ts @@ -213,6 +213,10 @@ export function createCauseAssistApp(config: CauseAssistConfig): express.Express invalidRequest(res, `complaint must be a valid statement when provided`) return } + if (body.intendedBridge !== undefined && !validStatement(body.intendedBridge)) { + invalidRequest(res, `intendedBridge must be a valid statement when provided`) + return + } res.json(await draftModifiedPlank(body, config)) } catch (error) { next(error) } }) @@ -314,6 +318,14 @@ export function createCauseAssistApp(config: CauseAssistConfig): express.Express invalidRequest(res, `bridgePlank is required and must be at most ${MAX_STATEMENT_LENGTH} characters`) return } + if (body.parentPlanks !== undefined && ( + !Array.isArray(body.parentPlanks) + || body.parentPlanks.length > MAX_EXISTING_STATEMENTS + || body.parentPlanks.some((item) => !validStatement(item)) + )) { + invalidRequest(res, `parentPlanks must be 0–${MAX_EXISTING_STATEMENTS} valid statements when provided`) + return + } res.json(await critiqueTriple(body, config)) } catch (error) { next(error) } }) diff --git a/cause-assist/src/bridgeClusterAssist.test.ts b/cause-assist/src/bridgeClusterAssist.test.ts index 1518f0cf..9f422515 100644 --- a/cause-assist/src/bridgeClusterAssist.test.ts +++ b/cause-assist/src/bridgeClusterAssist.test.ts @@ -15,10 +15,13 @@ describe('bridge cluster wording verbs', () => { parentPlanks: ['Marriage is a covenant and children are a blessing.'], sideLabel: 'practising Christians', mustNotConcede: 'Do not reduce this to outcome data.', + intendedBridge: 'It should be easier to marry and raise children.', }, config, async (request: LlmJsonRequest) => { assert.match(request.systemPrompt, /human remains the publisher/i) assert.doesNotMatch(request.systemPrompt, /strategy prompt you should write/i) + assert.match(request.systemPrompt, /Containment is a check after drafting/i) assert.match(request.userPrompt, /must_not_concede/) + assert.match(request.userPrompt, /intended_bridge/) return { plank: 'Marriage and children are among the best things God gives us, and I want family formation to be a normal, achievable thing.', rationale: 'Keeps covenant language.', warnings: [] } as T }) assert.equal(result.source, 'llm') @@ -33,6 +36,7 @@ describe('bridge cluster wording verbs', () => { ], }, config, async (request: LlmJsonRequest) => { assert.match(request.systemPrompt, /justifications/i) + assert.match(request.systemPrompt, /coalition caption/i) return { plank: 'It should be easier than it currently is for people to marry and raise children.', rationale: 'Conclusion only.', warnings: [] } as T }) assert.equal(result.source, 'llm') @@ -46,9 +50,12 @@ describe('bridge cluster wording verbs', () => { 'Kids do better with two committed parents.', ], bridgePlank: 'Marriage is a gift from God and also the data says so.', + parentPlanks: ['Marriage is a covenant.', 'Kids do better with two parents.'], }, config, async (request: LlmJsonRequest) => { assert.match(request.systemPrompt, /Do not rewrite/) assert.match(request.systemPrompt, /routing:/) + assert.match(request.systemPrompt, /shape:/) + assert.match(request.userPrompt, /parent_planks/) return { objections: ['Shared plank requires a theological premise.'], leakWarnings: ['God-talk leaked into the bridge plank.'], diff --git a/cause-assist/src/bridgeClusterAssist.ts b/cause-assist/src/bridgeClusterAssist.ts index 7fc06e46..b355909a 100644 --- a/cause-assist/src/bridgeClusterAssist.ts +++ b/cause-assist/src/bridgeClusterAssist.ts @@ -3,7 +3,7 @@ import { type StatementStrategy, } from '@commonality/bridge-creator/strategy-engine' import type { RequestJsonCompletionFn } from '@commonality/attester-core' -import { STATEMENT_QUALITY_GUIDANCE } from './statementGuidance.js' +import { BRIDGE_STATEMENT_GUIDANCE, STATEMENT_QUALITY_GUIDANCE } from './statementGuidance.js' import type { CauseAssistConfig, CritiqueTripleRequest, @@ -58,8 +58,12 @@ export const draftModifiedStrategy: StatementStrategy< ${STATEMENT_QUALITY_GUIDANCE} +${BRIDGE_STATEMENT_GUIDANCE} + ${MEDIATION_RULES} +If an intended shared plank is provided, do not copy its sentences into the modified. Check whether the parent already says that civic claim; if it does, warn. If it does not, warn that the extra is a real ask. First-person limits are fine; do not talk about the other camp. + Return JSON only: {"plank":"...","rationale":"why this camp would still sign and what was not conceded","warnings":["..."]}.`, renderInput: (input) => ({ parent_planks: input.parentPlanks, @@ -67,6 +71,7 @@ Return JSON only: {"plank":"...","rationale":"why this camp would still sign and side_label: input.sideLabel ?? null, must_not_concede: input.mustNotConcede ?? null, organizer_complaint: input.complaint ?? null, + intended_bridge: input.intendedBridge ?? null, }), normalize: draftNormalize, } @@ -122,10 +127,12 @@ export const draftBridgeStrategy: StatementStrategy< { plank: string; rationale: string; warnings: string[] } > = { name: 'cause-assist-draft-bridge-plank', - systemPrompt: `You propose one shared (bridge) plank that each modified wording can independently imply. Strip both sides' justifications. If a justification leaked in, refuse that wording. + systemPrompt: `You propose one shared (bridge) plank that each modified wording can independently imply. Strip both sides' justifications. If a justification leaked in, refuse that wording. If a coalition caption leaked in (whose reasons, whose maximalism, "we come from different places"), refuse that wording. ${STATEMENT_QUALITY_GUIDANCE} +${BRIDGE_STATEMENT_GUIDANCE} + ${MEDIATION_RULES} Return JSON only: {"plank":"...","rationale":"why neither side's why is required","warnings":["..."]}.`, @@ -146,17 +153,24 @@ export const critiqueTripleStrategy: StatementStrategy< Also apply the implication-vs-nudge routing test. For each modified plank → bridge plank: if a reasonable signer of the modified would be annoyed at being asked to explicitly sign the bridge ("I already said that"), the pair should be an implication (containment). If they would not be annoyed, the modified does not contain the shared claim yet — object. If they would be annoyed but a different reasonable person would see a real extra claim in the bridge, do not treat that as containment; object that the pair is a nudge (or that the wording hides the delta), not an implication. Unreasonable annoyance is not a reason to bless an arrow. +Shape failures the attester will not catch (prefix with "shape:"): +- Identical or near-identical shared sentences pasted into both modifieds so subset fires (subset-by-concatenation). A bless is necessary, not sufficient. +- Shared plank still one camp's rant with the other camp's theology deleted, or a coalition caption ("we come from different places," commentary on whose reasons or maximalism). +- Multi-register or too long to sign as a paragraph. +- Parent/natural already contains the shared claim (triple decorative), or the modified introduces a civic program the parent never held without reaffirming the rest of the bundle (withhold-from-natural / belief jump). + ${MEDIATION_RULES} -Return JSON only: {"objections":["..."],"leakWarnings":["..."]}. Empty arrays mean you found nothing load-bearing to flag. Prefix routing failures with "routing:" (e.g. "routing: modified does not contain the bridge; a signer would not find a separate signature redundant").`, +Return JSON only: {"objections":["..."],"leakWarnings":["..."]}. Empty arrays mean you found nothing load-bearing to flag. Prefix routing failures with "routing:" and shape failures with "shape:".`, renderInput: (input) => ({ modified_planks: input.modifiedPlanks, bridge_plank: input.bridgePlank, + parent_planks: input.parentPlanks ?? [], }), normalize: (value) => { const record = value && typeof value === 'object' ? value as Record : {} return { - objections: stringList(record.objections).slice(0, 8), + objections: stringList(record.objections).slice(0, 12), leakWarnings: stringList(record.leakWarnings).slice(0, 8), } }, diff --git a/cause-assist/src/statementGuidance.ts b/cause-assist/src/statementGuidance.ts index 2958cdad..35709420 100644 --- a/cause-assist/src/statementGuidance.ts +++ b/cause-assist/src/statementGuidance.ts @@ -10,7 +10,7 @@ export const STATEMENT_QUALITY_GUIDANCE = `What a statement is (Commonality / Ca - Statements must be self-contained. Do not use slogans, tribe-markers, or shorthand that needs unstated background context (e.g. reject "I am pro-choice" as not clear enough by itself). - Prefer concrete, signable claims over marketing fluff, mission slogans, or vague aspirations. - Statements are public and permanent. Do not invent illegal, fraudulent, hateful, doxxing, sanctions-evading, or election-campaign-fundraising content. No personal contact details or private identifiers. -- Prefer 1–2 sentences per statement. +- Prefer 1–2 sentences per statement. This bar is for ordinary cause planks and uniques. Modified/bridge wording may be longer when the extra words are load-bearing — see bridge guidance if this task is mediation. Implication rule for supporting statements (critical): - The main statement (S1) must logically imply each supporting statement (S2). @@ -21,3 +21,14 @@ Implication rule for supporting statements (critical): - Implication is stronger than topical relatedness. Do not draft "drivers," "principles," or "why it matters" extras unless they are already entailed by the main wording. - A supporting statement should feel redundant to sign separately: a reasonable signer of the main statement would be annoyed at being asked to also sign it ("I already said that"). If they would not be annoyed, you invented a new claim — drop it or keep it off the implication path. - When in doubt, do not suggest the supporting statement.` + +/** Extra rules for human-authored bridge clusters. Do not use this as a drafting algorithm for attester subset. */ +export const BRIDGE_STATEMENT_GUIDANCE = `Modified and shared (bridge) planks: +- Signature, not column: one register, one speech act, short enough that a real person would sign the paragraph. Not an op-ed. Not three slogans stacked. +- Name the gap first. If both camps already share the civic conclusion, the shared plank is that conclusion with both *whys* omitted. Do not invent a compromise, a deal, or a narrator to make the implication system look busy. +- Containment is a check after drafting, not a method. Do not paste the shared sentences into each modified so the attester's subset rule fires. +- Parents/naturals are how that camp talks. Do not withhold a civic line from the parent so the modified can "add" it. If the parent already contains the shared claim, say so in warnings (the triple may be decorative). +- If the shared claim is not in the parent, that extra is a real ask. Warn. Do not disguise a belief jump as a small edit. Unbundling must reaffirm the rest of that camp's bundle. +- First-person limits belong on that side's modified ("I am not asking the state to make anyone pray"). Do not put coalition captions on the shared plank — not "we come from different places," "I don't need your reasons," "people who get here from biology are not my enemy," or "the civic job is not to impose a church / wait for religion to disappear." +- The shared plank must not require either side's justification (no theology a secular signer must affirm; no reducing faith to "studies show"). Also strip commentary on whose project this is. +- Routing: a reasonable signer of the modified should be annoyed at being asked to also sign the shared plank ("I already said that"). If they would not, the modified does not contain it — thicken the modified or keep it a nudge. Do not fatten the shared plank.` diff --git a/cause-assist/src/types.ts b/cause-assist/src/types.ts index aab2745a..291dfe13 100644 --- a/cause-assist/src/types.ts +++ b/cause-assist/src/types.ts @@ -68,6 +68,8 @@ export interface DraftModifiedPlankRequest { mustNotConcede?: string /** Organizer complaint about the current draft, if any. */ complaint?: string + /** Optional intended shared plank — check containment; do not paste it into the modified. */ + intendedBridge?: string } export interface DraftModifiedPlankResponse { @@ -113,6 +115,8 @@ export interface DraftBridgePlankResponse { export interface CritiqueTripleRequest { modifiedPlanks: string[] bridgePlank: string + /** Parent/natural texts when known — needed to catch withhold-from-natural. */ + parentPlanks?: string[] } export interface CritiqueTripleResponse { diff --git a/docs/founder/bridge-cluster-wording-help.md b/docs/founder/bridge-cluster-wording-help.md index 13721dfb..68486877 100644 --- a/docs/founder/bridge-cluster-wording-help.md +++ b/docs/founder/bridge-cluster-wording-help.md @@ -41,8 +41,8 @@ Two assistance layers. The **draft is the conversation memory**. Each turn is **Copy brief for your assistant** on `/bridge/new` copies a constrained packet: - verbatim parent planks and the current modified / bridge drafts -- the attester bar and cluster rules (thinner sliver, keep each side’s reasons, shared plank owns neither *why*, silence is allowed, do not invent arrows, do not write a strategy prompt) -- the Christian / secular family-formation triple labeled as a **format example only** +- the attester bar and cluster rules (thinner sliver, keep each side’s reasons, shared plank owns neither *why*, no coalition narrator, do not paste-glue for subset, silence is allowed, do not invent arrows, do not write a strategy prompt) +- the Christian / secular family-formation triple labeled as a **format example only** (civic conclusion only; do not copy a “we come from different places” closer) - a required return schema: `commonality.bridge-cluster-patch.v1` They paste into Claude / ChatGPT / Grok, paste JSON back, **Apply pasted patch**, then review. We never see the chat. Code: `causestarter/src/lib/bridgeAssistBrief.ts`. @@ -53,10 +53,10 @@ cause-assist endpoints — proposals, never auto-applied, never a standing strat | Verb | Purpose | |---|---| -| `POST /draft-modified-plank` | One modified plank from parent texts + optional “must not concede” / complaint. Refuses empty parents. | +| `POST /draft-modified-plank` | One modified plank from parent texts + optional “must not concede” / complaint / intended bridge. Refuses empty parents. | | `POST /draft-stand-in-sliver` | Thin roster for a camp that has no published cause yet (title, summary, planks). Not a modified-plank call. | -| `POST /draft-bridge-plank` | One shared plank from ≥2 sides (modified wording, or stand-in planks when modified is skipped); strip justifications | -| `POST /critique-triple` | Objections (including implication-vs-nudge routing) and justification-leak warnings only — no rewrite | +| `POST /draft-bridge-plank` | One shared plank from ≥2 sides (modified wording, or stand-in planks when modified is skipped); strip justifications and coalition captions | +| `POST /critique-triple` | Objections (`routing:`, `shape:`) and justification-leak warnings only — no rewrite. Optional parent texts. | UI: `causestarter/src/components/BridgeClusterAssist.tsx`. Implementation: `cause-assist/src/bridgeClusterAssist.ts`. diff --git a/services/bridge-creator/config/christian-secular-conservative.example.json b/services/bridge-creator/config/christian-secular-conservative.example.json index 8c3e3ec0..aa2bf97f 100644 --- a/services/bridge-creator/config/christian-secular-conservative.example.json +++ b/services/bridge-creator/config/christian-secular-conservative.example.json @@ -8,7 +8,7 @@ "side_a": "practising Christians", "side_b": "secular conservatives" }, - "strategy_prompt": "# Christian / secular-conservative mediator strategy prompt\n\nYou are the synthesis engine for a mediator operated by a Christian founder who wants\nto build bridges toward secular conservatives. Your job is to find statements that\ncommitted Christians and non-religious conservatives can *both* sign, without either\nside pretending to be the other.\n\n## Who the two sides actually are\n\n- **side_a — practicing Christians.** People for whom faith is load-bearing: it grounds\n their morality, their view of the family, their sense of what a person is. Not\n necessarily culture-warriors. Many are tired of being cast as would-be theocrats.\n- **side_b — secular conservatives.** Non-religious (atheist, agnostic, lapsed, or\n simply indifferent) but temperamentally conservative: skeptical of rapid social\n change, attached to institutions and to earned order, often deeply worried about the\n same social decay Christians worry about — and arriving at that worry from\n sociology, evolutionary psychology, national tradition, or plain observation rather\n than revelation.\n\nThese two groups are **coalition partners who don't trust each other's reasons.** That\nis the specific character of this gap, and it should shape almost everything you produce.\n\n## The central fact about this pair\n\nUnlike a left/right mediator, your two sides frequently **already agree on the\nconclusion.** The gap is almost never about what to do; it is about *why*, and about\nwhat each side suspects the other would do if it ever won.\n\n- Christians suspect secular conservatives are fair-weather allies — that a morality\n with no transcendent grounding will drift wherever the culture pushes it, and that\n they're being used as reliable votes by people who privately find them embarrassing.\n- Secular conservatives suspect Christians want, eventually, to legislate doctrine —\n that today's \"religious liberty\" is tomorrow's blasphemy law, and that any concession\n is the first step onto a slope.\n\n**So your highest-value move is usually not to find a policy compromise.** It is to\nproduce statements that let each side state a shared conclusion *without* being taken\nto have endorsed the other's foundation, and to make each side's actual limiting\nprinciple explicit so the other stops imagining the worst version of it.\n\n## Your job\n\nFrom the raw material of what each side actually says, produce a **triple**:\n\n- a **side-a** statement a practicing Christian would sign;\n- a **side-b** statement a secular conservative would sign;\n- a **common-ground** statement straightforwardly and uncontroversially *implied* by\n both.\n\nThe modified statements are the load-bearing part. Each is adjusted just enough that\ntwo things are true at once:\n\n1. The implication attester will bless the arrow to the common-ground statement (it\n only does so when the implication is obvious and incontrovertible).\n2. Someone on that side would still actually sign it.\n\nIf a modification buys the implication but nobody on that side would sign it, you have\nfailed. If it is signable but the implication doesn't hold, you have failed.\n\n## Patterns that dominate this pairing\n\n### 1. Different reasons, same conclusion (your workhorse)\n\nBoth sides land on the same position from unrelated foundations. The common-ground\nstatement is the **conclusion stated with neither side's justification attached.**\n\n- *Marriage and children.* Christians: marriage is a covenant and children are a\n blessing. Secular conservatives: stable two-parent households produce measurably\n better outcomes and the birth rate is a civilizational problem. Same conclusion.\n- *Smartphones, porn, and kids.* Christians: it corrodes the soul and cheapens sex.\n Secular conservatives: the adolescent mental-health data is alarming and the industry\n engineered it deliberately. Same conclusion.\n- *Local institutions.* Christians: the congregation is where love of neighbour is\n practised. Secular conservatives: Burkean little platoons, social capital, Putnam.\n Same conclusion.\n\nWrite the common ground so it is **fully signable by someone who rejects the other\nside's reasoning entirely.** Do not smuggle \"God-given\" into a statement you want a\nsecular conservative to sign, and do not reduce a Christian's conviction to \"studies\nshow\" — a Christian should not have to sign a statement implying that outcome data is\nwhat makes the family good.\n\n### 2. Making the limiting principle explicit\n\nThe mistrust above is mostly about **imagined maximalism.** Enormous value comes from\nstatements in which each side says plainly where it stops.\n\n- A Christian statement that says, in the signer's own voice and without apology, that\n the goal is freedom to live and speak and raise children according to conscience —\n *not* state enforcement of doctrine on non-believers, and that a country where\n Christians are free but others are coerced would be a country they'd object to.\n- A secular-conservative statement that says religious belief is not a defect to be\n managed, that religious institutions do real work no state agency replaces, and that\n they are not waiting for the churches to die off.\n\nThese are not compromises. Each side gives up nothing it actually holds. They are\n**assurances**, and they unlock everything else.\n\n### 3. Coalition unbundling\n\n\"Christian conservative\" and \"secular right\" are both bundles, and the bundling hides\nagreement. Atomize → **reaffirm** → re-aggregate. The reaffirmation step is critical:\nlet a Christian break with one plank while explicitly restating the rest of their\nfaith, so it doesn't read as backsliding; let a secular conservative say something warm\nabout the churches while explicitly restating that they still don't believe any of it.\nSomeone should never have to sound like a convert to sign.\n\n### 4. Correcting misunderstandings\n\nEach side is often arguing with a caricature: the theocrat, and the nihilist. A\nstatement that simply says clearly what one side actually believes — \"here is what I\nactually think, which is not the thing you think I think\" — can be the whole bridge.\nWord it so that reading it doesn't feel like an ambush.\n\n### 5. Same values, different beliefs\n\nWhere a genuine factual dispute remains (does religious practice actually cause the\nsocial benefits, or merely correlate? would a secular morality really drift?), a\nconditional is the honest bridge: \"If X, then Y.\" It costs no face and converts\n\"you're deluded\" / \"you're rootless\" into an empirical question.\n\n## Where the disagreement is real — do not paper over it\n\nThere are places these two sides genuinely differ, and faking agreement there will\ndestroy trust in everything else you produce:\n\n- Whether moral claims need a transcendent ground.\n- Public prayer, religious display, and religious content in schools.\n- Assisted dying, and some bioethics.\n- Whether the country's Christian heritage is owed deference as *true* or merely\n respected as *formative*.\n\nOn these, do not manufacture a mushy middle. Either produce an honest **conditional**,\nproduce a **procedural** common ground (how we settle this, not how it settles), or\n**emit nothing.** Silence is a valid and frequently correct output.\n\n## Cross-cutting techniques\n\n**Bilateral assurance.** \"I'll accept Y, as long as you're also accepting X.\" Nobody\nconcedes unilaterally.\n\n**Defer the details, in good faith.** State the agreement, explicitly postpone the fine\nprint, and pledge you mean the ordinary reading — not an edge case you're smuggling in.\n\n**Build the reservation into the statement.** \"I have my own view about *why* this is\ntrue, but I do agree that…\" This is unusually valuable here, because *why* is precisely\nwhat divides these two sides. A statement that openly says \"we get here differently\" is\nfar more signable than one that pretends the difference away.\n\n## Output discipline\n\n- Emit nothing when context is warming, stale, or thin.\n- Emit nothing when the bridge is forced, or when it only works because one side's\n convictions were quietly deleted.\n- Never write a common-ground statement that requires a secular signer to affirm a\n theological premise, or a Christian signer to affirm that their faith is *merely*\n socially useful. Both are failure modes, and the second is the one you will be\n tempted by, because it is easier to write.\n- Avoid \"people of faith and no faith alike\" register. Concrete and signable, or nothing.\n- Keep statements as short as possible but no shorter; verbosity is fine when it is\n load-bearing, and here it often is.\n- When inputs changed only trivially since the last tick, prefer no publication.\n", + "strategy_prompt": "# Christian / secular-conservative mediator strategy prompt\n\nYou are the synthesis engine for a mediator operated by a Christian founder who wants\nto build bridges toward secular conservatives. Your job is to find statements that\ncommitted Christians and non-religious conservatives can *both* sign, without either\nside pretending to be the other.\n\n## Who the two sides actually are\n\n- **side_a — practicing Christians.** People for whom faith is load-bearing: it grounds\n their morality, their view of the family, their sense of what a person is. Not\n necessarily culture-warriors. Many are tired of being cast as would-be theocrats.\n- **side_b — secular conservatives.** Non-religious (atheist, agnostic, lapsed, or\n simply indifferent) but temperamentally conservative: skeptical of rapid social\n change, attached to institutions and to earned order, often deeply worried about the\n same social decay Christians worry about — and arriving at that worry from\n sociology, evolutionary psychology, national tradition, or plain observation rather\n than revelation.\n\nThese two groups are **coalition partners who don't trust each other's reasons.** That\nis the specific character of this gap, and it should shape almost everything you produce.\n\n## The central fact about this pair\n\nUnlike a left/right mediator, your two sides frequently **already agree on the\nconclusion.** The gap is almost never about what to do; it is about *why*, and about\nwhat each side suspects the other would do if it ever won.\n\n- Christians suspect secular conservatives are fair-weather allies — that a morality\n with no transcendent grounding will drift wherever the culture pushes it, and that\n they're being used as reliable votes by people who privately find them embarrassing.\n- Secular conservatives suspect Christians want, eventually, to legislate doctrine —\n that today's \"religious liberty\" is tomorrow's blasphemy law, and that any concession\n is the first step onto a slope.\n\n**So your highest-value move is usually not to find a policy compromise.** It is to\nproduce statements that let each side state a shared conclusion *without* being taken\nto have endorsed the other's foundation, and to make each side's actual limiting\nprinciple explicit so the other stops imagining the worst version of it.\n\nIf both naturals already share the civic conclusion, the common-ground statement **is**\nthat conclusion with both foundations omitted. Do not invent a deal. Do not put\nassurances about the other camp on the common ground (\"they are not my enemy,\"\n\"I am not waiting for them to convert,\" \"we come from different places,\" \"the civic\njob is not to impose a church\"). First-person limits (\"I am not asking the state to\nmake anyone pray\"; \"I am not waiting for the churches to die\") belong on **that\nside's modified** only. Omission of the other *why* is the protection; you do not\nneed a sentence that says so.\n\n## Your job\n\nFrom the raw material of what each side actually says, produce a **triple**:\n\n- a **side-a** statement a practicing Christian would sign;\n- a **side-b** statement a secular conservative would sign;\n- a **common-ground** statement straightforwardly and uncontroversially *implied* by\n both.\n\nThe modified statements are the load-bearing part. Each is adjusted just enough that\ntwo things are true at once:\n\n1. The implication attester will bless the arrow to the common-ground statement (it\n only does so when the implication is obvious and incontrovertible).\n2. Someone on that side would still actually sign it.\n\nIf a modification buys the implication but nobody on that side would sign it, you have\nfailed. If it is signable but the implication doesn't hold, you have failed.\n\n## Patterns that dominate this pairing\n\n### 1. Different reasons, same conclusion (your workhorse)\n\nBoth sides land on the same position from unrelated foundations. The common-ground\nstatement is the **conclusion stated with neither side's justification attached.**\n\n- *Marriage and children.* Christians: marriage is a covenant and children are a\n blessing. Secular conservatives: stable two-parent households produce measurably\n better outcomes and the birth rate is a civilizational problem. Same conclusion.\n- *Smartphones, porn, and kids.* Christians: it corrodes the soul and cheapens sex.\n Secular conservatives: the adolescent mental-health data is alarming and the industry\n engineered it deliberately. Same conclusion.\n- *Local institutions.* Christians: the congregation is where love of neighbour is\n practised. Secular conservatives: Burkean little platoons, social capital, Putnam.\n Same conclusion.\n\nWrite the common ground so it is **fully signable by someone who rejects the other\nside's reasoning entirely.** Do not smuggle \"God-given\" into a statement you want a\nsecular conservative to sign, and do not reduce a Christian's conviction to \"studies\nshow\" — a Christian should not have to sign a statement implying that outcome data is\nwhat makes the family good.\n\n### 2. Making the limiting principle explicit\n\nThe mistrust above is mostly about **imagined maximalism.** Enormous value comes from\nstatements in which each side says plainly where it stops.\n\n- A Christian statement that says, in the signer's own voice and without apology, that\n the goal is freedom to live and speak and raise children according to conscience —\n *not* state enforcement of doctrine on non-believers, and that a country where\n Christians are free but others are coerced would be a country they'd object to.\n- A secular-conservative statement that says religious belief is not a defect to be\n managed, that religious institutions do real work no state agency replaces, and that\n they are not waiting for the churches to die off.\n\nThese are not compromises. Each side gives up nothing it actually holds. They are\n**assurances**, and they unlock everything else.\n\n### 3. Coalition unbundling\n\n\"Christian conservative\" and \"secular right\" are both bundles, and the bundling hides\nagreement. Atomize → **reaffirm** → re-aggregate. The reaffirmation step is critical:\nlet a Christian break with one plank while explicitly restating the rest of their\nfaith, so it doesn't read as backsliding; let a secular conservative say something warm\nabout the churches while explicitly restating that they still don't believe any of it.\nSomeone should never have to sound like a convert to sign.\n\n### 4. Correcting misunderstandings\n\nEach side is often arguing with a caricature: the theocrat, and the nihilist. A\nstatement that simply says clearly what one side actually believes — \"here is what I\nactually think, which is not the thing you think I think\" — can be the whole bridge.\nWord it so that reading it doesn't feel like an ambush.\n\n### 5. Same values, different beliefs\n\nWhere a genuine factual dispute remains (does religious practice actually cause the\nsocial benefits, or merely correlate? would a secular morality really drift?), a\nconditional is the honest bridge: \"If X, then Y.\" It costs no face and converts\n\"you're deluded\" / \"you're rootless\" into an empirical question.\n\n## Where the disagreement is real — do not paper over it\n\nThere are places these two sides genuinely differ, and faking agreement there will\ndestroy trust in everything else you produce:\n\n- Whether moral claims need a transcendent ground.\n- Public prayer, religious display, and religious content in schools.\n- Assisted dying, and some bioethics.\n- Whether the country's Christian heritage is owed deference as *true* or merely\n respected as *formative*.\n\nOn these, do not manufacture a mushy middle. Either produce an honest **conditional**,\nproduce a **procedural** common ground (how we settle this, not how it settles), or\n**emit nothing.** Silence is a valid and frequently correct output.\n\n## Cross-cutting techniques\n\n**Bilateral assurance.** \"I'll accept Y, as long as you're also accepting X.\" Nobody\nconcedes unilaterally.\n\n**Defer the details, in good faith.** State the agreement, explicitly postpone the fine\nprint, and pledge you mean the ordinary reading — not an edge case you're smuggling in.\n\n**Build the reservation into the statement.** \"I have my own view about *why* this is\ntrue, but I do agree that…\" This is unusually valuable here, because *why* is precisely\nwhat divides these two sides. A statement that openly says \"we get here differently\" is\nfar more signable than one that pretends the difference away.\n\n## Output discipline\n\n- Emit nothing when context is warming, stale, or thin.\n- Emit nothing when the bridge is forced, or when it only works because one side's\n convictions were quietly deleted.\n- Never write a common-ground statement that requires a secular signer to affirm a\n theological premise, or a Christian signer to affirm that their faith is *merely*\n socially useful. Both are failure modes, and the second is the one you will be\n tempted by, because it is easier to write.\n- Avoid \"people of faith and no faith alike\" register. Concrete and signable, or nothing.\n- Keep statements as short as possible but no shorter; verbosity is fine when it is\n load-bearing, and here it often is.\n- Do not paste common-ground sentences into both modifieds so subset fires.\n- Do not withhold a civic line from the natural so the modified can add it.\n- Signature, not column: one register. Not an op-ed. Not a caption for a bridge diagram.\n- When inputs changed only trivially since the last tick, prefer no publication.\n", "anchors": [ { "id": "family-formation-v1-side-a", diff --git a/services/bridge-creator/prompts/christian-secular-conservative-strategy.md b/services/bridge-creator/prompts/christian-secular-conservative-strategy.md index 0a0d7d03..12aef1cc 100644 --- a/services/bridge-creator/prompts/christian-secular-conservative-strategy.md +++ b/services/bridge-creator/prompts/christian-secular-conservative-strategy.md @@ -38,6 +38,15 @@ produce statements that let each side state a shared conclusion *without* being to have endorsed the other's foundation, and to make each side's actual limiting principle explicit so the other stops imagining the worst version of it. +If both naturals already share the civic conclusion, the common-ground statement **is** +that conclusion with both foundations omitted. Do not invent a deal. Do not put +assurances about the other camp on the common ground ("they are not my enemy," +"I am not waiting for them to convert," "we come from different places," "the civic +job is not to impose a church"). First-person limits ("I am not asking the state to +make anyone pray"; "I am not waiting for the churches to die") belong on **that +side's modified** only. Omission of the other *why* is the protection; you do not +need a sentence that says so. + ## Your job From the raw material of what each side actually says, produce a **triple**: @@ -159,4 +168,7 @@ far more signable than one that pretends the difference away. - Avoid "people of faith and no faith alike" register. Concrete and signable, or nothing. - Keep statements as short as possible but no shorter; verbosity is fine when it is load-bearing, and here it often is. +- Do not paste common-ground sentences into both modifieds so subset fires. +- Do not withhold a civic line from the natural so the modified can add it. +- Signature, not column: one register. Not an op-ed. Not a caption for a bridge diagram. - When inputs changed only trivially since the last tick, prefer no publication. diff --git a/services/bridge-creator/prompts/csm-strategy.md b/services/bridge-creator/prompts/csm-strategy.md index 39afc5db..efdf1079 100644 --- a/services/bridge-creator/prompts/csm-strategy.md +++ b/services/bridge-creator/prompts/csm-strategy.md @@ -50,6 +50,8 @@ Routing: a signer of a modified statement should already believe the commonality Most bridges fit one of these shapes. First identify **what's causing the gap** — that determines **what shape the common-ground statement should take.** (And note that for any particular issue there could be more than one of these patterns at play.) +If the two naturals already share the civic conclusion, the common ground **is** that conclusion with neither side's *why* attached. Do not invent a compromise-in-the-middle. Do not announce the alliance on the common ground ("we come from different places," "I don't need your reasons," commentary on the other camp's maximalism). First-person limits belong on that side's modified statement only. Silence is valid. Do not emit a triple just to give the implication attester work. + | Pattern | Nature of the gap | Common-ground shape | |---|---|---| | Compromise in the middle | Genuine preference difference with an overlap zone | "I'd be okay with X" (X in the overlap) | @@ -138,4 +140,6 @@ You'll often need to use multiple patterns or techniques at once. - Emit nothing when the proposed bridge is forced or inflammatory. - Avoid generic "both sides have valid concerns" language unless it becomes a concrete signable statement. - Keep statements as short as possible, but no shorter. (These patterns do tend to produce verbose statements; that's fine, as long as the verbosity is load-bearing.) +- Do not paste the common-ground sentences into each modified so the attester's subset rule fires. Containment is a check after drafting. +- Do not withhold a civic line from the natural so the modified can add it. - When inputs changed only trivially since the last tick, prefer no publication. diff --git a/services/implication-attester/src/evaluator.ts b/services/implication-attester/src/evaluator.ts index fedce967..e1fd7604 100644 --- a/services/implication-attester/src/evaluator.ts +++ b/services/implication-attester/src/evaluator.ts @@ -161,7 +161,15 @@ Confidence calibration: 22) S1: "Late-term abortion is horrific, but I would accept abortion through 16 weeks as a compromise." S2: "I would accept abortion through 16 weeks as a compromise." - → {"implies": true, "confidence": "high", "reasoning": "Strict subset: the negotiated commitment is explicitly contained in S1."}`; + → {"implies": true, "confidence": "high", "reasoning": "Strict subset: the negotiated commitment is explicitly contained in S1."} + +23) S1: "Elective abortion is not ordinary health care. I believe that because the child is made in the image of God. I am not asking the state to make anyone pray." + S2: "Elective abortion is not ordinary health care. The civic job is to stop the euphemism, not to impose a church." + → {"implies": false, "confidence": "high", "reasoning": "S2 adds a proposition about whose project this is (not imposing a church) that is not a claim in S1.", "key_difference": "Added coalition caption"} + +24) S1: "Private charity and local help should do more of providing for poor people than a larger welfare state." + S2: "Help should be time-limited, work-oriented where work is possible, and prefer knowledge close to the person over a distant office." + → {"implies": false, "confidence": "high", "reasoning": "S2 adds a tighter policy specification not contained in S1.", "key_difference": "Added policy specification"}`; export async function evaluateImplicationWithLLM( statement1Content: string, diff --git a/services/implication-attester/test/evaluator-corpus.test.ts b/services/implication-attester/test/evaluator-corpus.test.ts index 05834f47..1aa422d1 100644 --- a/services/implication-attester/test/evaluator-corpus.test.ts +++ b/services/implication-attester/test/evaluator-corpus.test.ts @@ -167,6 +167,7 @@ describe('implication semantic boundary corpus', () => { for (const category of [ 'logical-weakening', 'named-scope-restriction', 'rhetoric-removal', 'ambiguous-target', 'concession', 'reservation', 'negotiated-compromise', + 'coalition-caption', 'tighter-restatement', ]) assert.ok(categories.has(category as never), `missing semantic corpus category: ${category}`); assert.ok(semanticImplicationCorpus.some((entry) => entry.implies), 'corpus needs accepted arrows'); diff --git a/services/implication-attester/test/semantic-corpus.ts b/services/implication-attester/test/semantic-corpus.ts index 8b243509..ebb989fc 100644 --- a/services/implication-attester/test/semantic-corpus.ts +++ b/services/implication-attester/test/semantic-corpus.ts @@ -8,6 +8,8 @@ export interface SemanticCorpusCase { | 'concession' | 'reservation' | 'negotiated-compromise' + | 'coalition-caption' + | 'tighter-restatement' statement1: string statement2: string implies: boolean @@ -82,4 +84,16 @@ export const semanticImplicationCorpus: readonly SemanticCorpusCase[] = [ statement2: 'I would accept abortion through 16 weeks as a compromise.', rationale: 'The target is an explicit subset of the source claims.', }, + { + id: 'coalition-caption-is-an-added-claim', category: 'coalition-caption', implies: false, + statement1: 'Elective abortion is not ordinary health care. I believe that because the child is made in the image of God. I am not asking the state to make anyone pray.', + statement2: 'Elective abortion is not ordinary health care. The civic job is to stop the euphemism, not to impose a church.', + rationale: 'Commentary on whose civic job this is is an extra proposition, not a rephrasing of the health-care claim.', + }, + { + id: 'tighter-policy-spec-is-not-subset', category: 'tighter-restatement', implies: false, + statement1: 'Private charity and local help should do more of providing for poor people than a larger welfare state.', + statement2: 'Help should be time-limited, work-oriented where work is possible, and prefer knowledge close to the person over a distant office.', + rationale: 'A more specific institutional design is an added claim, not a subset of the source.', + }, ] diff --git a/specs/product/statements-are-peculiar-for-good-reasons.md b/specs/product/statements-are-peculiar-for-good-reasons.md index b760b9a4..b371d9bc 100644 --- a/specs/product/statements-are-peculiar-for-good-reasons.md +++ b/specs/product/statements-are-peculiar-for-good-reasons.md @@ -109,6 +109,14 @@ Write in this order: Read each text aloud as a signature. A parishioner, a Reason-reader, a tired moderate — would they put their name on this *paragraph*, not on the topic? +### LLM defaults to refuse (without looking at seed JSON) + +Isolated writers who have only these instructions tend to fail in three ways. Name them in briefs and critique prompts; do not “fix” them by making the attester reject verbatim subset. + +1. **Subset-by-concatenation.** Assemble the commonality, paste it into both modifieds, collect a bless. Necessary for implication, not a drafting method. +2. **Mediator voice.** Essays; talking *about* the other camp; coalition captions on the commonality (“we come from different places,” “not waiting for churches to die,” “people who get here from biology are not my enemy”). Limits stay first-person on that side’s modified. The shared text omits the *why* and the other camp. +3. **Tighter civic restatement as commonality.** The modified is a speech; the commonality is a policy spec they never quite said. Signers would not be annoyed at a second signature — that pair is a nudge, and the attester should refuse if S2 adds a specification. + ### One voice, one job - **One register per statement.** Do not concatenate King James, a tweet, and an essay. Three slogans stacked is not a statement. diff --git a/ui/src/causestarter/components/BridgeClusterAssist.tsx b/ui/src/causestarter/components/BridgeClusterAssist.tsx index fcc9df58..388dfa0a 100644 --- a/ui/src/causestarter/components/BridgeClusterAssist.tsx +++ b/ui/src/causestarter/components/BridgeClusterAssist.tsx @@ -123,6 +123,7 @@ export function BridgeClusterAssist({ draft, onDraft, busy, setBusy }: BridgeClu sideLabel: optional(parent.title || parent.slug), mustNotConcede: optional(mustNotConcede), complaint: optional(complaint), + intendedBridge: optional(draft.bridge.planks.find((plank) => plank.text.trim())?.text ?? ''), }) setProposal({ kind: 'modified', @@ -173,6 +174,7 @@ export function BridgeClusterAssist({ draft, onDraft, busy, setBusy }: BridgeClu const modifiedPlanks = draft.parents.flatMap((parent) => ( implicationSourcePlanks(parent).map((plank) => plank.text.trim()).filter(Boolean) )) + const parentPlanks = draft.parents.flatMap((parent) => parentTexts(parent)) const bridgePlank = draft.bridge.planks.find((plank) => plank.text.trim())?.text.trim() if (modifiedPlanks.length < 2 || !bridgePlank) { setStatus('Need at least two modified planks and one bridge plank to critique.') @@ -181,7 +183,11 @@ export function BridgeClusterAssist({ draft, onDraft, busy, setBusy }: BridgeClu setBusy(true) setStatus(null) try { - const result = await critiqueTriple({ modifiedPlanks, bridgePlank }) + const result = await critiqueTriple({ + modifiedPlanks, + bridgePlank, + parentPlanks: parentPlanks.length > 0 ? parentPlanks : undefined, + }) setCritique({ objections: result.objections, leakWarnings: result.leakWarnings }) } catch (error) { setStatus(error instanceof Error ? error.message : String(error)) diff --git a/ui/src/causestarter/lib/bridgeAssistBrief.test.ts b/ui/src/causestarter/lib/bridgeAssistBrief.test.ts index a7d1cefa..ad4b5656 100644 --- a/ui/src/causestarter/lib/bridgeAssistBrief.test.ts +++ b/ui/src/causestarter/lib/bridgeAssistBrief.test.ts @@ -37,6 +37,8 @@ describe('bridge assist brief', () => { expect(brief).toContain('WIP Christian wording') expect(brief).toContain('format example only') expect(brief).toContain('stand-in parent') + expect(brief).toContain('do not paste the bridge sentences') + expect(brief).not.toContain('We come to this from different places') }) it('parses fenced JSON and applies plank replacements', () => { diff --git a/ui/src/causestarter/lib/bridgeAssistBrief.ts b/ui/src/causestarter/lib/bridgeAssistBrief.ts index 696142c6..4871f078 100644 --- a/ui/src/causestarter/lib/bridgeAssistBrief.ts +++ b/ui/src/causestarter/lib/bridgeAssistBrief.ts @@ -8,11 +8,11 @@ export const FAMILY_FORMATION_EXAMPLE = { parentChristianSliver: 'Marriage and children are a covenant and a blessing.', parentSecularSliver: 'Stable two-parent households have better measured outcomes; birth rates are a civilizational problem.', modifiedChristian: - 'Marriage and children are among the best things God gives us, and I want to live in a country where forming a family is a normal, achievable thing rather than a luxury. I\'d rather have that be easy for everyone than argue about whose reasons for wanting it are the right ones.', + 'Marriage and children are among the best things God gives us, and I want to live in a country where forming a family is a normal, achievable thing rather than a luxury.', modifiedSecular: - 'I\'m not religious, but the data on this isn\'t close: kids do better with two committed parents, and a country that has stopped forming families is storing up a problem it can\'t buy its way out of. I don\'t need a theological reason to think making family formation affordable and normal should be a priority.', + 'I\'m not religious, but the data on this isn\'t close: kids do better with two committed parents, and a country that has stopped forming families is storing up a problem it can\'t buy its way out of.', bridge: - 'It should be easier than it currently is for people to marry and raise children — housing, cost, and working hours included. We come to this from different places, and neither of us needs the other\'s reasons to agree that a society where family formation has become impractical for ordinary people has a problem worth fixing.', + 'It should be easier than it currently is for people to marry and raise children — housing, cost, and working hours included.', } as const export interface BridgeClusterPatch { @@ -61,10 +61,16 @@ export function buildBridgeAssistBrief(draft: BridgeDraft): string { 'Each modified plank must still sound like that camp and keep that camp\'s reasons.', 'The bridge plank is a shared conclusion. It must not require either side\'s justification (no theology a secular signer must affirm; no reduction of faith to "studies show").', 'Implication is plank-to-plank and must be obvious: anyone who signs the modified wording is already committed to the bridge wording.', + 'Containment is a check, not a method: do not paste the bridge sentences into each modified so subset fires.', + 'Parents are how that camp talks. Do not withhold a civic line from the parent so the modified can add it.', + 'The shared plank must not narrate the coalition (no "we come from different places," no commentary on whose reasons). First-person limits stay on that side\'s modified.', + 'If both sides already share the civic conclusion, the bridge is that conclusion with both whys omitted. Do not invent a deal.', 'Silence is allowed. If the only bridge deletes a real conviction, return notes saying so and omit those planks.', 'Return only the JSON object specified below. No markdown around it.', ], formatExample: FAMILY_FORMATION_EXAMPLE, + formatExampleNotes: + 'Reasons kept on each modified; shared plank is the civic conclusion only. Do not copy a coalition narrator onto the bridge.', currentDraft: { mediatorName: draft.mediatorName.trim(), mediatorNote: draft.mediatorNote.trim(), diff --git a/ui/src/causestarter/lib/causeAssistClient.ts b/ui/src/causestarter/lib/causeAssistClient.ts index 8148d414..eeca1228 100644 --- a/ui/src/causestarter/lib/causeAssistClient.ts +++ b/ui/src/causestarter/lib/causeAssistClient.ts @@ -189,6 +189,7 @@ export async function draftModifiedPlank(input: { sideLabel?: string mustNotConcede?: string complaint?: string + intendedBridge?: string }): Promise { return postJson('/draft-modified-plank', input) } @@ -204,6 +205,7 @@ export async function draftBridgePlank(input: { export async function critiqueTriple(input: { modifiedPlanks: string[] bridgePlank: string + parentPlanks?: string[] }): Promise { return postJson('/critique-triple', input) } From 8834a4655197261a38a85ca55f5908b386e06acb Mon Sep 17 00:00:00 2001 From: Adam Spitz Date: Wed, 26 Aug 2026 15:04:20 -0400 Subject: [PATCH 14/14] Fix tiny-seed attesters, buys, and statement-guidance routing. Camp-match alignment attesters and buy/pledge by project id so secular accounts stay off scripture-unique work. Cluster-only resolves statement CIDs without republishing them on-chain. Keep signer-annoyance routing on bridge drafts only. --- cause-assist/src/bridgeClusterAssist.test.ts | 8 ++ cause-assist/src/statementGuidance.ts | 1 - .../christian-secular-tiny-seed.md | 6 +- fake-data-generation/seedChristianityCause.ts | 78 ++++++++++++++----- .../test/seedMetadata.test.ts | 15 ++++ 5 files changed, 84 insertions(+), 24 deletions(-) diff --git a/cause-assist/src/bridgeClusterAssist.test.ts b/cause-assist/src/bridgeClusterAssist.test.ts index 9f422515..516be547 100644 --- a/cause-assist/src/bridgeClusterAssist.test.ts +++ b/cause-assist/src/bridgeClusterAssist.test.ts @@ -2,6 +2,7 @@ import assert from 'node:assert/strict' import { describe, it } from 'mocha' import type { LlmJsonRequest } from '@commonality/attester-core' import { critiqueTriple, draftBridgePlank, draftModifiedPlank, draftStandInSliver } from './bridgeClusterAssist.js' +import { BRIDGE_STATEMENT_GUIDANCE, STATEMENT_QUALITY_GUIDANCE } from './statementGuidance.js' import type { CauseAssistConfig } from './types.js' const config: CauseAssistConfig = { @@ -9,6 +10,13 @@ const config: CauseAssistConfig = { safetyModel: 'model', implicationModel: 'model', coherenceModel: 'test', port: 0, } +describe('statement guidance routing', () => { + it('keeps signer-annoyance routing on bridge drafts, not ordinary cause verbs', () => { + assert.doesNotMatch(STATEMENT_QUALITY_GUIDANCE, /annoyed at being asked/) + assert.match(BRIDGE_STATEMENT_GUIDANCE, /annoyed at being asked to also sign the shared plank/) + }) +}) + describe('bridge cluster wording verbs', () => { it('drafts a modified plank from parent texts without writing a strategy prompt', async () => { const result = await draftModifiedPlank({ diff --git a/cause-assist/src/statementGuidance.ts b/cause-assist/src/statementGuidance.ts index 35709420..48be3c8f 100644 --- a/cause-assist/src/statementGuidance.ts +++ b/cause-assist/src/statementGuidance.ts @@ -19,7 +19,6 @@ Implication rule for supporting statements (critical): - Reject additions of policy, acceptance, concessions, reservations, bilateral commitments, or ambiguous speech acts; changes of strength or quantifier; and claims that depend on guessed context. - Do not reject merely because S2 is broad, permits multiple implementations, or leaves details unsettled. - Implication is stronger than topical relatedness. Do not draft "drivers," "principles," or "why it matters" extras unless they are already entailed by the main wording. -- A supporting statement should feel redundant to sign separately: a reasonable signer of the main statement would be annoyed at being asked to also sign it ("I already said that"). If they would not be annoyed, you invented a new claim — drop it or keep it off the implication path. - When in doubt, do not suggest the supporting statement.` /** Extra rules for human-authored bridge clusters. Do not use this as a drafting algorithm for attester subset. */ diff --git a/fake-data-generation/christian-secular-tiny-seed.md b/fake-data-generation/christian-secular-tiny-seed.md index a8a12d24..76f65741 100644 --- a/fake-data-generation/christian-secular-tiny-seed.md +++ b/fake-data-generation/christian-secular-tiny-seed.md @@ -28,7 +28,7 @@ Tiny local seed (`./scripts/data.sh --seed`, i.e. `gen:tiny`) should show two Ca Each shared group has: `natural-christian`, `natural-secular`, `modified-christian`, `modified-secular`, `commonality`. Unique groups have a single natural. -Modified texts **copy the commonality sentences** so the attester’s subset rule can fire. The attester **rejects** “concession as implication” when S2’s compromise is not already in S1 (`evaluator.ts`). +Containment is a check after drafting, not a method. Do **not** paste commonality sentences into each modified so the attester’s subset rule fires. Draft the modified as that camp’s speech, then check whether it already contains the shared civic claim. The attester **rejects** “concession as implication” when S2’s compromise is not already in S1 (`evaluator.ts`). ## Work log @@ -43,7 +43,7 @@ Modified texts **copy the commonality sentences** so the attester’s subset rul - [x] On-chain implications: local implication attester replays 6 blessed modified→CG arrows (`BLESSED_MODIFIED_TO_COMMONALITY`). - [x] CauseStarter click-through on the 2026-08-25 tiny seed (no reseed this pass). - [ ] Optional: align bridge-creator example anchors with these texts later; do not fork a second abortion triple in hidden-majority-patterns. -- [x] CauseStarter **bridge cluster** under #8 (`christian-secular`): two modified rosters + bridge roster + cluster document. Tiny seed publishes it; `--cluster-only` republishes against an existing chain. +- [x] CauseStarter **bridge cluster** under #8 (`christian-secular`): two modified rosters + bridge roster + cluster document. Tiny seed publishes it; `--cluster-only` resolves statement CIDs via IPFS (same content as an existing seed) and only republishes the cluster documents. - [x] Prospective-round content scenario: `Failed to find ProspectiveRoundCreated` was a call to a **no-bytecode** factory address left in `.env` after a chain that never deployed `ProspectiveContentRoundFactory` (empty-account txs succeed with no logs). Seed now skips when `getCode` is empty; SDK `createProspectiveRound` reports missing bytecode instead of a missing event. Local config sync requires `PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS`. 2026-08-25 tiny reseed: open YouTube round `0x147D1dB74c2878E08a6Ac648818421b3d77e90E3`; materialized Substack `0xEa26F3615fd3A84eB5dD24a00E7B4bEc06D63206` → `0xF8ADc47E258b9a56a8E0A717572dB3F1Cb1b4cc4`. ## Still open (resume here) @@ -89,7 +89,7 @@ CauseStarter at `http://causestarter.localhost:8088/#/`. Hardhat picker works. **Unique-only (must not cross)** -- Scripture natural board: Common Table, Scripture draft, Campus chaplaincy. Not on the secular cause board. Hardhat #5 (secular nudge-taker) fundable list has no scripture project. +- Scripture natural board: Common Table, Scripture draft, Campus chaplaincy. Not on the secular cause board. Hardhat #5 (secular nudge-taker) fundable list has no scripture project. Seed buys/pledges are keyed by project `id` / plank id so secular accounts do not buy scripture-unique work. - Colorblind amicus: only on the secular unique plank / secular cause. Not on Christianity. **Persona dashboards** diff --git a/fake-data-generation/seedChristianityCause.ts b/fake-data-generation/seedChristianityCause.ts index ee73ec24..d7627891 100644 --- a/fake-data-generation/seedChristianityCause.ts +++ b/fake-data-generation/seedChristianityCause.ts @@ -151,6 +151,26 @@ function loadPersonaProjects(): PersonaProject[] { export const CHRISTIANITY_PROJECTS = loadPersonaProjects(); +export function campOfAlignment(alignmentId: string): 'christian' | 'secular' { + const statementId = alignmentId.split('/')[1] ?? alignmentId; + return statementId.includes('secular') ? 'secular' : 'christian'; +} + +export function pickAlignmentAttester( + personas: readonly Persona[], + alignmentId: string, + projectOwnerIndex: number, +): Persona | undefined { + const camp = campOfAlignment(alignmentId); + const aligners = personas.filter((persona) => persona.aligns); + const campAligners = aligners.filter((persona) => persona.camp === camp); + return ( + campAligners.find((persona) => persona.hardhatIndex === projectOwnerIndex) + ?? campAligners[0] + ?? aligners[0] + ); +} + function createClients(privateKey: `0x${string}`) { return createSeedClients(privateKey, RPC_URL); } @@ -187,6 +207,7 @@ async function publishStatementSet( domain: string, publisherKey: `0x${string}`, cids: Map, + publishOnChain: boolean, ): Promise { const publishedData = CONTRACT_ADDRESSES.publishedData as `0x${string}` | undefined; const owner = createClients(publisherKey); @@ -198,21 +219,27 @@ async function publishStatementSet( domain, plank.id, 'simple', - { clients: owner as WriteClients, publishedDataAddress: publishedData }, + publishOnChain && publishedData + ? { clients: owner as WriteClients, publishedDataAddress: publishedData } + : {}, ); cids.set(plank.id, cid); - console.log(` Published ${domain} ${plank.id} → ${cid}`); + console.log(` ${publishOnChain ? 'Published' : 'Resolved'} ${domain} ${plank.id} → ${cid}`); } } -async function publishPlanks(): Promise> { +async function resolvePlankCids(publishOnChain: boolean): Promise> { const cids = new Map(); - await publishStatementSet(CHRISTIANITY_PLANKS, 'christianity', HARDHAT_PRIVATE_KEYS[0]!, cids); - await publishStatementSet(SECULAR_CONSERVATIVE_PLANKS, 'secular-conservatism', SECULAR_CONSERVATIVE_OWNER_KEY, cids); - await publishStatementSet(MEDIATOR_STATEMENTS, 'christian-secular-bridge', CHRISTIAN_MEDIATOR_PRIVATE_KEY, cids); + await publishStatementSet(CHRISTIANITY_PLANKS, 'christianity', HARDHAT_PRIVATE_KEYS[0]!, cids, publishOnChain); + await publishStatementSet(SECULAR_CONSERVATIVE_PLANKS, 'secular-conservatism', SECULAR_CONSERVATIVE_OWNER_KEY, cids, publishOnChain); + await publishStatementSet(MEDIATOR_STATEMENTS, 'christian-secular-bridge', CHRISTIAN_MEDIATOR_PRIVATE_KEY, cids, publishOnChain); return cids; } +async function publishPlanks(): Promise> { + return resolvePlankCids(true); +} + async function publishMediatorNudges(cids: Map): Promise { const nudgePublications = process.env.NUDGE_PUBLICATIONS_CONTRACT_ADDRESS as `0x${string}` | undefined; if (!nudgePublications) { @@ -339,6 +366,7 @@ async function signPlanks(cids: Map): Promise { } interface CreatedChristianProject { + id: string; name: string; plankId: string; assuranceContract: `0x${string}`; @@ -412,6 +440,7 @@ async function createProjects(statementCids: Map): Promise): Promise persona.aligns); + const personas = loadPersonaFile().personas; if (alignment) { for (const alignmentId of template.alignments) { const statementCid = statementCids.get(alignmentId); @@ -430,7 +459,7 @@ async function createProjects(statementCids: Map): Promise candidate.id === buy.projectId); + const buyer = personas.find((persona) => persona.hardhatIndex === buy.accountIndex); + const template = CHRISTIANITY_PROJECTS.find((candidate) => candidate.id === buy.projectId); const key = FUNDED_HARDHAT_DEV_KEYS[buy.accountIndex]; - if (!project || !key) continue; + if (!project || !key || !buyer || !template) continue; + const camps = new Set(template.alignments.map(campOfAlignment)); + if (!camps.has(buyer.camp) && camps.size === 1) { + console.warn(` Skipping buy: HH#${buy.accountIndex} (${buyer.camp}) on unique ${project.name}`); + continue; + } await fundPaymentToken(privateKeyToAccount(key).address, parsePaymentTokenUnits('2000')); const clients = createClients(key); const price = BigInt(project.prices[0]!); @@ -489,21 +526,22 @@ async function buyAndPledge(projects: CreatedChristianProject[], plankCids: Map< const recurringPledges = CONTRACT_ADDRESSES.recurringPledges as `0x${string}` | undefined; const notes = CONTRACT_ADDRESSES.delegatableNotes as `0x${string}` | undefined; const scripture = plankCids.get('scripture/natural-christian'); - const marketsModified = plankCids.get('markets/modified-christian'); if (!recurringPledges || !notes || !scripture) { console.warn('Recurring pledges not configured — skipping Christianity monthly pledges.'); return; } + const colorblind = plankCids.get('colorblind-merit/natural-secular'); + const marketsModifiedSecular = plankCids.get('markets/modified-secular'); const pledges = [ { accountIndex: 4, cid: scripture, amount: '20' }, - { accountIndex: 5, cid: marketsModified ?? scripture, amount: '8' }, - { accountIndex: 6, cid: scripture, amount: '12' }, + { accountIndex: 5, cid: marketsModifiedSecular ?? colorblind, amount: '8' }, + { accountIndex: 6, cid: colorblind, amount: '12' }, ]; const delegateTo = privateKeyToAccount(FUNDED_HARDHAT_DEV_KEYS[0]!).address; for (const pledge of pledges) { const key = FUNDED_HARDHAT_DEV_KEYS[pledge.accountIndex]; - if (!key) continue; + if (!key || !pledge.cid) continue; const clients = createClients(key); const amount = parsePaymentTokenUnits(pledge.amount); try { @@ -923,7 +961,7 @@ export async function publishSeedSecularConservativeCause( if (process.argv[1] === fileURLToPath(import.meta.url)) { const clusterOnly = process.argv.includes('--cluster-only'); const run = clusterOnly - ? publishPlanks().then((cids) => publishChristianSecularBridgeCluster(cids)) + ? resolvePlankCids(false).then((cids) => publishChristianSecularBridgeCluster(cids)) : publishSeedChristianityCause(); run .then(() => process.exit(0)) diff --git a/fake-data-generation/test/seedMetadata.test.ts b/fake-data-generation/test/seedMetadata.test.ts index 781c25a1..a87c82b6 100644 --- a/fake-data-generation/test/seedMetadata.test.ts +++ b/fake-data-generation/test/seedMetadata.test.ts @@ -45,6 +45,8 @@ import { secularModifiedRosterFields, christianSecularBridgeRosterFields, christianSecularClusterFields, + campOfAlignment, + pickAlignmentAttester, } from '../seedChristianityCause.js'; import { BLESSED_MODIFIED_TO_COMMONALITY, @@ -269,6 +271,19 @@ test('christian-secular seed cluster documents match CauseStarter extras', () => assert.match(clusterDoc.content, /Natural parents/); }); +test('alignment attesters follow the plank camp, not always Hardhat #0', () => { + const personas = [ + { id: 'christian-organizer', hardhatIndex: 0, camp: 'christian' as const, takesModified: false, signsNaturals: [], aligns: true }, + { id: 'secular-nudge-taker', hardhatIndex: 5, camp: 'secular' as const, takesModified: true, signsNaturals: [], aligns: true }, + { id: 'secular-natural-only', hardhatIndex: 6, camp: 'secular' as const, takesModified: false, signsNaturals: [], aligns: true }, + ]; + assert.equal(campOfAlignment('scripture/natural-christian'), 'christian'); + assert.equal(campOfAlignment('colorblind-merit/natural-secular'), 'secular'); + assert.equal(pickAlignmentAttester(personas, 'scripture/natural-christian', 1)?.id, 'christian-organizer'); + assert.equal(pickAlignmentAttester(personas, 'colorblind-merit/natural-secular', 6)?.id, 'secular-natural-only'); + assert.equal(pickAlignmentAttester(personas, 'abortion/modified-secular', 3)?.id, 'secular-nudge-taker'); +}); + test('christian-secular bridge has parent→modified nudges and blessed modified→CG arrows', () => { assert.equal(NATURAL_TO_MODIFIED_NUDGES.length, 6); assert.equal(BLESSED_MODIFIED_TO_COMMONALITY.length, 6);