diff --git a/AGENTS.md b/AGENTS.md index 5662abf0..134724a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,3 +17,9 @@ Write temporary debug scripts to `tmp/` and clean them up when done. ## Diagnostics Avoid `lens_diagnostics mode=full` unless you specifically need a project-wide scan; it can hang or report stale cache noise. Prefer bounded `lsp_diagnostics` on touched files plus the relevant workspace typecheck/tests. + +## Branching and PRs + +Read `workflow/branching.md` before working with branches or creating PRs. Key points: +- Always compare to `dev`, not `master` +- Feature PRs target `dev`; `dev → master` is a separate release step diff --git a/README.md b/README.md index 6345d9f2..bf432b46 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Commonality is a system for decentralized crowdfunding of public goods: people can fund projects aligned with shared values without needing a central organization to coordinate them. See [What is Commonality?](./specs/README.md#what-is-commonality) for the product overview. - [AI continuity notes](./CONTINUITY.md) + - [Fake / seed data plan](./fake-data-generation/PLAN.md) — tiny local world vs real Conceptspace statements vs mass fake activity; **next step for a fresh LLM** - [To-do list](./TODO.md) — where LLMs file new one-shot tasks. Tag each with its [autonomy tier](./workflow/task-tiers.md) (Ask / Tell / Trust); untagged means Ask. - [Task autonomy tiers](./workflow/task-tiers.md): how much latitude an LLM has to act without Adam — **Ask** before, **Tell** after, **Trust** entirely. - [Needs attention](./inbox.md) — Adam's inbox: Ask items awaiting a ruling, and Tell items reported after the fact. diff --git a/TODO.md b/TODO.md index fc3c8235..d91f6dab 100644 --- a/TODO.md +++ b/TODO.md @@ -8,6 +8,12 @@ CauseStarter also keeps its own product/architecture backlog in [`causestarter/T When an item from this page is done and no longer needs an LLM implementor's attention, don't mark it "done", just delete it. I don't want this file to get cluttered with already-completed items. +Fake data / seed content is a **standing plan**, not a pile of one-shots: read [`fake-data-generation/PLAN.md`](fake-data-generation/PLAN.md) and do the next unchecked item there (tiny UI world vs real statements vs stress traffic). Do not invent a parallel seed pipeline. + +---- + +- **(Tell)** Next fake-data/seed-data step lives in [`fake-data-generation/PLAN.md`](fake-data-generation/PLAN.md). Abortion, immigration, crime, and LGBT-schools triples are accepted; next is the demo-seed live UI pass. + ---- - Align `foldReimbursements` donation rounding with the contract’s per-share diff --git a/cause-assist/README.md b/cause-assist/README.md index 99aa9d95..f0bd3735 100644 --- a/cause-assist/README.md +++ b/cause-assist/README.md @@ -28,14 +28,14 @@ See `src/statementGuidance.ts` and the Implication Attester evaluator prompt for | --- | --- | --- | --- | | GET | `/health` | — | Liveness + whether an API key is configured | | POST | `/suggest-statements` | `{ goal, existingStatements?, count? }` | 1–5 suggestions (filtered by implication check when LLM is on) | -| POST | `/atomize` | `{ description, existingPlanks?, count? }` | Rough cause description → 1–5 independent candidate planks | -| POST | `/sharpen-plank` | `{ plank, causeDescription? }` | Critique + optional reword against the attestable + signable bar (callers should treat `plank` as a suggestion, not auto-apply) | +| POST | `/atomize` | `{ description, existingPlanks?, count? }` | Rough cause description → 1–5 independent candidate planks. Drops taxonomy / pay-the-work / slogan / heuristic-safety failures so they are never shown. | +| POST | `/sharpen-plank` | `{ plank, causeDescription? }` | Critique + optional reword against the attestable + signable bar (callers should treat `plank` as a suggestion, not auto-apply). A reword that fails the same quality gate is withheld; the original plank is returned with warnings. | | 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?, 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 and coalition captions. | -| POST | `/critique-triple` | `{ modifiedPlanks[], bridgePlank, parentPlanks? }` | Objections (including `routing:` and `shape:`), justification-leak warnings — no rewrite | +| POST | `/critique-triple` | `{ modifiedPlanks[], bridgePlank, parentPlanks? }` | Objections (including `routing:` and `shape:`), justification-leak warnings — no rewrite. Also runs the live implication attester on each modified→bridge pair and adds `routing: attester…` objections when a pair would not bless. | | 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 de2ed754..e5a6819d 100644 --- a/cause-assist/src/bridgeClusterAssist.test.ts +++ b/cause-assist/src/bridgeClusterAssist.test.ts @@ -62,6 +62,9 @@ describe('bridge cluster wording verbs', () => { 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) => { + if (request.title === 'CauseAssist Implication Check') { + return { implies: true, confidence: 'high', reasoning: 'contained' } as T + } assert.match(request.systemPrompt, /Do not rewrite/) assert.match(request.systemPrompt, /routing:/) assert.match(request.systemPrompt, /shape:/) @@ -86,6 +89,19 @@ describe('bridge cluster wording verbs', () => { assert.equal(result.leakWarnings.length, 1) }) + it('surfaces attester refusals as critique objections', async () => { + const result = await critiqueTriple({ + modifiedPlanks: ['Ban all abortion.', 'Permit abortion through birth.'], + bridgePlank: 'Legal elective abortion until 12–16 weeks.', + }, config, async (request: LlmJsonRequest) => { + if (request.systemPrompt.includes('Do not rewrite')) { + return { objections: [], leakWarnings: [] } as T + } + return { implies: false, confidence: 'high', reasoning: 'compromise not contained' } as T + }) + assert.ok(result.objections.some((item) => /^routing: attester/.test(item))) + }) + it('drafts a stand-in sliver without treating it as a modified parent', async () => { const result = await draftStandInSliver({ sideLabel: 'secular conservatives', diff --git a/cause-assist/src/bridgeClusterAssist.ts b/cause-assist/src/bridgeClusterAssist.ts index f98a02a7..7217778a 100644 --- a/cause-assist/src/bridgeClusterAssist.ts +++ b/cause-assist/src/bridgeClusterAssist.ts @@ -4,6 +4,7 @@ import { } from '@commonality/bridge-creator/strategy-engine' import type { RequestJsonCompletionFn } from '@commonality/attester-core' import { BRIDGE_STATEMENT_GUIDANCE, STATEMENT_QUALITY_GUIDANCE } from './statementGuidance.js' +import { attesterRoutingObjections } from './statementQualityGate.js' import type { CauseAssistConfig, CritiqueTripleRequest, @@ -264,8 +265,25 @@ export async function critiqueTriple( source: 'fallback', } } + const drafted = await runStatementStrategy( + critiqueTripleStrategy, + request, + engineConfig(config), + dependencies(requestFn), + ) + const attesterObjections = await attesterRoutingObjections( + request.modifiedPlanks, + request.bridgePlank, + config, + requestFn, + ) + const objections = [...drafted.objections] + for (const objection of attesterObjections) { + if (!objections.includes(objection)) objections.push(objection) + } return { - ...await runStatementStrategy(critiqueTripleStrategy, request, engineConfig(config), dependencies(requestFn)), + ...drafted, + objections: objections.slice(0, 12), source: 'llm', } } diff --git a/cause-assist/src/plankStrategies.test.ts b/cause-assist/src/plankStrategies.test.ts index ad26eec2..8aa02529 100644 --- a/cause-assist/src/plankStrategies.test.ts +++ b/cause-assist/src/plankStrategies.test.ts @@ -27,6 +27,17 @@ describe('plank-first strategies', () => { assert.equal(result.planks.length, 2) }) + it('does not return atomize planks that fail seed quality checks', async () => { + const result = await atomizeCause({ description: 'open source', count: 2 }, config, async () => ({ + planks: [ + { text: 'Open source is a public good.', rationale: 'taxonomy' }, + { text: 'I want widely used library L to stay maintained.', rationale: 'want' }, + ], + } as T)) + assert.equal(result.planks.length, 1) + assert.match(result.planks[0].text, /library L/) + }) + it('sharpens against attestable and signable criteria', async () => { const result = await sharpenPlank({ plank: 'Better parks' }, config, async (request: LlmJsonRequest) => { assert.match(request.systemPrompt, /Hedge explicitly/i) @@ -37,6 +48,16 @@ describe('plank-first strategies', () => { assert.equal(result.warnings.length, 1) }) + it('withholds a sharpened plank that is still taxonomy', async () => { + const result = await sharpenPlank({ plank: 'Better parks' }, config, async () => ({ + plank: 'Parks are a worthwhile local public good.', + rationale: 'More specific.', + warnings: [], + } as T)) + assert.equal(result.plank, 'Better parks') + assert.ok(result.warnings.some((warning) => /withheld/.test(warning))) + }) + it('drafts a disjunctive anchor with verbatim planks and correct implication direction', () => { const planks = ['The creek should be clean.', 'Oak Street should be safe at night.'] const result = draftDisjunctiveAnchor(planks) diff --git a/cause-assist/src/plankStrategies.ts b/cause-assist/src/plankStrategies.ts index d7ca2d48..a8da589d 100644 --- a/cause-assist/src/plankStrategies.ts +++ b/cause-assist/src/plankStrategies.ts @@ -5,6 +5,7 @@ import { } from '@commonality/bridge-creator/strategy-engine' import type { RequestJsonCompletionFn } from '@commonality/attester-core' import { STATEMENT_QUALITY_GUIDANCE } from './statementGuidance.js' +import { filterSignablePlanks, gateSharpenedPlank } from './statementQualityGate.js' import type { AtomizeRequest, AtomizeResponse, CauseAssistConfig, PlankDraft, SharpenPlankRequest, SharpenPlankResponse, @@ -65,12 +66,14 @@ export async function atomizeCause(request: AtomizeRequest, config: CauseAssistC const count = Math.min(5, Math.max(1, Math.floor(request.count ?? 4))) if (!config.apiKey) return { planks: [], source: 'fallback' } const planks = await runStatementStrategy(atomizeStrategy, { ...request, count }, engineConfig(config), dependencies(requestFn)) - return { planks: planks.slice(0, count), source: 'llm' } + return { planks: filterSignablePlanks(planks).slice(0, count), source: 'llm' } } export async function sharpenPlank(request: SharpenPlankRequest, config: CauseAssistConfig, requestFn?: RequestJsonCompletionFn): Promise { if (!config.apiKey) return { plank: request.plank.trim(), rationale: 'No language model is configured; wording was left unchanged.', warnings: ['Automated attestability review is unavailable.'], source: 'fallback' } - return { ...await runStatementStrategy(sharpenStrategy, request, engineConfig(config), dependencies(requestFn)), source: 'llm' } + const drafted = await runStatementStrategy(sharpenStrategy, request, engineConfig(config), dependencies(requestFn)) + const gated = gateSharpenedPlank(request.plank, drafted.plank, drafted.warnings) + return { ...drafted, plank: gated.plank, warnings: gated.warnings, source: 'llm' } } export function draftDisjunctiveAnchor(planks: string[]) { diff --git a/cause-assist/src/statementQualityGate.test.ts b/cause-assist/src/statementQualityGate.test.ts new file mode 100644 index 00000000..fcbbbcd2 --- /dev/null +++ b/cause-assist/src/statementQualityGate.test.ts @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'mocha' +import type { LlmJsonRequest } from '@commonality/attester-core' +import { + attesterRoutingObjections, + filterSignablePlanks, + gateSharpenedPlank, + qualityFailures, +} from './statementQualityGate.js' +import type { CauseAssistConfig } from './types.js' + +const config: CauseAssistConfig = { + apiKey: 'key', apiBaseUrl: 'https://example.test/v1', suggestModel: 'model', + safetyModel: 'model', implicationModel: 'model', coherenceModel: 'test', port: 0, +} + +describe('statement quality gate', () => { + it('drops taxonomy, pay-the-work, and slogan planks', () => { + assert.ok(qualityFailures('Neighborhood gardens are a public good.').length > 0) + assert.ok(qualityFailures('I want people who write docs to get paid.').length > 0) + assert.ok(qualityFailures('Material support is a legitimate way to keep Linux available.').length > 0) + assert.ok(qualityFailures('I am pro-choice.').length > 0) + assert.equal(qualityFailures('I want more CSA in Grey County, Ontario.').length, 0) + }) + + it('filters atomize candidates', () => { + const kept = filterSignablePlanks([ + { text: 'Open-source libraries are a public good.', rationale: 'taxonomy' }, + { text: 'I want widely used library L to stay maintained and well-documented.', rationale: 'want' }, + ]) + assert.equal(kept.length, 1) + assert.match(kept[0].text, /library L/) + }) + + it('withholds a sharpened plank that fails the bar', () => { + const gated = gateSharpenedPlank( + 'Better parks', + 'Parks are a worthwhile local public good.', + ['Define parks.'], + ) + assert.equal(gated.plank, 'Better parks') + assert.equal(gated.withheld, true) + assert.ok(gated.warnings.some((w) => /withheld/.test(w))) + }) + + it('adds attester routing objections when modified does not imply the bridge', async () => { + const objections = await attesterRoutingObjections( + ['I want late abortion available.', 'I want abortion banned.'], + 'Legal elective abortion until 12–16 weeks, then only for health exceptions.', + config, + async (_request: LlmJsonRequest) => ({ + implies: false, + confidence: 'high', + reasoning: 'Parents do not contain the gestational compromise.', + } as T), + ) + assert.equal(objections.length, 2) + assert.match(objections[0], /^routing: attester/) + }) +}) diff --git a/cause-assist/src/statementQualityGate.ts b/cause-assist/src/statementQualityGate.ts new file mode 100644 index 00000000..905ed1d5 --- /dev/null +++ b/cause-assist/src/statementQualityGate.ts @@ -0,0 +1,78 @@ +import { heuristicCheckItem } from './heuristicSafety.js' +import { checkImplications } from './implicationCheck.js' +import type { CauseAssistConfig, PlankDraft } from './types.js' +import type { RequestJsonCompletionFn } from '@commonality/attester-core' + +const TAXONOMY = /\b(is|are)\s+(a\s+)?((worthwhile|important|valuable|local)\s+)*public\s+goods?\b/i +const MATERIAL_SUPPORT = /\bmaterial support\b/i +const PAY_THE_WORK = /\b(get paid|nights[- ]and[- ]weekends|unpaid nights)\b/i +const ATTESTATION_META = /\b(attested as|attestation graph|implication attester)\b/i +const SLOGAN_ONLY = /^(i am pro[- ]?(choice|life)|defund the police|black lives matter|make america great again)\.?$/i + +export function qualityFailures(text: string): string[] { + const trimmed = text.trim() + if (!trimmed) return ['empty'] + const failures: string[] = [] + if (TAXONOMY.test(trimmed)) failures.push('taxonomy: classifies the topic instead of wanting an outcome') + if (MATERIAL_SUPPORT.test(trimmed) || PAY_THE_WORK.test(trimmed)) { + failures.push('funding-mechanism: planks the pay, not the work-product') + } + if (ATTESTATION_META.test(trimmed)) failures.push('meta: talks about the protocol instead of a signable want') + if (SLOGAN_ONLY.test(trimmed)) failures.push('slogan: not self-contained enough to sign') + const safety = heuristicCheckItem({ text: trimmed }) + if (safety && !safety.allowed) failures.push(`safety: ${safety.category}`) + return failures +} + +export function isSignablePlank(text: string): boolean { + return qualityFailures(text).length === 0 +} + +export function filterSignablePlanks(planks: PlankDraft[]): PlankDraft[] { + return planks.filter((plank) => isSignablePlank(plank.text)) +} + +/** If the sharpened wording fails the seed checks, keep the original and warn. */ +export function gateSharpenedPlank(original: string, candidate: string, warnings: string[]): { + plank: string + warnings: string[] + withheld: boolean +} { + const failures = qualityFailures(candidate) + if (failures.length === 0) return { plank: candidate, warnings, withheld: false } + return { + plank: original.trim(), + warnings: [ + ...warnings, + 'Suggested rewording was withheld because it failed the same checks as seed generation.', + ...failures, + ], + withheld: true, + } +} + +export async function attesterRoutingObjections( + modifiedPlanks: string[], + bridgePlank: string, + config: CauseAssistConfig, + requestFn?: RequestJsonCompletionFn, +): Promise { + if (!config.apiKey) return [] + const objections: string[] = [] + for (const modified of modifiedPlanks) { + const check = await checkImplications( + { mainStatement: modified, supportingStatements: [bridgePlank] }, + config, + requestFn, + ) + const verdict = check.results[0] + if (!verdict) continue + if (verdict.source === 'heuristic') continue + if (!verdict.implies || verdict.confidence === 'low') { + objections.push( + `routing: attester did not bless modified→bridge (${verdict.confidence}: ${verdict.reasoning})`, + ) + } + } + return objections +} diff --git a/causestarter/README.md b/causestarter/README.md index acf9d2ec..9c8a1b85 100644 --- a/causestarter/README.md +++ b/causestarter/README.md @@ -211,7 +211,10 @@ See [`cause-assist/README.md`](../cause-assist/README.md). Bridge-cluster wordin ## Design notes -- **Landing pitch is jobs, not a movement lifecycle.** Hero and `/docs/the-jobs` +- **Landing pitch is jobs, not a movement lifecycle.** `/docs` + (`docs/end-user/causestarter/index.md`) is the spoken briefing: bulletin board + of Kickstarters, plus delegation and retroactive funding; the rest of the + docs are objections and separable jobs. Hero and `/docs/the-jobs` (`docs/end-user/causestarter/the-jobs.md`) are the “do the part you’d do anyway” catalog. Do not restore Start → Grow → Deliver or “build a Movement.” - **In-app docs** (`/docs/*`) bundle `docs/end-user/causestarter/`, `shared/`, diff --git a/docs/end-user/causestarter/faq.md b/docs/end-user/causestarter/faq.md new file mode 100644 index 00000000..e5adb042 --- /dev/null +++ b/docs/end-user/causestarter/faq.md @@ -0,0 +1,53 @@ +# Questions people actually ask + +The [CauseStarter](./index.md) pitch is a bulletin board of crowdfundable projects, plus [delegation](../shared/key-ideas/delegation.md) and [retroactive funding](../lazyGiving/retroactive-funding.md). These are the next questions that usually show up. They are mostly objections, not a second tutorial. + +## What’s a cause? What’s a cause board? + +A **cause** here is just a **statement**: ordinary language about something someone believes in. “The library should stay free.” “This neighborhood should have a block party this summer.” “Rural towns should have working water infrastructure.” + +A **cause board** is a published mix of those statements, plus the fundable projects that have been vouched as advancing *some* of them. It is not a manifesto, a membership roll, or a charity. People sign statements one at a time. Projects attach to statements, not to the board as a blob. + +## What happens if I sign a statement? + +Three things, none of which join you to an organization: + +1. The signer count on that statement goes up by one. You can revoke later; current standing is what the count shows. +2. Your CauseStarter **home** becomes a personal fundable-projects board. By default it shows work vouched as aligned with the statements you’ve signed. +3. It can also show work aligned with statements those imply, if you trust the people (or services) drawing those [implication](../tally/statements-and-implication-graph.md) links. You do not have to go hunting for every related wording. + +Signing is optional. You can pledge, start a project, or vouch without ever signing. Signing is the cheap way to tell the board what you actually mean. + +## How does a project end up on *my* board? + +Someone publishes a project. Someone vouches that it is aligned with a statement (or that it already delivered). You see that vouch if the voucher is in your [trust network](../shared/key-ideas/trust-networks.md): people you trust, plus people they trust, transitively. + +There is no platform verdict that a project is good, aligned, or successful. Different people with different trust settings see different boards. Maintaining a small trust graph is work; the alternative is either spam or a committee. + +## I don’t like this board’s mix of statements. + +Publish your own. Signatures live on the statements, so a new combination still inherits signer counts and aligned projects for every statement you reuse. Even new wording can pick up overlap through implication and [bridges](../tally/suggestions-and-nudges.md). + +That is success, not a fork to suppress. See [Start a cause board](./start-a-cause.md). + +## Do I have to pick which projects get the money? + +No. Pledge $X/month (or a lump sum) and delegate the picking to a *person* you already trust. Watch the decisions if you want; they are public. Revoke unspent funds whenever you like. + +If you *do* want to pick, you still don’t have to predict the future. The **Not yet reimbursed** tab is for work that has already delivered: you close the loop at cost so early contributors can fund the next attempt. + +## What if not enough other people show up? + +The pledge is an [assurance contract](../lazyGiving/assurance-contracts.md). If the project’s threshold isn’t met, you get the money back. Ordinary donation sites take the money either way. + +## I have skills, not a grant application. + +You don’t need a grant officer. Publish the project, talk to someone a hop better-connected, get an alignment vouch onto a statement people already watch. The project can then show up on every cause board that includes that statement — including this one. + +## Who decides the wording? + +Nobody, collectively. Write the sentence you mean. If someone else’s sentence is close, implication and bridges can still connect the two, so you are not stranded on a blank petition. You are cooperating on agreement, not recruiting members. + +## Where do I actually do each job? + +[Do the part you’d do anyway](./the-jobs.md) is the catalog. The short version is still [the landing page](./index.md). diff --git a/docs/end-user/causestarter/index.md b/docs/end-user/causestarter/index.md index dd613a36..56127fd2 100644 --- a/docs/end-user/causestarter/index.md +++ b/docs/end-user/causestarter/index.md @@ -1,51 +1,49 @@ # CauseStarter -You probably got to this website by clicking a link to a **cause board** that someone else posted or sent you. +A **cause board** is like a bulletin board of crowdfundable **projects**, for some causes you might believe in. -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. +Each project is something like: “We want to raise $1000 to do this particular piece of work by the end of the month.” Like Kickstarter, if the project doesn’t reach its goal, the contributors [get their money back](../lazyGiving/assurance-contracts.md). So this is useful for things where you’re like, “I’d be glad to contribute, say, $20, as long as enough other people do.” Notice that this is the kind of thing for which our usual solution is either “government does it” or “some big charity org does it,” both of which have their problems. This is a third way to get projects like that funded. -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.” +## Two twists -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. +**You don’t have to watch the board.** If you (like most people) feel like, “There’s just no way I’m going to actually watch this bulletin board and figure out which projects to contribute to,” you can choose someone you trust and [delegate](../shared/key-ideas/delegation.md) the decisions to them. So this can be a very easy fire-and-forget kind of system: if you want to be hands-off, just pledge $X/month to someone you trust and then forget about it. It’s transparent and revokable, though. -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. +**You don’t have to bet on pitches.** The fundable-projects board has two tabs: **Not yet funded** and **Not yet reimbursed**. If you (like most people) are uneasy about trying to figure out ahead of time which projects will be successful and aren’t scams, take a look at **Not yet reimbursed**. The idea is to find projects that have *already* been successful at producing real value. You [reimburse](../lazyGiving/retroactive-funding.md) the early contributors, freeing up their money to go make early contributions to *other* projects. -## What you can do here +That’s the whole product, in the sense that matters: a bulletin board of Kickstarters, plus delegation, plus retroactive funding. The rest of this page is mostly answering the next questions people ask. -Pick the job you would already take. You do not have to do the others. +## A few obvious questions -- **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. +**What’s a cause?** Just a statement someone made, in normal language, about something they believe in. A cause board can have several of those on it. -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)**. +**What happens if I sign a statement?** The number of signers goes up by one, and your personal home page’s fundable-projects board will (by default) show you projects aligned with that statement — and with statements it implies. Signing is optional. It does not join you to an organization. -## Why this isn’t lame +**How does a project end up on the board?** Someone vouches that it is aligned with one of the statements. You only see vouches from people you [trust](../shared/key-ideas/trust-networks.md), or people they trust, and so on. There’s a bit of work in maintaining that trust graph; the point is to keep spam and abuse off *your* board without a platform committee deciding what is legitimate. -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. +**I don’t like the combination of causes on this particular board.** Make your own. You don’t start from zero. People’s signatures are attached to the individual statements, not to the board. A new board with a different mix still shows how many people have signed the statements you chose, and still shows the projects aligned with those statements. -**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. +More of the same shape — short answers to the next objections — is in **[Questions people actually ask](./faq.md)**. -**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. +## The jobs are separable -**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.” +We’ve tried very hard to split the work so you can do the part you’re inclined to do, and not do anything else: -**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. +- **Pledge money** without having to pick projects. Put up $X/month (or a one-shot amount) and [delegate the picking](../shared/key-ideas/delegation.md) to a person you already trust. Revoke anytime. +- **Direct money** — yours, or other people’s if they have delegated to you — without having to identify winners in advance. Directing 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. +- **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 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. -**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. +The catalog of “I’d be happy to X, but ugh Y” is **[Do the part you’d do anyway](./the-jobs.md)**. Organizers who want a different mix: **[Start a cause board](./start-a-cause.md)**. -**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. +## If “support a cause” still makes you roll your eyes -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. +Most products with that pitch ask you to join something, trust a black box, or become a part-time grants officer. The extra job is usually worse than the original impulse. **[Why this isn’t lame](./why-this-isnt-lame.md)** is the objection-handling for that: fire-and-forget giving without a charity you don’t trust, refunds if the crowd doesn’t show, proven work instead of pitches, no matching manifesto, judgment as a first-class job. -## See it as a story +You do not need the longer [vision and strategy](../commonality/vision-and-strategy/README.md) notes to use a cause board. They are there if you want the civilizational argument. -Concrete versions of the same tools: +## See it as a story - [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 (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/causestarter/the-jobs.md b/docs/end-user/causestarter/the-jobs.md index 9845c7a9..b66fab47 100644 --- a/docs/end-user/causestarter/the-jobs.md +++ b/docs/end-user/causestarter/the-jobs.md @@ -58,4 +58,4 @@ Project-doers can get money for worthwhile work. Donors can help financially wit That is why “there are *so* many of us — why can’t we get anything done?” was never a law of nature. It was coordination tax. This ecosystem is the claim that the tax was optional. -For the longer argument (late aggregation, organic coalitions, why it is hard to stop), see [Why Commonality?](../commonality/vision-and-strategy/README.md). Role-by-role one-liners also live in [pitches](../commonality/vision-and-strategy/pitches.md). +The spoken pitch (bulletin board, two twists, separable jobs) is **[CauseStarter](./index.md)**. The eye-roll objections are **[Why this isn’t lame](./why-this-isnt-lame.md)**. For the longer argument (late aggregation, organic coalitions, why it is hard to stop), see [Why Commonality?](../commonality/vision-and-strategy/README.md). Role-by-role one-liners also live in [pitches](../commonality/vision-and-strategy/pitches.md). diff --git a/docs/end-user/causestarter/why-this-isnt-lame.md b/docs/end-user/causestarter/why-this-isnt-lame.md new file mode 100644 index 00000000..47298aaf --- /dev/null +++ b/docs/end-user/causestarter/why-this-isnt-lame.md @@ -0,0 +1,21 @@ +# Why this isn’t lame + +Look, when I hear about some kind of “let’s support a cause!” thing, I roll my eyes: maybe it’s going to ask me to trust people I don’t trust, or ask me to do a bunch of work I don’t have time for. + +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. + +The everyday catalog of those jobs is **[Do the part you’d do anyway](./the-jobs.md)**. The spoken pitch is back on **[CauseStarter](./index.md)**. 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/common-sense-majority/hidden-majority-patterns.md b/docs/end-user/common-sense-majority/hidden-majority-patterns.md index 70bbffa2..7b9a8ab0 100644 --- a/docs/end-user/common-sense-majority/hidden-majority-patterns.md +++ b/docs/end-user/common-sense-majority/hidden-majority-patterns.md @@ -69,7 +69,8 @@ A disagreement over facts, not values — people who'd agree with the other side **Examples:** - Criminal rehabilitation: "If rehabilitating criminals actually works well, great — we should do more of it. If it doesn't work well, we shouldn't keep releasing repeat offenders on the hope that they won't reoffend." -- Schools and LGBT: Right says "Schools are actively pushing kids toward LGBT identities — that's wrong." Left: "They're not pushing anything; they're just being supportive of kids who worked it out themselves." Common ground: "*If* schools really are pushing kids in that direction, that's wrong" — which lets the disagreement become "is that actually happening?", a question of fact. +- Repeat-offender concentration (not a second rehab wording): the right is usually much more familiar than the left with the claim that a relatively small number of career criminals do a disproportionate share of violent crime. Moderate left often does not believe that fact, and hears three-strikes as more incarceration of people who never got a fair shot. Shared *value*: if that concentration is real, dangerous career violent criminals should not be cycling through catch-and-release. Common ground is the conditional: "If it's true that a relatively small number of repeat offenders do a disproportionate amount of the violent crime, we should focus on keeping those people off the streets, including with something like three-strikes laws for violent offenses. We can argue about racism and rehabilitation another time; keeping dangerous career criminals off the streets is high priority if that's how the crime is concentrated." The left can sign without conceding the fact; the right already believes the fact, so the conditional is cheap. Accepted 2026-08-31; keep this page, `fake-data-generation/statement-generation-exercises/04-crime-repeat-offenders.json`, and `fake-data-generation/seed-content/crime-repeat-offenders.json` in lockstep. +- Schools and LGBT (not a second wording of the Christian×secular LGBT unbundle): the right is usually much more sure than the left that schools are actively pushing kids toward LGBT identities. Moderate left often does not believe that fact, and hears the complaint as smearing ordinary support. Shared *value*: if a push is really happening, that is wrong. Common ground is the conditional: "If schools really are pushing kids toward LGBT identities, that's wrong. We can argue about what counts as 'supporting kids who are LGBT' versus 'pushing kids towards LGBT identities' another time, but to the extent that the latter is what's happening, that is not okay." The left can sign without conceding the fact; the right already believes the fact, so the conditional is cheap. Accepted 2026-08-31; keep this page, `fake-data-generation/statement-generation-exercises/05-lgbt-schools.json`, and `fake-data-generation/seed-content/lgbt-schools.json` in lockstep. - Racism in the justice system: "If it's true that black people really do still face systemic racism in America's justice system, I'm in favor of protesting that. On the other hand, if there's no systemic discrimination and black people really do just commit more crimes, then obviously criminals should be arrested and protesting that is silly." Changes the disagreement from "how could you not support protest of the systemic racism that black people face??" or "how could you excuse this destructive rioting that's being done for no reason??" to an empirical disagreement — most of the people on either side have no desire to excuse systemic racism *or* excuse destructive rioting, they just disagree on what the facts are. ### Misunderstandings of what the other side believes @@ -138,6 +139,30 @@ The mediator looks at those and sees that they don't actually conflict, or at le - Modified moderate right: "Late-term abortion is horrific. I'd still rather not see abortions early in the pregnancy, but I don't feel as strongly about it. Allowing abortion during the first 12-16 weeks and forbidding it after that isn't what I'd write if I were making the law alone, but I'd be okay with that cutoff if it meant we got this settled instead of fighting over it forever." - Common ground: "I'd be okay with it if abortion were allowed during the first 12-16 weeks, and forbidden after that. This isn't my ideal outcome, but I'd rather get this settled than keep fighting over it forever." +Same pattern on **crime / repeat-offender concentration** (fact-conditional, not compromise-in-the-middle). Accepted 2026-08-31; keep this page, `fake-data-generation/statement-generation-exercises/04-crime-repeat-offenders.json`, and `fake-data-generation/seed-content/crime-repeat-offenders.json` in lockstep. Not a second wording of the rehabilitation-works sketch above. + +- Natural left: "I'm not signing up for three-strikes laws. A lot of people cycling through the system never got a fair shot, and locking more of them up doesn't fix poverty, addiction, or a stacked justice system." +- Natural right: "A handful of repeat offenders do most of the violent crime. Stop catch-and-release. Three strikes, you're out." +- Modified left: "I'm not signing up for three-strikes laws as a general program. I don't believe a small number of repeat offenders actually account for a disproportionate share of violent crime — poverty, addiction, and a stacked system explain more of it, as far as I can tell. But if it turned out that a relatively small number of career violent criminals really do a disproportionate amount of the violent crime, then of course we should focus on keeping those people off the streets, including with something like three-strikes for violent offenses. I still want to argue about racism and rehabilitation, just not as a reason to keep dangerous career criminals in the rotation if that's how the crime is concentrated; keeping them off the streets would be high priority in that case." +- Modified right: "A handful of repeat offenders do most of the violent crime. Stop catch-and-release. I'd still rather we used three-strikes on repeat burglary and car theft too, not just violent offenses, but I don't feel as strongly about those as about the career violent ones. I already think a relatively small number of repeat offenders do a disproportionate amount of the violent crime, so of course we should focus on keeping those people off the streets, including with three-strikes for violent offenses. If that concentration weren't real, I wouldn't treat incapacitation as the main lever. We can argue about racism and rehabilitation another time; keeping dangerous career criminals off the streets is high priority." +- Common ground: "If it's true that a relatively small number of repeat offenders do a disproportionate amount of the violent crime, we should focus on keeping those people off the streets, including with something like three-strikes laws for violent offenses. We can argue about racism and rehabilitation another time; keeping dangerous career criminals off the streets is high priority if that's how the crime is concentrated." + +Same pattern on **LGBT / schools pushing** (fact-conditional, not coalition unbundling, not the Christian×secular civic list). Accepted 2026-08-31; keep this page, `fake-data-generation/statement-generation-exercises/05-lgbt-schools.json`, and `fake-data-generation/seed-content/lgbt-schools.json` in lockstep. + +- Natural left: "Schools aren't pushing kids toward LGBT identities. They're just being supportive of kids who already worked that out themselves. Calling that a push is a smear." +- Natural right: "Schools are actively pushing kids toward LGBT identities. That's wrong. Stop it." +- Modified left: "I don't think schools are pushing kids toward LGBT identities. They're being supportive of kids who already worked it out themselves, and calling that a push is a smear. But if it turned out schools really are pushing kids in that direction, that's wrong — of course they shouldn't be doing it. I still want to argue about what counts as support versus a push; I just wouldn't defend an actual push if that's what's happening." +- Modified right: "Schools are actively pushing kids toward LGBT identities. That's wrong. I'd still rather we also stopped social transition at school without parents even in cases that look like 'just support,' but I don't feel as strongly about borderline support as about an actual push. I already think schools are pushing, so of course that's wrong. If they really weren't pushing, I wouldn't treat this as the same fight. We can argue about curriculum details another time; if schools are pushing kids toward LGBT identities, that's wrong." +- Common ground: "If schools really are pushing kids toward LGBT identities, that's wrong. We can argue about what counts as 'supporting kids who are LGBT' versus 'pushing kids towards LGBT identities' another time, but to the extent that the latter is what's happening, that is not okay." + +Same pattern on **immigration** (mixed outcomes for peaceful unauthorized people). Accepted 2026-08-31; keep this page, `fake-data-generation/statement-generation-exercises/03-compromise-immigration.json`, and `fake-data-generation/seed-content/compromise-immigration.json` in lockstep. + +- Natural left: "People who are here illegally but otherwise keep their heads down and work shouldn't be hunted down. Deport the ones who commit other crimes." +- Natural right: "If you're here illegally you should be deported. Start with the ones who've committed other crimes, then the rest." +- Modified left: "People who are here illegally but otherwise keep their heads down and work shouldn't be hunted down. I'd prefer we only deport people here illegally who've also committed other crimes, and leave the peaceful ones alone. I can live with some peaceful ones getting deported as a side effect of actually deporting the ones who've committed other crimes, and I already accept that some peaceful ones stay. I'd rather get this settled than keep fighting over it forever." +- Modified right: "If you're here illegally you should be deported. I'd still rather we deport the peaceful ones too, but I don't feel as strongly about them as about the ones who've committed other crimes. Deporting people who are here illegally and have committed other crimes, while accepting that some peaceful ones get deported and some don't, isn't what I'd write if I were making the law alone, but I'd be okay with that if it meant we got this settled instead of fighting over it forever." +- Common ground: "I'd be okay with it if we deport people who are here illegally and have committed other crimes, even though that means some peaceful ones get deported too and some peaceful ones don't. This isn't my ideal outcome, but I'd rather get this settled than keep fighting over it forever." + The implication attester can legitimately link modified → common-ground (those really do imply each other). The nudge system suggests to users that they might be willing to sign the modified version. The noninflammatory-content system lets people on one side point to the modified version for the other side with an attestation that it won't be inflammatory. diff --git a/docs/end-user/tldr-for-llms.md b/docs/end-user/tldr-for-llms.md index 85d7e3ee..84a176a8 100644 --- a/docs/end-user/tldr-for-llms.md +++ b/docs/end-user/tldr-for-llms.md @@ -11,9 +11,25 @@ Each entry follows the same shape: [Full page](/docs/end-user/causestarter/index.md) -- **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. +- **What it is:** The spoken newcomer briefing. A cause board is a bulletin board of crowdfundable projects (Kickstarter-style refunds if the goal is missed), plus two twists: [delegate](shared/key-ideas/delegation.md) so you don't watch the board, and a **Not yet reimbursed** tab so you fund proven work instead of pitches. Almost everyone arrives via a circulated URL, not a catalog. Causes are ordinary-language statements; the board is a mix, not a club. Jobs are separable: pledge, direct, start, vouch, optionally sign. After signing, CauseStarter **home** is a personal fundable-projects board (including implied statements). - **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. +- **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. Next objections: [faq.md](/docs/end-user/causestarter/faq.md). Eye-roll “support a cause” objections: [why-this-isnt-lame.md](/docs/end-user/causestarter/why-this-isnt-lame.md). + +## CauseStarter FAQ (objections, not a second tutorial) + +[Full page](/docs/end-user/causestarter/faq.md) + +- **What it is:** Short answers to the questions after the spoken pitch: what a cause/statement is, what signing does (including implied statements), how trust-filtered vouches populate the board, forking a mix, refunds, no grant officer, wording. +- **When a user encounters it:** `/docs/faq`, or when they ask one of those questions after the landing briefing. +- **What they might want help with:** Signing vs pledging; why a new board isn’t empty; why they see (or don’t see) a project; implication vs direct support. + +## Why this isn’t lame + +[Full page](/docs/end-user/causestarter/why-this-isnt-lame.md) + +- **What it is:** Objection-handling for people who roll their eyes at “support a cause”: no black-box charity, no sucker-risk, no pitch-betting, no manifesto, judgment as a job, wording fights don’t kill funding. +- **When a user encounters it:** `/docs/why-this-isnt-lame`, or when they say this sounds like a movement/org they don’t want to join. +- **What they might want help with:** Why delegation to a person isn’t charity; why retroactive funding is the easy path; why they can ignore three of five statements. ## Do the part you’d do anyway (jobs, not an org) diff --git a/fake-data-generation/PLAN.md b/fake-data-generation/PLAN.md new file mode 100644 index 00000000..2ef4cc11 --- /dev/null +++ b/fake-data-generation/PLAN.md @@ -0,0 +1,70 @@ +# Fake data and seed data — living plan + +Tell a fresh LLM: **read this file, then do the next unchecked item under [Next](#next).** Do not invent a second seed pipeline. Details of *how* to write statements live in [`statement-generation.md`](./statement-generation.md). Tiny CauseStarter story: [`christian-secular-tiny-seed.md`](./christian-secular-tiny-seed.md). Commands: [`README.md`](./README.md). + +This file is the **index** of current state and remaining work. Update it when work lands. + +## Four different jobs (do not mix) + +| Job | What it is | What it is not | How you get it | +|---|---|---|---| +| **A. Tiny local world** | A *small* set of **fake** users, projects, signs, alignments, content rounds, and a few statements with the **right shape**, so UI/tests have something to look at | Not a catalog for real users. Not a load test | `./scripts/data.sh --seed` (default **tiny** / `npm run gen:tiny`). Verifier `stack.fresh-seeded` | +| **B. Demo / Alignment fixtures** | Same idea as A, plus formal seed-content statements and **replayed** explorer/nudge/finder outputs | Not live workers. Not mass generation | `./scripts/data.sh --seed=demo` (`gen:seed:local`) | +| **C. Real seed statements** | Curated **Conceptspace** texts for early users: signable planks, later bridges. Statements can be real; **do not** invent fake users or projects for this job | Not random `universe.json`. Not stress traffic | Author into `statement-generation-exercises/`, human-accept into `seed-content/*.json`. Process: [`statement-generation.md`](./statement-generation.md) | +| **D. Mass fake user activity** | Random users + random actions (beliefs, buys, delegations) to **stress** contracts/indexer | Not realistic UI narrative. Not the statement catalog | `./scripts/data.sh --seed=small\|medium` or `npm run gen:large` (100 users / 10 rounds). **1000+ users is not built** | + +Job C is statements only. Jobs A/B/D create on-chain activity. Tiny (A) **does not** publish the random 12-statement universe slice. + +## Current state (2026-08-31) + +### A — Tiny world + +**Usable for CauseStarter UI.** Hand-authored personas (~10 projects), Christianity + secular-conservatism boards (naturals only), mediator #8 cluster, local-food garden, content-funding rounds, Hardhat #0–#9 bookmarks. + +- Live attester: 6 designed-yes / 6 designed-no on the Christian×secular triples. +- **Weak implication demo:** camps already share the civic conclusion; commonality is just the policy. Keep as “two nearby camps.” Do **not** polish that pairing into a fake middle-ground. Do **not** train generation on it. +- Nudges exist on #8; subscriber dashboards of #0 do not show them (suggester strip follows #0). +- Optional leftover: align `bridge-creator` example anchors with these texts. +- **Second cluster (2026-08-31):** accepted abortion compromise is also published by tiny (`data/tiny-clusters/compromise-abortion.json`). Left parent `#3` `abortion-left`, right parent `#6` `abortion-right`, mediator `#8`. Christianity/secular boards unchanged. +- **Generic cluster driver (2026-08-31):** tiny bridge clusters are JSON in `data/tiny-clusters/` plus `seedTinyCluster.ts`. Do not add `compromiseAbortion.ts`-style modules for new topics. + +### B — Demo + +Implemented. `data/seed-worker-outputs.json` replayed on `seed=demo`. **Never fully walked in the UI** after the Riverside garden storyline. Local public-goods use cases A5/E2 still thin. See root `TODO.md`. + +### C — Real statements + +Curriculum in [`statement-generation.md`](./statement-generation.md): + +| Step | Status | +|---|---| +| 1. Simple public-goods causes | Gold + live `seed-content/simple-causes.json`. List not complete. Nested-place rollup is **board inclusion**, not implication | +| 2. In-camp variants | `npm run gen:proliferation` exists (attester/finder test, not a user catalog) | +| 3. Real-gap bridges | **Four** accepted triples: abortion (`seed-content/compromise-abortion.json`, 2026-08-30), immigration (`seed-content/compromise-immigration.json`, 2026-08-31), crime (`seed-content/crime-repeat-offenders.json`, 2026-08-31), and LGBT schools (`seed-content/lgbt-schools.json`, 2026-08-31). Abortion also has a tiny-seed cluster. | +| 4. Non-political public-goods bridges | Not started | +| Gate cause-assist on the same checks | Exercise 3 **done 2026-08-31** | +| Bulk hundreds of unique planks | Wait until step 1’s loop is trusted without wordsmithing. Do **not** mass-generate triples until more than one real-gap cluster survives attester + routing + read-aloud | + +Checked-in `data/seed-implication-evaluations.original-variants.json` is **stale** vs the nested-place prompt (root TODO). + +### D — Stress activity + +`gen:small` / `medium` / `large` work. README still lists 1000+ users, visualization, and deep indexer validation as future. + +## Next + +Do these in order unless Adam names a different one. Each item is a session-sized chunk. + +1. **[x] Feature a real-gap cluster in tiny (or add a second cluster).** Added the accepted **abortion compromise** cluster beside Christianity/secular boards (option a). Generic driver: `seedTinyCluster.ts` + `data/tiny-clusters/*.json`. +2. **[x] More left/right bridges — remaining Adam topics done for abortion, immigration, crime, LGBT schools.** One topic per exercise file. Reuse [hidden-majority-patterns](/docs/end-user/common-sense-majority/hidden-majority-patterns.md); do not fork a second wording of a pattern that already has a canonical example. **LGBT schools accepted 2026-08-31** (`seed-content/lgbt-schools.json`, fact-conditional — not the Christian×secular unbundle). Optional later: LGB-vs-T coalition unbundling as a separate file. +3. **[x] Gate cause-assist** (`/atomize`, `/sharpen-plank`, `/critique-triple`) so failed checks are not shown. Same pipeline as seed. Statement-generation exercise 3. Quality gate in `cause-assist/src/statementQualityGate.ts`. +4. **[ ] Demo-seed live UI pass** (`--seed=demo`) and thicken local public-goods if A5/E2 still invisible. Root TODO already has this. +5. **[ ] Refresh implication-evaluation corpus** after a model that returns JSON. Root TODO. +6. **[ ] Volume of simple-cause uniques** (hundreds) only after (1)–(2) prove the loop. Then conceptspace seed, **no** fake users/projects. +7. **[ ] Stress scale** (`gen:large` today is 100 users). 1000+ users, graphs, indexer deep-compare: not started. + +## Pointers + +- Kinds of data also summarized in [`README.md`](./README.md), [seed-content rationale](/specs/tech/subsystems/conceptspace/seed-content/README.md), [local development](/workflow/local-development.md). +- One-shot engineering leftovers stay in root [`TODO.md`](/TODO.md) (demo UI pass, implication-regression refresh, funding-portal seed tests). +- Do not file a second Christianity, a second abortion wording, or random `universe.json` statements into tiny. diff --git a/fake-data-generation/README.md b/fake-data-generation/README.md index 1868392c..e9bacb14 100644 --- a/fake-data-generation/README.md +++ b/fake-data-generation/README.md @@ -1,6 +1,15 @@ -# Generative Testing Suite +# Fake data generation -This directory contains scripts for generative testing of the Commonality smart contracts. The goal is to validate the system through automated simulation with randomly generated users and actions. +**Standing plan (current state + next step):** [`PLAN.md`](./PLAN.md). Statement-writing process: [`statement-generation.md`](./statement-generation.md). + +This directory is **four jobs**, not one “generate fake data” switch: + +1. **Tiny local world** (`./scripts/data.sh --seed` / `npm run gen:tiny`) — a *small* mix of fake users, projects, signs, alignments, and a few well-shaped statements so UI and basic tests are not empty. Default local seed. Not a catalog for real users. +2. **Demo fixtures** (`--seed=demo` / `gen:seed:local`) — formal `seed-content/` plus replayed Alignment explorer/nudge/finder outputs, still fake activity. +3. **Real seed statements** (`seed-content/*.json`) — curated Conceptspace text for early users (findable causes). No fake users or projects in this job. Drafts sit in `statement-generation-exercises/` until a human accepts them. +4. **Mass fake user activity** (`gen:small` / `gen:medium` / `gen:large`) — random users and actions to stress contracts and the indexer. `gen:large` is 100 users / 10 rounds; 1000+ is not built. Do not use this for UI narrative or statement quality. + +The rest of this README is the **generative simulation** (jobs 1, 2, 4): randomly generated users and on-chain actions. Job 3 is the JSON + attester loop, not `runSimulation`. ## Overview @@ -93,11 +102,11 @@ 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). How to **generate** viable seed / cause-assist text without hand-wordsmithing: [`statement-generation.md`](./statement-generation.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. +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). How to **generate** viable seed / cause-assist text without hand-wordsmithing: [`statement-generation.md`](./statement-generation.md). Working plan for the Christianity × secular-conservatism tiny seed: [`christian-secular-tiny-seed.md`](./christian-secular-tiny-seed.md). Tiny **bridge clusters** (owners, slugs, personas, projects) are `data/tiny-clusters/*.json` published by [`seedTinyCluster.ts`](./seedTinyCluster.ts). `gen:tiny` does **not** publish the random 12-statement `universe.json` slice; CauseStarter Christianity + secular-conservatism + the abortion-compromise cluster (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: -- one JSON file per seed-content purpose (`fundable-projects`, `hidden-majority`, `meta`, `content-funding`, `simple-causes`, `christian-secular-bridge`) +- one JSON file per seed-content purpose (`fundable-projects`, `hidden-majority`, `meta`, `content-funding`, `simple-causes`, `christian-secular-bridge`, `compromise-abortion`, `compromise-immigration`, `crime-repeat-offenders`, `lgbt-schools`) - collection-level and group-level notes so the rationale from the specs is not lost - per-statement IDs, optional roles (for example `commonality`, `normal-left`, `pole-right`), and optional `createdDate` when a seed statement needs a stable well-known CID diff --git a/fake-data-generation/christian-secular-tiny-seed.md b/fake-data-generation/christian-secular-tiny-seed.md index 6a2075cb..bf607e2c 100644 --- a/fake-data-generation/christian-secular-tiny-seed.md +++ b/fake-data-generation/christian-secular-tiny-seed.md @@ -1,6 +1,6 @@ # 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. +Status: **tiny CauseStarter story is seeded and walkable; implication exercise is still weak.** Remaining work for *all* fake/seed jobs (including next left/right topics) lives in [`PLAN.md`](./PLAN.md) — resume there, not by expanding this pairing. 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. @@ -19,7 +19,7 @@ Tiny local seed (`./scripts/data.sh --seed`, i.e. `gen:tiny`) should show two Ca - 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. +- **Personas** as hand-authored JSON driving signs / creates / attests — do not grow random `universe.json` soup for this. Further clusters: add `data/tiny-clusters/.json` (generic `seedTinyCluster.ts`); do not add an issue-named `.ts` module. - **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 @@ -36,7 +36,7 @@ Containment is a check after drafting, not a method. Do **not** paste commonalit - [x] Author seed JSON (8 naturals + 3 triples). - [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] Persona + cluster JSON (`data/tiny-clusters/christian-secular.json`) + generic driver (`seedTinyCluster.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). - [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`). @@ -52,7 +52,9 @@ Optional: align bridge-creator example anchors later. Prospective-round seed is **Generation process for later clusters (and LLM bulk seed):** [statement-generation.md](./statement-generation.md). Exercise 1 simple causes live in [seed-content/simple-causes.json](./seed-content/simple-causes.json). Not this pairing. -**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. +**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. + +**Option (a) landed 2026-08-31.** Christianity / secular boards stay. Tiny also publishes the accepted left/right abortion cluster (`data/tiny-clusters/compromise-abortion.json`, wording from `seed-content/compromise-abortion.json`). See [`PLAN.md`](./PLAN.md). **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. @@ -104,6 +106,6 @@ CauseStarter at `http://causestarter.localhost:8088/#/`. Hardhat picker works. ## 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`. +- Existing Christianity seed: `seedChristianityCause.ts`, `npm run gen:seed:christianity`. Tiny clusters: `data/tiny-clusters/*.json`. - 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 deleted file mode 100644 index 87c17d77..00000000 --- a/fake-data-generation/christianSecularBridge.ts +++ /dev/null @@ -1,74 +0,0 @@ -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; - -/** 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/data/seed-worker-outputs.json b/fake-data-generation/data/seed-worker-outputs.json index 78973e16..92edf092 100644 --- a/fake-data-generation/data/seed-worker-outputs.json +++ b/fake-data-generation/data/seed-worker-outputs.json @@ -1,8 +1,8 @@ { "schemaVersion": 1, - "generatedAt": "2026-08-27T11:28:36.187Z", + "generatedAt": "2026-08-31T19:33:35.570Z", "algorithm": "deterministic-seed-content-fixture-v1", - "seedContentFingerprint": "7638407d05647c6655970f84989e7d58ff35c2f258046c671b909cf9a4f158d5", + "seedContentFingerprint": "0e9c078727fc6698a16dbb86691a2d7b34942b7075e5523474ad374ab00ed33d", "explorerCollection": { "stream": "fundable-project-explorer", "entries": [ @@ -125,6 +125,76 @@ "label": "The law should treat people as individuals, not as racial blocs. Hiri...", "topicArea": "Christianity × secular conservatism (tiny seed)" }, + { + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "natural-left", + "label": "I want abortion to be available so that women aren't forced into goin...", + "topicArea": "Abortion — compromise in the middle" + }, + { + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "natural-right", + "label": "Late-term abortion is horrific.", + "topicArea": "Abortion — compromise in the middle" + }, + { + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "modified-left", + "label": "I want abortion to be available so that women aren't forced into goin...", + "topicArea": "Abortion — compromise in the middle" + }, + { + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "modified-right", + "label": "Late-term abortion is horrific. I'd still rather not see abortions ea...", + "topicArea": "Abortion — compromise in the middle" + }, + { + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "commonality", + "label": "I'd be okay with it if abortion were allowed during the first 12-16 w...", + "topicArea": "Abortion — compromise in the middle" + }, + { + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "natural-left", + "label": "People who are here illegally but otherwise keep their heads down and...", + "topicArea": "Immigration — compromise in the middle" + }, + { + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "natural-right", + "label": "If you're here illegally you should be deported. Start with the ones ...", + "topicArea": "Immigration — compromise in the middle" + }, + { + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "modified-left", + "label": "People who are here illegally but otherwise keep their heads down and...", + "topicArea": "Immigration — compromise in the middle" + }, + { + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "modified-right", + "label": "If you're here illegally you should be deported. I'd still rather we ...", + "topicArea": "Immigration — compromise in the middle" + }, + { + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "commonality", + "label": "I'd be okay with it if we deport people who are here illegally and ha...", + "topicArea": "Immigration — compromise in the middle" + }, { "collectionId": "content-funding", "groupId": "civility-topic", @@ -182,109 +252,39 @@ "topicArea": "Content Funding" }, { - "collectionId": "csm", - "groupId": "mission", - "statementId": "mission-statement", - "label": "I think most people who currently identify as left-wing or right-wing...", - "topicArea": "Common Sense Majority" - }, - { - "collectionId": "fundable-projects", - "groupId": "finding-common-ground", - "statementId": "common-ground-across-divides", - "label": "I am interested in furthering the cause of helping people find common...", - "topicArea": "Fundable Projects" - }, - { - "collectionId": "fundable-projects", - "groupId": "finding-common-ground", - "statementId": "charitable-cross-partisan-content", - "label": "I am interested in furthering the cause of content that explains one ...", - "topicArea": "Fundable Projects" - }, - { - "collectionId": "fundable-projects", - "groupId": "finding-common-ground", - "statementId": "identify-political-agreement", - "label": "I am interested in furthering the cause of identifying where politica...", - "topicArea": "Fundable Projects" - }, - { - "collectionId": "fundable-projects", - "groupId": "finding-common-ground", - "statementId": "reduce-tribal-polarization", - "label": "I am interested in furthering the cause of reducing tribal polarizati...", - "topicArea": "Fundable Projects" - }, - { - "collectionId": "fundable-projects", - "groupId": "finding-common-ground", - "statementId": "inform-not-inflame", - "label": "I am interested in furthering the cause of journalism that informs ra...", - "topicArea": "Fundable Projects" - }, - { - "collectionId": "fundable-projects", - "groupId": "government-accountability", - "statementId": "expose-corruption-and-waste", - "label": "I am interested in furthering the cause of exposing government corrup...", - "topicArea": "Fundable Projects" - }, - { - "collectionId": "fundable-projects", - "groupId": "government-accountability", - "statementId": "transparent-and-auditable-spending", - "label": "I am interested in furthering the cause of making government spending...", - "topicArea": "Fundable Projects" - }, - { - "collectionId": "fundable-projects", - "groupId": "government-accountability", - "statementId": "congressional-term-limits", - "label": "I am interested in furthering the cause of congressional term limits.", - "topicArea": "Fundable Projects" - }, - { - "collectionId": "fundable-projects", - "groupId": "government-accountability", - "statementId": "money-out-of-politics", - "label": "I am interested in furthering the cause of getting money out of polit...", - "topicArea": "Fundable Projects" - }, - { - "collectionId": "fundable-projects", - "groupId": "government-accountability", - "statementId": "end-revolving-door", - "label": "I am interested in furthering the cause of ending the revolving door ...", - "topicArea": "Fundable Projects" + "collectionId": "crime-repeat-offenders", + "groupId": "crime-repeat-offender-concentration", + "statementId": "natural-left", + "label": "I'm not signing up for three-strikes laws. A lot of people cycling th...", + "topicArea": "Crime — repeat-offender concentration (fact-conditional)" }, { - "collectionId": "fundable-projects", - "groupId": "government-accountability", - "statementId": "better-voting-systems", - "label": "I am interested in furthering the cause of better voting systems and ...", - "topicArea": "Fundable Projects" + "collectionId": "crime-repeat-offenders", + "groupId": "crime-repeat-offender-concentration", + "statementId": "natural-right", + "label": "A handful of repeat offenders do most of the violent crime. Stop catc...", + "topicArea": "Crime — repeat-offender concentration (fact-conditional)" }, { - "collectionId": "fundable-projects", - "groupId": "government-accountability", - "statementId": "break-regulatory-capture", - "label": "I am interested in furthering the cause of breaking up regulatory cap...", - "topicArea": "Fundable Projects" + "collectionId": "crime-repeat-offenders", + "groupId": "crime-repeat-offender-concentration", + "statementId": "modified-left", + "label": "I'm not signing up for three-strikes laws as a general program. I don...", + "topicArea": "Crime — repeat-offender concentration (fact-conditional)" }, { - "collectionId": "fundable-projects", - "groupId": "civil-liberties", - "statementId": "free-speech-unpopular-speech", - "label": "I am interested in furthering the cause of free speech, especially un...", - "topicArea": "Fundable Projects" + "collectionId": "crime-repeat-offenders", + "groupId": "crime-repeat-offender-concentration", + "statementId": "modified-right", + "label": "A handful of repeat offenders do most of the violent crime. Stop catc...", + "topicArea": "Crime — repeat-offender concentration (fact-conditional)" }, { - "collectionId": "fundable-projects", - "groupId": "civil-liberties", - "statementId": "censorship-resistant-publishing", - "label": "I am interested in furthering the cause of censorship-resistant publi...", - "topicArea": "Fundable Projects" + "collectionId": "crime-repeat-offenders", + "groupId": "crime-repeat-offender-concentration", + "statementId": "commonality", + "label": "If it's true that a relatively small number of repeat offenders do a ...", + "topicArea": "Crime — repeat-offender concentration (fact-conditional)" } ] }, @@ -530,114 +530,114 @@ }, { "target": { - "collectionId": "content-funding", - "groupId": "civility-topic", - "statementId": "noninflammatory-civility-topic" + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "natural-left" }, "suggested": { - "collectionId": "content-funding", - "groupId": "right-to-left-translation", - "statementId": "lean-left-open-to-right-perspectives" + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "natural-right" }, - "reason": "Seeded local-dev suggestion: another statement in Civility topic.", + "reason": "Seeded local-dev suggestion: another statement in Abortion — first-trimester cutoff.", "confidence": 0.6 }, { "target": { - "collectionId": "content-funding", - "groupId": "right-to-left-translation", - "statementId": "lean-left-open-to-right-perspectives" + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "natural-right" }, "suggested": { - "collectionId": "content-funding", - "groupId": "right-to-left-translation", - "statementId": "lean-right-wants-charitable-right-explanations" + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "natural-left" }, - "reason": "Seeded local-dev suggestion: another statement in Right-to-left translation.", + "reason": "Seeded local-dev suggestion: another statement in Abortion — first-trimester cutoff.", "confidence": 0.6 }, { "target": { - "collectionId": "content-funding", - "groupId": "right-to-left-translation", - "statementId": "lean-right-wants-charitable-right-explanations" + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "modified-left" }, "suggested": { - "collectionId": "content-funding", - "groupId": "right-to-left-translation", - "statementId": "lean-left-open-to-right-perspectives" + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "natural-left" }, - "reason": "Seeded local-dev suggestion: another statement in Right-to-left translation.", + "reason": "Seeded local-dev suggestion: another statement in Abortion — first-trimester cutoff.", "confidence": 0.6 }, { "target": { - "collectionId": "content-funding", - "groupId": "right-to-left-translation", - "statementId": "right-perspectives-noninflammatory" + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "modified-right" }, "suggested": { - "collectionId": "content-funding", - "groupId": "right-to-left-translation", - "statementId": "lean-left-open-to-right-perspectives" + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "natural-left" }, - "reason": "Seeded local-dev suggestion: another statement in Right-to-left translation.", + "reason": "Seeded local-dev suggestion: another statement in Abortion — first-trimester cutoff.", "confidence": 0.6 }, { "target": { - "collectionId": "content-funding", - "groupId": "left-to-right-translation", - "statementId": "lean-right-open-to-left-perspectives" + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "commonality" }, "suggested": { - "collectionId": "content-funding", - "groupId": "left-to-right-translation", - "statementId": "lean-left-wants-charitable-left-explanations" + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "natural-left" }, - "reason": "Seeded local-dev suggestion: another statement in Left-to-right translation.", + "reason": "Seeded local-dev suggestion: another statement in Abortion — first-trimester cutoff.", "confidence": 0.6 }, { "target": { - "collectionId": "content-funding", - "groupId": "left-to-right-translation", - "statementId": "lean-left-wants-charitable-left-explanations" + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "natural-left" }, "suggested": { - "collectionId": "content-funding", - "groupId": "left-to-right-translation", - "statementId": "lean-right-open-to-left-perspectives" + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "natural-right" }, - "reason": "Seeded local-dev suggestion: another statement in Left-to-right translation.", + "reason": "Seeded local-dev suggestion: another statement in Immigration — deport criminals; accept mixed outcomes for the peaceful.", "confidence": 0.6 }, { "target": { - "collectionId": "content-funding", - "groupId": "left-to-right-translation", - "statementId": "left-perspectives-noninflammatory" + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "natural-right" }, "suggested": { - "collectionId": "content-funding", - "groupId": "left-to-right-translation", - "statementId": "lean-right-open-to-left-perspectives" + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "natural-left" }, - "reason": "Seeded local-dev suggestion: another statement in Left-to-right translation.", + "reason": "Seeded local-dev suggestion: another statement in Immigration — deport criminals; accept mixed outcomes for the peaceful.", "confidence": 0.6 }, { "target": { - "collectionId": "content-funding", - "groupId": "cross-partisan-explanatory-content", - "statementId": "other-side-perspectives-without-anger" + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "modified-left" }, "suggested": { - "collectionId": "content-funding", - "groupId": "civility-topic", - "statementId": "noninflammatory-civility-topic" + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "natural-left" }, - "reason": "Seeded local-dev suggestion: another statement in Cross-partisan explanatory content.", + "reason": "Seeded local-dev suggestion: another statement in Immigration — deport criminals; accept mixed outcomes for the peaceful.", "confidence": 0.6 } ] @@ -800,6 +800,110 @@ }, "reason": "Seeded implication-finder candidate from christian-secular-bridge/markets." }, + { + "from": { + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "commonality" + }, + "to": { + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "modified-left" + }, + "reason": "Seeded implication-finder candidate from compromise-abortion/abortion-gestational-cutoff." + }, + { + "from": { + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "modified-left" + }, + "to": { + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "modified-right" + }, + "reason": "Seeded implication-finder candidate from compromise-abortion/abortion-gestational-cutoff." + }, + { + "from": { + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "modified-right" + }, + "to": { + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "natural-left" + }, + "reason": "Seeded implication-finder candidate from compromise-abortion/abortion-gestational-cutoff." + }, + { + "from": { + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "natural-left" + }, + "to": { + "collectionId": "compromise-abortion", + "groupId": "abortion-gestational-cutoff", + "statementId": "natural-right" + }, + "reason": "Seeded implication-finder candidate from compromise-abortion/abortion-gestational-cutoff." + }, + { + "from": { + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "commonality" + }, + "to": { + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "modified-left" + }, + "reason": "Seeded implication-finder candidate from compromise-immigration/immigration-criminal-priority-overlap." + }, + { + "from": { + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "modified-left" + }, + "to": { + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "modified-right" + }, + "reason": "Seeded implication-finder candidate from compromise-immigration/immigration-criminal-priority-overlap." + }, + { + "from": { + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "modified-right" + }, + "to": { + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "natural-left" + }, + "reason": "Seeded implication-finder candidate from compromise-immigration/immigration-criminal-priority-overlap." + }, + { + "from": { + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "natural-left" + }, + "to": { + "collectionId": "compromise-immigration", + "groupId": "immigration-criminal-priority-overlap", + "statementId": "natural-right" + }, + "reason": "Seeded implication-finder candidate from compromise-immigration/immigration-criminal-priority-overlap." + }, { "from": { "collectionId": "content-funding", @@ -852,6 +956,58 @@ }, "reason": "Seeded implication-finder candidate from content-funding/right-to-left-translation." }, + { + "from": { + "collectionId": "crime-repeat-offenders", + "groupId": "crime-repeat-offender-concentration", + "statementId": "commonality" + }, + "to": { + "collectionId": "crime-repeat-offenders", + "groupId": "crime-repeat-offender-concentration", + "statementId": "modified-left" + }, + "reason": "Seeded implication-finder candidate from crime-repeat-offenders/crime-repeat-offender-concentration." + }, + { + "from": { + "collectionId": "crime-repeat-offenders", + "groupId": "crime-repeat-offender-concentration", + "statementId": "modified-left" + }, + "to": { + "collectionId": "crime-repeat-offenders", + "groupId": "crime-repeat-offender-concentration", + "statementId": "modified-right" + }, + "reason": "Seeded implication-finder candidate from crime-repeat-offenders/crime-repeat-offender-concentration." + }, + { + "from": { + "collectionId": "crime-repeat-offenders", + "groupId": "crime-repeat-offender-concentration", + "statementId": "modified-right" + }, + "to": { + "collectionId": "crime-repeat-offenders", + "groupId": "crime-repeat-offender-concentration", + "statementId": "natural-left" + }, + "reason": "Seeded implication-finder candidate from crime-repeat-offenders/crime-repeat-offender-concentration." + }, + { + "from": { + "collectionId": "crime-repeat-offenders", + "groupId": "crime-repeat-offender-concentration", + "statementId": "natural-left" + }, + "to": { + "collectionId": "crime-repeat-offenders", + "groupId": "crime-repeat-offender-concentration", + "statementId": "natural-right" + }, + "reason": "Seeded implication-finder candidate from crime-repeat-offenders/crime-repeat-offender-concentration." + }, { "from": { "collectionId": "fundable-projects", @@ -1007,162 +1163,6 @@ "statementId": "individual-and-community-preparedness" }, "reason": "Seeded implication-finder candidate from fundable-projects/defense-security-and-resilience." - }, - { - "from": { - "collectionId": "fundable-projects", - "groupId": "defense-security-and-resilience", - "statementId": "individual-and-community-preparedness" - }, - "to": { - "collectionId": "fundable-projects", - "groupId": "defense-security-and-resilience", - "statementId": "supply-chain-resilience" - }, - "reason": "Seeded implication-finder candidate from fundable-projects/defense-security-and-resilience." - }, - { - "from": { - "collectionId": "fundable-projects", - "groupId": "education", - "statementId": "classic-literature-and-history" - }, - "to": { - "collectionId": "fundable-projects", - "groupId": "education", - "statementId": "educational-support-for-disadvantaged-kids" - }, - "reason": "Seeded implication-finder candidate from fundable-projects/education." - }, - { - "from": { - "collectionId": "fundable-projects", - "groupId": "education", - "statementId": "educational-support-for-disadvantaged-kids" - }, - "to": { - "collectionId": "fundable-projects", - "groupId": "education", - "statementId": "homeschooling-resources" - }, - "reason": "Seeded implication-finder candidate from fundable-projects/education." - }, - { - "from": { - "collectionId": "fundable-projects", - "groupId": "education", - "statementId": "homeschooling-resources" - }, - "to": { - "collectionId": "fundable-projects", - "groupId": "education", - "statementId": "learning-outcomes-research" - }, - "reason": "Seeded implication-finder candidate from fundable-projects/education." - }, - { - "from": { - "collectionId": "fundable-projects", - "groupId": "education", - "statementId": "learning-outcomes-research" - }, - "to": { - "collectionId": "fundable-projects", - "groupId": "education", - "statementId": "school-alternatives" - }, - "reason": "Seeded implication-finder candidate from fundable-projects/education." - }, - { - "from": { - "collectionId": "fundable-projects", - "groupId": "education", - "statementId": "school-alternatives" - }, - "to": { - "collectionId": "fundable-projects", - "groupId": "education", - "statementId": "vocational-training" - }, - "reason": "Seeded implication-finder candidate from fundable-projects/education." - }, - { - "from": { - "collectionId": "fundable-projects", - "groupId": "environment-and-sustainability", - "statementId": "clean-energy-research" - }, - "to": { - "collectionId": "fundable-projects", - "groupId": "environment-and-sustainability", - "statementId": "conservation-of-local-natural-areas" - }, - "reason": "Seeded implication-finder candidate from fundable-projects/environment-and-sustainability." - }, - { - "from": { - "collectionId": "fundable-projects", - "groupId": "environment-and-sustainability", - "statementId": "conservation-of-local-natural-areas" - }, - "to": { - "collectionId": "fundable-projects", - "groupId": "environment-and-sustainability", - "statementId": "independent-environmental-monitoring" - }, - "reason": "Seeded implication-finder candidate from fundable-projects/environment-and-sustainability." - }, - { - "from": { - "collectionId": "fundable-projects", - "groupId": "environment-and-sustainability", - "statementId": "independent-environmental-monitoring" - }, - "to": { - "collectionId": "fundable-projects", - "groupId": "environment-and-sustainability", - "statementId": "reduce-industrial-food-dependence" - }, - "reason": "Seeded implication-finder candidate from fundable-projects/environment-and-sustainability." - }, - { - "from": { - "collectionId": "fundable-projects", - "groupId": "environment-and-sustainability", - "statementId": "reduce-industrial-food-dependence" - }, - "to": { - "collectionId": "fundable-projects", - "groupId": "environment-and-sustainability", - "statementId": "sustainable-agriculture-research" - }, - "reason": "Seeded implication-finder candidate from fundable-projects/environment-and-sustainability." - }, - { - "from": { - "collectionId": "fundable-projects", - "groupId": "faith-civil-society-and-charitable-coordination", - "statementId": "civil-society-alternatives" - }, - "to": { - "collectionId": "fundable-projects", - "groupId": "faith-civil-society-and-charitable-coordination", - "statementId": "community-coordination-without-bureaucracy" - }, - "reason": "Seeded implication-finder candidate from fundable-projects/faith-civil-society-and-charitable-coordination." - }, - { - "from": { - "collectionId": "fundable-projects", - "groupId": "faith-civil-society-and-charitable-coordination", - "statementId": "community-coordination-without-bureaucracy" - }, - "to": { - "collectionId": "fundable-projects", - "groupId": "faith-civil-society-and-charitable-coordination", - "statementId": "cross-church-coordination" - }, - "reason": "Seeded implication-finder candidate from fundable-projects/faith-civil-society-and-charitable-coordination." } ] } diff --git a/fake-data-generation/data/christian-secular-personas.json b/fake-data-generation/data/tiny-clusters/christian-secular.json similarity index 58% rename from fake-data-generation/data/christian-secular-personas.json rename to fake-data-generation/data/tiny-clusters/christian-secular.json index cdb37eda..647b60ac 100644 --- a/fake-data-generation/data/christian-secular-personas.json +++ b/fake-data-generation/data/tiny-clusters/christian-secular.json @@ -1,8 +1,135 @@ { - "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." + "format": "commonality-tiny-cluster-v1", + "id": "christian-secular", + "collectionId": "christian-secular-bridge", + "clusterSlug": "christian-secular", + "mediatorHardhatIndex": 8, + "mediatorName": "Christian / secular-conservative mediator", + "mediatorDescription": "Finds statements practising Christians and non-religious conservatives can both sign, without either side adopting the other’s reasons.", + "mediatorNote": "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.", + "nudgeReason": "Mediator wording that keeps your reasons while naming the overlapping claim.", + "attachMediatorServiceUrlEnv": "SEED_CHRISTIAN_MEDIATOR_URL", + "attachMediatorDefaultUrl": "http://127.0.0.1:3011", + "parents": [ + { + "id": "christianity", + "ownerHardhatIndex": 0, + "slug": "christianity", + "title": "Christianity", + "summary": "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.", + "naturals": [ + "abortion/natural-christian", + "markets/natural-christian", + "lgbt/natural-christian", + "scripture/natural-christian" + ], + "modifiedSlug": "christian-secular-christianity-modified", + "modifiedTitle": "Christianity (modified, mediator wording)", + "modifiedSummary": "Mediator wording of practising-Christian planks that still imply the shared abortion, markets, and LGBT claims. Not an official revision of the Christianity cause.", + "modifieds": [ + "abortion/modified-christian", + "markets/modified-christian", + "lgbt/modified-christian" + ], + "attachMediator": true + }, + { + "id": "secular-conservatism", + "ownerHardhatIndex": 9, + "slug": "secular-conservatism", + "title": "Secular conservatism", + "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.", + "naturals": [ + "abortion/natural-secular", + "markets/natural-secular", + "lgbt/natural-secular", + "colorblind-merit/natural-secular" + ], + "modifiedSlug": "christian-secular-secular-conservatism-modified", + "modifiedTitle": "Secular conservatism (modified, mediator wording)", + "modifiedSummary": "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.", + "modifieds": [ + "abortion/modified-secular", + "markets/modified-secular", + "lgbt/modified-secular" + ] + } + ], + "bridge": { + "slug": "christian-secular-bridge", + "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.", + "planks": [ + "abortion/commonality", + "markets/commonality", + "lgbt/commonality" + ] + }, + "personas": [ + { + "id": "christian-organizer", + "hardhatIndex": 0, + "side": "christian", + "takesModified": false, + "signsNaturals": ["abortion/natural-christian", "markets/natural-christian", "lgbt/natural-christian", "scripture/natural-christian"], + "aligns": true + }, + { + "id": "christian-nudge-taker", + "hardhatIndex": 1, + "side": "christian", + "takesModified": true, + "signsNaturals": ["abortion/natural-christian", "markets/natural-christian", "lgbt/natural-christian", "scripture/natural-christian"], + "aligns": true + }, + { + "id": "christian-natural-only", + "hardhatIndex": 2, + "side": "christian", + "takesModified": false, + "signsNaturals": ["abortion/natural-christian", "lgbt/natural-christian", "scripture/natural-christian"], + "aligns": false + }, + { + "id": "christian-markets-modified", + "hardhatIndex": 4, + "side": "christian", + "takesModified": true, + "signsNaturals": ["markets/natural-christian", "scripture/natural-christian"], + "aligns": false + }, + { + "id": "secular-founder", + "hardhatIndex": 9, + "side": "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, + "side": "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, + "side": "secular", + "takesModified": false, + "signsNaturals": ["abortion/natural-secular", "colorblind-merit/natural-secular"], + "aligns": true + }, + { + "id": "secular-lgbt-modified", + "hardhatIndex": 3, + "side": "secular", + "takesModified": true, + "signsNaturals": ["lgbt/natural-secular", "markets/natural-secular"], + "aligns": true + } ], "projects": [ { @@ -86,70 +213,11 @@ "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 - } + "buys": [ + { "accountIndex": 4, "projectId": "parish-warming", "count": 6 }, + { "accountIndex": 5, "projectId": "colorblind-admissions", "count": 3 }, + { "accountIndex": 6, "projectId": "colorblind-admissions", "count": 4 }, + { "accountIndex": 4, "projectId": "apprenticeship-fund", "count": 2 }, + { "accountIndex": 1, "projectId": "parish-warming", "count": 5 } ] } diff --git a/fake-data-generation/data/tiny-clusters/compromise-abortion.json b/fake-data-generation/data/tiny-clusters/compromise-abortion.json new file mode 100644 index 00000000..ab1cb1e1 --- /dev/null +++ b/fake-data-generation/data/tiny-clusters/compromise-abortion.json @@ -0,0 +1,98 @@ +{ + "format": "commonality-tiny-cluster-v1", + "id": "compromise-abortion", + "collectionId": "compromise-abortion", + "clusterSlug": "compromise-abortion", + "mediatorHardhatIndex": 8, + "mediatorName": "Left / right abortion mediator", + "mediatorNote": "Gestational-cutoff compromise. Camp boards hold naturals only. Modifieds and the shared settlement are mediator-authored. This is a real policy gap, not the Christian×secular same-conclusion pairing.", + "nudgeReason": "Mediator wording that keeps your concern and names the 12–16 week settlement.", + "parents": [ + { + "id": "left", + "ownerHardhatIndex": 3, + "slug": "abortion-left", + "title": "Abortion access", + "summary": "People who want abortion available so women are not forced through a pregnancy they do not want. The 12–16 week settlement is not this board — it lives on the mediator cluster.", + "naturals": ["abortion-gestational-cutoff/natural-left"], + "modifiedSlug": "compromise-abortion-left-modified", + "modifiedTitle": "Abortion access (modified, mediator wording)", + "modifiedSummary": "Mediator wording that keeps the access concern and adds the 12–16 week settlement. Not an official revision of the access board.", + "modifieds": ["abortion-gestational-cutoff/modified-left"] + }, + { + "id": "right", + "ownerHardhatIndex": 6, + "slug": "abortion-right", + "title": "Late-term abortion is horrific", + "summary": "People whose primary concern is later-term abortion. The 12–16 week settlement is not this board — it lives on the mediator cluster.", + "naturals": ["abortion-gestational-cutoff/natural-right"], + "modifiedSlug": "compromise-abortion-right-modified", + "modifiedTitle": "Late-term horror (modified, mediator wording)", + "modifiedSummary": "Mediator wording that keeps the late-term concern and adds the 12–16 week settlement. Not an official revision of the late-term board.", + "modifieds": ["abortion-gestational-cutoff/modified-right"] + } + ], + "bridge": { + "slug": "compromise-abortion-bridge", + "title": "Abortion — 12–16 week settlement", + "summary": "Bridge cause whose featured plank is implied by each modified wording: allow abortion in the first 12–16 weeks, forbid it afterward, and prefer settlement to endless fighting.", + "planks": ["abortion-gestational-cutoff/commonality"] + }, + "personas": [ + { + "id": "left-nudge-taker", + "hardhatIndex": 3, + "side": "left", + "takesModified": true, + "signsNaturals": ["abortion-gestational-cutoff/natural-left"], + "aligns": true + }, + { + "id": "left-natural-only", + "hardhatIndex": 1, + "side": "left", + "takesModified": false, + "signsNaturals": ["abortion-gestational-cutoff/natural-left"], + "aligns": false + }, + { + "id": "right-nudge-taker", + "hardhatIndex": 6, + "side": "right", + "takesModified": true, + "signsNaturals": ["abortion-gestational-cutoff/natural-right"], + "aligns": true + }, + { + "id": "right-natural-only", + "hardhatIndex": 5, + "side": "right", + "takesModified": false, + "signsNaturals": ["abortion-gestational-cutoff/natural-right"], + "aligns": false + } + ], + "projects": [ + { + "id": "first-trimester-access", + "name": "First-trimester decision clinic", + "description": "Keep a clinic open that can schedule an abortion within the first 12–16 weeks, with counseling that treats the cutoff as the settlement rather than a stepping stone.", + "kind": "abortion-compromise", + "ownerIndex": 3, + "alignments": ["abortion-gestational-cutoff/modified-left"] + }, + { + "id": "late-term-cutoff-enforcement", + "name": "After-cutoff enforcement brief", + "description": "A short public brief arguing that once the 12–16 week settlement is law, later abortions are not an undo button. Not a total ban.", + "kind": "abortion-compromise", + "ownerIndex": 6, + "alignments": ["abortion-gestational-cutoff/modified-right"] + } + ], + "buys": [ + { "accountIndex": 3, "projectId": "first-trimester-access", "count": 2 }, + { "accountIndex": 6, "projectId": "late-term-cutoff-enforcement", "count": 2 } + ] +} diff --git a/fake-data-generation/seed-content/compromise-immigration.json b/fake-data-generation/seed-content/compromise-immigration.json new file mode 100644 index 00000000..abd70966 --- /dev/null +++ b/fake-data-generation/seed-content/compromise-immigration.json @@ -0,0 +1,53 @@ +{ + "format": "commonality-seed-content-v1", + "id": "compromise-immigration", + "title": "Immigration — compromise in the middle", + "description": "Accepted left/right enforcement-overlap bridge triple. Copied from statement-generation-exercises/03-compromise-immigration.json after Adam accepted it on 2026-08-31.", + "notes": [ + "Curriculum step 3: a real-gap compromise-in-the-middle bridge. Process: fake-data-generation/statement-generation.md.", + "Naturals state each camp's concern without the deal. Modifieds reaffirm that concern and add the smallest mixed-outcome settlement each camp can sign. Commonality contains only the shared settlement.", + "Not the culture-vs-race misunderstanding cluster on hidden-majority-patterns.md.", + "Live attester on 2026-08-31 with deepseek/deepseek-v3.2: both designed-yes arrows blessed and all eight designed-no arrows refused with high confidence." + ], + "groups": [ + { + "id": "immigration-criminal-priority-overlap", + "title": "Immigration — deport criminals; accept mixed outcomes for the peaceful", + "notes": [ + "Gap: the moderate left's primary concern is leaving peaceful otherwise-law-abiding unauthorized people alone; the moderate right's primary concern is that being here illegally is itself enough to deport, with criminals first. Neither natural states a willingness to settle.", + "Modified → commonality is implication containment. Natural → modified remains a nudge because the mixed-outcome settlement is extra beliefs." + ], + "statements": [ + { + "id": "natural-left", + "role": "natural-left", + "text": "People who are here illegally but otherwise keep their heads down and work shouldn't be hunted down. Deport the ones who commit other crimes." + }, + { + "id": "natural-right", + "role": "natural-right", + "text": "If you're here illegally you should be deported. Start with the ones who've committed other crimes, then the rest." + }, + { + "id": "modified-left", + "role": "modified-left", + "text": "People who are here illegally but otherwise keep their heads down and work shouldn't be hunted down. I'd prefer we only deport people here illegally who've also committed other crimes, and leave the peaceful ones alone. I can live with some peaceful ones getting deported as a side effect of actually deporting the ones who've committed other crimes, and I already accept that some peaceful ones stay. I'd rather get this settled than keep fighting over it forever." + }, + { + "id": "modified-right", + "role": "modified-right", + "text": "If you're here illegally you should be deported. I'd still rather we deport the peaceful ones too, but I don't feel as strongly about them as about the ones who've committed other crimes. Deporting people who are here illegally and have committed other crimes, while accepting that some peaceful ones get deported and some don't, isn't what I'd write if I were making the law alone, but I'd be okay with that if it meant we got this settled instead of fighting over it forever." + }, + { + "id": "commonality", + "role": "commonality", + "text": "I'd be okay with it if we deport people who are here illegally and have committed other crimes, even though that means some peaceful ones get deported too and some peaceful ones don't. This isn't my ideal outcome, but I'd rather get this settled than keep fighting over it forever." + } + ], + "implicationNotes": [ + "Expect yes: modified-left → commonality; modified-right → commonality.", + "Expect no: natural-left → commonality; natural-right → commonality; natural-left → modified-left; natural-right → modified-right; either modified → the other modified; commonality → either modified." + ] + } + ] +} diff --git a/fake-data-generation/seed-content/crime-repeat-offenders.json b/fake-data-generation/seed-content/crime-repeat-offenders.json new file mode 100644 index 00000000..d4b4c77d --- /dev/null +++ b/fake-data-generation/seed-content/crime-repeat-offenders.json @@ -0,0 +1,53 @@ +{ + "format": "commonality-seed-content-v1", + "id": "crime-repeat-offenders", + "title": "Crime — repeat-offender concentration (fact-conditional)", + "description": "Accepted left/right same-values-different-beliefs bridge triple. Copied from statement-generation-exercises/04-crime-repeat-offenders.json after Adam accepted it on 2026-08-31.", + "notes": [ + "Curriculum step 3: a real-gap same-values-different-beliefs bridge. Process: fake-data-generation/statement-generation.md.", + "Naturals state each camp's talk without the if-true-then-priority deal. Modifieds add that deal in camp-specific prose. Commonality is only the shared conditional plus deferring the rest of the justice-system fight.", + "Not the rehabilitation-works sketch, not policing 'no major controversy', and not the racism-in-the-justice-system conditional on hidden-majority-patterns.md.", + "Live attester on 2026-08-31 with deepseek/deepseek-v3.2: both designed-yes arrows blessed and all eight designed-no arrows refused with high confidence." + ], + "groups": [ + { + "id": "crime-repeat-offender-concentration", + "title": "Crime — if repeat offenders dominate violent crime, keep them off the streets", + "notes": [ + "Gap: the moderate left's natural talk is against three-strikes / more incarceration and does not grant the concentration fact. The moderate right's natural talk asserts the fact and the incapacitation policy as obvious. Neither natural states the if-true-then-priority deal that lets the left sign without conceding the fact.", + "Modified → commonality is implication containment. Natural → modified remains a nudge because the conditional deal (and on the right, a camp extra plus deferring racism/rehab) is extra beliefs." + ], + "statements": [ + { + "id": "natural-left", + "role": "natural-left", + "text": "I'm not signing up for three-strikes laws. A lot of people cycling through the system never got a fair shot, and locking more of them up doesn't fix poverty, addiction, or a stacked justice system." + }, + { + "id": "natural-right", + "role": "natural-right", + "text": "A handful of repeat offenders do most of the violent crime. Stop catch-and-release. Three strikes, you're out." + }, + { + "id": "modified-left", + "role": "modified-left", + "text": "I'm not signing up for three-strikes laws as a general program. I don't believe a small number of repeat offenders actually account for a disproportionate share of violent crime — poverty, addiction, and a stacked system explain more of it, as far as I can tell. But if it turned out that a relatively small number of career violent criminals really do a disproportionate amount of the violent crime, then of course we should focus on keeping those people off the streets, including with something like three-strikes for violent offenses. I still want to argue about racism and rehabilitation, just not as a reason to keep dangerous career criminals in the rotation if that's how the crime is concentrated; keeping them off the streets would be high priority in that case." + }, + { + "id": "modified-right", + "role": "modified-right", + "text": "A handful of repeat offenders do most of the violent crime. Stop catch-and-release. I'd still rather we used three-strikes on repeat burglary and car theft too, not just violent offenses, but I don't feel as strongly about those as about the career violent ones. I already think a relatively small number of repeat offenders do a disproportionate amount of the violent crime, so of course we should focus on keeping those people off the streets, including with three-strikes for violent offenses. If that concentration weren't real, I wouldn't treat incapacitation as the main lever. We can argue about racism and rehabilitation another time; keeping dangerous career criminals off the streets is high priority." + }, + { + "id": "commonality", + "role": "commonality", + "text": "If it's true that a relatively small number of repeat offenders do a disproportionate amount of the violent crime, we should focus on keeping those people off the streets, including with something like three-strikes laws for violent offenses. We can argue about racism and rehabilitation another time; keeping dangerous career criminals off the streets is high priority if that's how the crime is concentrated." + } + ], + "implicationNotes": [ + "Expect yes: modified-left → commonality; modified-right → commonality.", + "Expect no: natural-left → commonality; natural-right → commonality; natural-left → modified-left; natural-right → modified-right; either modified → the other modified; commonality → either modified." + ] + } + ] +} diff --git a/fake-data-generation/seed-content/lgbt-schools.json b/fake-data-generation/seed-content/lgbt-schools.json new file mode 100644 index 00000000..ec00db21 --- /dev/null +++ b/fake-data-generation/seed-content/lgbt-schools.json @@ -0,0 +1,53 @@ +{ + "format": "commonality-seed-content-v1", + "id": "lgbt-schools", + "title": "LGBT — schools pushing (fact-conditional)", + "description": "Accepted left/right same-values-different-beliefs bridge triple. Copied from statement-generation-exercises/05-lgbt-schools.json after Adam accepted it on 2026-08-31 (commonality wording is Adam's clarification).", + "notes": [ + "Curriculum step 3: a real-gap same-values-different-beliefs bridge. Process: fake-data-generation/statement-generation.md.", + "Naturals state each camp's talk without the if-true-then-wrong deal. Modifieds add that deal in camp-specific prose. Commonality is only the shared conditional plus deferring support-versus-push.", + "Not the Christian×secular LGBT unbundle, not LGB-vs-T coalition unbundling.", + "Live attester on 2026-08-31 with deepseek/deepseek-v3.2, including after Adam's commonality reword: both designed-yes arrows blessed and all eight designed-no arrows refused with high confidence." + ], + "groups": [ + { + "id": "lgbt-schools-pushing", + "title": "LGBT — if schools are pushing kids toward LGBT identities, that's wrong", + "notes": [ + "Gap: the moderate left's natural talk denies the push (support is not a push; calling it one is a smear). The moderate right's natural talk asserts the push and condemns it as obvious. Neither natural states the if-true-then-wrong deal that lets the left sign without conceding the fact.", + "Modified → commonality is implication containment. Natural → modified remains a nudge because the conditional deal (and on the right, a camp extra) is extra beliefs." + ], + "statements": [ + { + "id": "natural-left", + "role": "natural-left", + "text": "Schools aren't pushing kids toward LGBT identities. They're just being supportive of kids who already worked that out themselves. Calling that a push is a smear." + }, + { + "id": "natural-right", + "role": "natural-right", + "text": "Schools are actively pushing kids toward LGBT identities. That's wrong. Stop it." + }, + { + "id": "modified-left", + "role": "modified-left", + "text": "I don't think schools are pushing kids toward LGBT identities. They're being supportive of kids who already worked it out themselves, and calling that a push is a smear. But if it turned out schools really are pushing kids in that direction, that's wrong — of course they shouldn't be doing it. I still want to argue about what counts as support versus a push; I just wouldn't defend an actual push if that's what's happening." + }, + { + "id": "modified-right", + "role": "modified-right", + "text": "Schools are actively pushing kids toward LGBT identities. That's wrong. I'd still rather we also stopped social transition at school without parents even in cases that look like 'just support,' but I don't feel as strongly about borderline support as about an actual push. I already think schools are pushing, so of course that's wrong. If they really weren't pushing, I wouldn't treat this as the same fight. We can argue about curriculum details another time; if schools are pushing kids toward LGBT identities, that's wrong." + }, + { + "id": "commonality", + "role": "commonality", + "text": "If schools really are pushing kids toward LGBT identities, that's wrong. We can argue about what counts as 'supporting kids who are LGBT' versus 'pushing kids towards LGBT identities' another time, but to the extent that the latter is what's happening, that is not okay." + } + ], + "implicationNotes": [ + "Expect yes: modified-left → commonality; modified-right → commonality.", + "Expect no: natural-left → commonality; natural-right → commonality; natural-left → modified-left; natural-right → modified-right; either modified → the other modified; commonality → either modified." + ] + } + ] +} diff --git a/fake-data-generation/seedChristianityCause.ts b/fake-data-generation/seedChristianityCause.ts index d7627891..dcbe5b27 100644 --- a/fake-data-generation/seedChristianityCause.ts +++ b/fake-data-generation/seedChristianityCause.ts @@ -1,45 +1,25 @@ /** - * Seed a Christianity cause that exercises CauseStarter's mediator card: - * published roster with a machine-readable mediator, several LazyGiving - * projects, a mixed content contract, plank signers, and monthly pledges. + * Tiny CauseStarter story orchestrator: local-food bookmarks, Christianity + * content-funding extras, recurring pledges, and every tiny-cluster JSON file. * - * Safe to run against an already-seeded local chain (does not wipe). Also - * called from the main simulation so a fresh `--seed` includes the same story. + * Cluster world-building is data/tiny-clusters/*.json + seedTinyCluster.ts. + * Do not add issue-specific publishers here. */ import { privateKeyToAccount } from 'viem/accounts'; import { fileURLToPath } from 'url'; import { - AlignmentAttestationsAbi, - AssuranceContractAbi, - BeliefsAbi, - ImplicationsAbi, MutableRefUpdaterAbi, - NudgePublicationsAbi, - ProjectFactoryAbi, - PublishedDataAbi, RecurringPledgesAbi, } from '@commonality/sdk/abis'; -import { - createDefaultDocumentStore, - createDisplayableDocument, -} from '@commonality/sdk/displayable-documents'; import { approveRecurringPledgeToken, createStandingPledge } from '@commonality/sdk/delegation'; -import { attestAlignment, PROJECT_ALIGNMENT_TOPIC, toSubjectId } from '@commonality/sdk/fundingportals'; -import { buyProjectTokens, createProject as sdkCreateProject } from '@commonality/sdk/lazy-giving'; -import { createSDKMachinery } from '@commonality/sdk/machinery'; import { getRef, updateRef } from '@commonality/sdk/mutable-refs'; -import { createIPFSConfigInNodeJSFromTheUsualEnvVars } from '@commonality/sdk/node'; -import { cidToBytes32, type IpfsCidV1, uploadToIPFS, type WriteClients } from '@commonality/sdk/utils'; -import { publishGeneratedStatement } from './generateStatements.js'; -import { HARDHAT_PRIVATE_KEYS } from './generateUsers.js'; +import { type IpfsCidV1, type WriteClients } from '@commonality/sdk/utils'; import { CONTRACT_ADDRESSES, loadEnv, RPC_URL } from './loadEnv.js'; 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, SEED_CAUSE_OWNER_ADDRESS, @@ -48,489 +28,151 @@ 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'; -import { dirname, join } from 'path'; + bridgeRosterFields, + clusterBookmarkEntries, + clusterDocumentFields, + deriveImplications, + deriveNudges, + loadTinyClusterDefs, + mediatorAddress, + mediatorServiceUrl, + modifiedRosterFields, + parentPlanks, + parentRosterFields, + pickAlignmentAttester, + publishAllTinyClusters, + publishClusterStatements, + publishTinyClusterDocuments, + requireParent, + requireTinyCluster, + sideOfAlignment, +} from './seedTinyCluster.js'; loadEnv(); -const paymentTokenFundingAbi = [ - { - name: 'transfer', - type: 'function', - stateMutability: 'nonpayable', - inputs: [ - { name: 'to', type: 'address' }, - { name: 'amount', type: 'uint256' }, - ], - outputs: [{ name: '', type: 'bool' }], - }, - { - name: 'mintTo', - type: 'function', - stateMutability: 'nonpayable', - inputs: [ - { name: 'to', type: 'address' }, - { name: 'amount', type: 'uint256' }, - ], - outputs: [], - }, -] as const; - -const BELIEVES = 1; const MONTH_SECONDS = 30n * 24n * 60n * 60n; -export const CHRISTIANITY_CAUSE_SLUG = 'christianity'; -export const CHRISTIANITY_CAUSE_TITLE = 'Christianity'; -export const CHRISTIANITY_CAUSE_SUMMARY = - '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]!; -export const CHRISTIAN_MEDIATOR_ADDRESS = privateKeyToAccount(CHRISTIAN_MEDIATOR_PRIVATE_KEY).address; +const christianSecular = requireTinyCluster('christian-secular'); +const christianityParent = requireParent(christianSecular, 'christianity'); +const secularParent = requireParent(christianSecular, 'secular-conservatism'); -export const CHRISTIAN_MEDIATOR_NAME = 'Christian / secular-conservative mediator'; +export const CHRISTIANITY_CAUSE_SLUG = christianityParent.slug; +export const CHRISTIANITY_CAUSE_TITLE = christianityParent.title; +export const CHRISTIANITY_CAUSE_SUMMARY = christianityParent.summary; +export const CHRISTIAN_MEDIATOR_PRIVATE_KEY = FUNDED_HARDHAT_DEV_KEYS[christianSecular.mediatorHardhatIndex]!; +export const CHRISTIAN_MEDIATOR_ADDRESS = mediatorAddress(christianSecular); +export const CHRISTIAN_MEDIATOR_NAME = christianSecular.mediatorName; export const CHRISTIAN_MEDIATOR_DESCRIPTION = - 'Finds statements practising Christians and non-religious conservatives can both sign, without either side adopting the other’s reasons.'; + christianSecular.mediatorDescription ?? christianSecular.mediatorNote; export function seedChristianMediatorServiceUrl(): string { - return (process.env.SEED_CHRISTIAN_MEDIATOR_URL ?? 'http://127.0.0.1:3011').replace(/\/+$/, ''); + return mediatorServiceUrl(christianSecular); } -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]!; +export const CHRISTIANITY_PLANKS = parentPlanks(christianSecular, 'christianity'); +export const SECULAR_CONSERVATIVE_OWNER_KEY = FUNDED_HARDHAT_DEV_KEYS[secularParent.ownerHardhatIndex]!; export const SECULAR_CONSERVATIVE_OWNER_ADDRESS = privateKeyToAccount(SECULAR_CONSERVATIVE_OWNER_KEY).address; -export const SECULAR_CONSERVATIVE_CAUSE_SLUG = 'secular-conservatism'; -export const SECULAR_CONSERVATIVE_CAUSE_TITLE = 'Secular conservatism'; -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 SECULAR_CONSERVATIVE_CAUSE_SLUG = secularParent.slug; +export const SECULAR_CONSERVATIVE_CAUSE_TITLE = secularParent.title; +export const SECULAR_CONSERVATIVE_CAUSE_SUMMARY = secularParent.summary; +export const SECULAR_CONSERVATIVE_PLANKS = parentPlanks(christianSecular, 'secular-conservatism'); +export const CHRISTIAN_SECULAR_CLUSTER_SLUG = christianSecular.clusterSlug; +export const CHRISTIAN_MODIFIED_CAUSE_SLUG = christianityParent.modifiedSlug; +export const SECULAR_MODIFIED_CAUSE_SLUG = secularParent.modifiedSlug; +export const CHRISTIAN_SECULAR_BRIDGE_CAUSE_SLUG = christianSecular.bridge.slug; +export const CHRISTIAN_SECULAR_CLUSTER_NOTE = christianSecular.mediatorNote; +export const CHRISTIANITY_PROJECTS = christianSecular.projects; +export const NATURAL_TO_MODIFIED_NUDGES = deriveNudges(christianSecular); +export const BLESSED_MODIFIED_TO_COMMONALITY = deriveImplications(christianSecular); -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; - 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; +function createClients(privateKey: `0x${string}`) { + return createSeedClients(privateKey, RPC_URL); } -export const CHRISTIANITY_PROJECTS = loadPersonaProjects(); - export function campOfAlignment(alignmentId: string): 'christian' | 'secular' { - const statementId = alignmentId.split('/')[1] ?? alignmentId; - return statementId.includes('secular') ? 'secular' : 'christian'; + return sideOfAlignment(alignmentId) === '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] - ); -} +export { pickAlignmentAttester }; -function createClients(privateKey: `0x${string}`) { - return createSeedClients(privateKey, RPC_URL); -} - -async function fundPaymentToken(to: `0x${string}`, amount: bigint): Promise { - const token = process.env.PAYMENT_TOKEN_ADDRESS as `0x${string}` | undefined; - if (!token) throw new Error('PAYMENT_TOKEN_ADDRESS not configured'); - const funder = createClients(FUNDED_HARDHAT_DEV_KEYS[0]!); +async function mergeBookmarks(): Promise { + const mutableRef = CONTRACT_ADDRESSES.mutableRefUpdater as `0x${string}` | undefined; + if (!mutableRef) return; + const refContract = { address: mutableRef, abi: MutableRefUpdaterAbi }; + const reader = createClients(FUNDED_HARDHAT_DEV_KEYS[0]!); + let existing: { owner: string; slug: string }[] = []; try { - const hash = await funder.walletClient.writeContract({ - address: token, - abi: paymentTokenFundingAbi, - functionName: 'transfer', - args: [to, amount], - chain: funder.walletClient.chain, - account: funder.walletClient.account!, - }); - await funder.publicClient.waitForTransactionReceipt({ hash }); + const raw = await getRef( + reader as WriteClients, + refContract, + reader.account, + CAUSE_BOOKMARKS_REF, + ); + if (raw) { + const parsed = JSON.parse(raw) as { causes?: { owner: string; slug: string }[] }; + existing = parsed.causes ?? []; + } } catch { - const hash = await funder.walletClient.writeContract({ - address: token, - abi: paymentTokenFundingAbi, - functionName: 'mintTo', - args: [to, amount], - chain: funder.walletClient.chain, - account: funder.walletClient.account!, - }); - await funder.publicClient.waitForTransactionReceipt({ hash }); + existing = []; } -} - -async function publishStatementSet( - statements: readonly { id: string; text: string }[], - domain: string, - publisherKey: `0x${string}`, - cids: Map, - publishOnChain: boolean, -): Promise { - const publishedData = CONTRACT_ADDRESSES.publishedData as `0x${string}` | undefined; - const owner = createClients(publisherKey); - const ipfsConfig = createIPFSConfigInNodeJSFromTheUsualEnvVars(); - for (const plank of statements) { - const cid = await publishGeneratedStatement( - ipfsConfig, - { text: plank.text, domain, position: plank.id }, - domain, - plank.id, - 'simple', - publishOnChain && publishedData - ? { clients: owner as WriteClients, publishedDataAddress: publishedData } - : {}, - ); - cids.set(plank.id, cid); - console.log(` ${publishOnChain ? 'Published' : 'Resolved'} ${domain} ${plank.id} → ${cid}`); + const wanted = [ + { owner: SEED_CAUSE_OWNER_ADDRESS, slug: SEED_CAUSE_SLUG }, + ...loadTinyClusterDefs().flatMap(clusterBookmarkEntries), + ]; + const seen = new Set(existing.map((item) => `${item.owner.toLowerCase()}/${item.slug}`)); + const merged = [...existing]; + for (const item of wanted) { + const key = `${item.owner.toLowerCase()}/${item.slug}`; + if (!seen.has(key)) merged.push(item); + } + const value = serializeSeedCauseBookmarkList(merged); + for (const key of FUNDED_HARDHAT_DEV_KEYS) { + const clients = createClients(key); + await updateRef(clients as WriteClients, refContract, CAUSE_BOOKMARKS_REF, value); } } -async function resolvePlankCids(publishOnChain: boolean): Promise> { - const cids = new Map(); - 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); +export function christianityRosterFields(plankCids: string[]): SeedCauseRosterFields { + return parentRosterFields(christianSecular, christianityParent, plankCids); } -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}`); +export function secularConservativeRosterFields(plankCids: string[]): SeedCauseRosterFields { + return parentRosterFields(christianSecular, secularParent, plankCids); } -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`); +export function christianModifiedRosterFields(plankCids: string[]): SeedCauseRosterFields { + return modifiedRosterFields(christianSecular, christianityParent, plankCids); } -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}`; +export function secularModifiedRosterFields(plankCids: string[]): SeedCauseRosterFields { + return modifiedRosterFields(christianSecular, secularParent, plankCids); } -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; - } - 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); - 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, - functionName: 'setBelief', - args: [cidToBytes32(cid), BELIEVES], - chain: clients.walletClient.chain, - account: clients.walletClient.account, - }); - await clients.publicClient.waitForTransactionReceipt({ hash }); - signed += 1; - } - console.log(` ✓ HH#${persona.hardhatIndex} (${persona.id}) signed ${signed} statements`); - } +export function christianSecularBridgeRosterFields(plankCids: string[]): SeedCauseRosterFields { + return bridgeRosterFields(christianSecular, plankCids); } -interface CreatedChristianProject { - id: string; - name: string; - plankId: string; - assuranceContract: `0x${string}`; - erc1155: `0x${string}`; - tokenIds: number[]; - prices: string[]; +export function christianSecularClusterFields(cids: Map) { + return clusterDocumentFields(christianSecular, cids); } -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; - if (!factory || !paymentToken) { - console.warn('ProjectFactory or payment token missing — skipping Christianity projects.'); - return []; - } - - const created: CreatedChristianProject[] = []; - const latest = await createClients(FUNDED_HARDHAT_DEV_KEYS[0]!).publicClient.getBlock(); - const deadline = latest.timestamp + 30n * 24n * 60n * 60n; - const threshold = parsePaymentTokenUnits('2'); - const tokenIds = [1n, 2n, 3n]; - const maxSupplies = [100n, 500n, 1000n]; - const prices = [ - parsePaymentTokenUnits('0.1'), - parsePaymentTokenUnits('0.05'), - parsePaymentTokenUnits('0.01'), - ]; - - for (const template of loadPersonaProjects()) { - const key = FUNDED_HARDHAT_DEV_KEYS[template.ownerIndex]; - if (!key) continue; - const clients = createClients(key); - const ipfsConfig = createIPFSConfigInNodeJSFromTheUsualEnvVars(); - const store = createDefaultDocumentStore(createSDKMachinery({ ipfsConfig }), { - clients: clients as WriteClients, - ...(publishedData - ? { publishedDataContract: { address: publishedData, abi: PublishedDataAbi } } - : {}), - }); - const alignedStatementRefs = template.alignments.map((alignment) => { - const [groupId, statementId] = alignment.split('/'); - return { collectionId: 'christian-secular-bridge', groupId, statementId }; - }); - const publication = await store.publish(createDisplayableDocument({ - format: 'markdown-restricted', - content: template.description, - extras: { - statementType: 'lazy-giving-project-metadata', - name: template.name, - description: template.description, - seedProjectKind: template.kind, - alignedStatementRefs, - }, - })); - const { projectDetails } = await sdkCreateProject( - clients as WriteClients, - { address: factory, abi: ProjectFactoryAbi }, - { - metadataURI: `ipfs://${publication.cid}/`, - contractURI: `ipfs://${publication.cid}`, - owner: clients.account, - recipient: clients.account, - paymentToken, - threshold, - deadline, - projectMetadataCid: publication.cid, - tokenIds, - tokenCounts: maxSupplies, - tokenPrices: prices, - }, - ); - created.push({ - id: template.id, - name: template.name, - plankId: template.alignments[0] ?? template.id, - assuranceContract: projectDetails.assuranceContractAddress, - erc1155: projectDetails.tokenAddress, - tokenIds: tokenIds.map(Number), - prices: prices.map((price) => price.toString()), - }); - console.log(` ✓ Project ${template.name} → ${projectDetails.assuranceContractAddress}`); - - const alignment = CONTRACT_ADDRESSES.alignmentAttestations as `0x${string}` | undefined; - const personas = loadPersonaFile().personas; - 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 = pickAlignmentAttester(personas, alignmentId, template.ownerIndex); - 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 }); - } - } +async function resolvePlankCids(publishOnChain: boolean): Promise> { + const cids = new Map(); + for (const cluster of loadTinyClusterDefs()) { + await publishClusterStatements(cluster, cids, publishOnChain); } - return created; + return cids; } -async function buyAndPledge(projects: CreatedChristianProject[], plankCids: Map): Promise { +async function seedChristianPledges(plankCids: Map): Promise { const paymentToken = process.env.PAYMENT_TOKEN_ADDRESS as `0x${string}` | undefined; - if (!paymentToken) return; - - const buys = [ - { accountIndex: 4, projectId: 'parish-warming', count: 6 }, - { accountIndex: 5, projectId: 'colorblind-admissions', count: 3 }, - { accountIndex: 6, projectId: 'colorblind-admissions', count: 4 }, - { accountIndex: 4, projectId: 'apprenticeship-fund', count: 2 }, - { accountIndex: 1, projectId: 'parish-warming', count: 5 }, - ]; - const personas = loadPersonaFile().personas; - for (const buy of buys) { - const project = projects.find((candidate) => 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 || !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]!); - try { - await buyProjectTokens( - clients as WriteClients, - { address: project.assuranceContract, abi: AssuranceContractAbi }, - { - buyer: clients.account, - tokenAddress: project.erc1155, - tokenIds: [BigInt(project.tokenIds[0]!)], - tokenCounts: [BigInt(buy.count)], - totalCost: price * BigInt(buy.count), - }, - ); - console.log(` ✓ HH#${buy.accountIndex} bought ${buy.count} on ${project.name}`); - } catch (error) { - console.warn(` Failed buy on ${project.name}:`, error instanceof Error ? error.message : error); - } - } - 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'); - if (!recurringPledges || !notes || !scripture) { + if (!paymentToken || !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 = [ @@ -568,262 +210,45 @@ async function buyAndPledge(projects: CreatedChristianProject[], plankCids: Map< } } -async function mergeBookmarks(): Promise { - const mutableRef = CONTRACT_ADDRESSES.mutableRefUpdater as `0x${string}` | undefined; - if (!mutableRef) return; - const refContract = { address: mutableRef, abi: MutableRefUpdaterAbi }; - const reader = createClients(FUNDED_HARDHAT_DEV_KEYS[0]!); - let existing: { owner: string; slug: string }[] = []; - try { - const raw = await getRef( - reader as WriteClients, - refContract, - reader.account, - CAUSE_BOOKMARKS_REF, - ); - if (raw) { - const parsed = JSON.parse(raw) as { causes?: { owner: string; slug: string }[] }; - existing = parsed.causes ?? []; - } - } catch { - existing = []; - } - const wanted = [ - { 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]; - for (const item of wanted) { - const key = `${item.owner.toLowerCase()}/${item.slug}`; - if (!seen.has(key)) merged.push(item); - } - const value = serializeSeedCauseBookmarkList(merged); - for (const key of FUNDED_HARDHAT_DEV_KEYS) { - const clients = createClients(key); - await updateRef(clients as WriteClients, refContract, CAUSE_BOOKMARKS_REF, value); - } -} - -export function christianityRosterFields(plankCids: string[]): SeedCauseRosterFields { - const mediator = { - name: CHRISTIAN_MEDIATOR_NAME, - description: CHRISTIAN_MEDIATOR_DESCRIPTION, - address: CHRISTIAN_MEDIATOR_ADDRESS, - serviceUrl: seedChristianMediatorServiceUrl(), - }; - return { - title: CHRISTIANITY_CAUSE_TITLE, - summary: CHRISTIANITY_CAUSE_SUMMARY, - plankCids, - mediatorBlurb: `${mediator.name}: ${mediator.description}`, - mediator, - }; -} - -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', - }, +async function seedChristianContent(plankMap: Map): Promise { + const scripture = plankMap.get('scripture/natural-christian'); + const cfAddresses = { + channelRegistry: CONTRACT_ADDRESSES.channelRegistry, + channelVerifier: CONTRACT_ADDRESSES.channelVerifier, + creatorContractFactory: CONTRACT_ADDRESSES.creatorContractFactory, + publishedData: CONTRACT_ADDRESSES.publishedData, + alignmentAttestations: CONTRACT_ADDRESSES.alignmentAttestations, }; -} - -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, - }, + if ( + !cfAddresses.channelRegistry + || !cfAddresses.channelVerifier + || !cfAddresses.creatorContractFactory + || !scripture + ) { + return; + } + try { + await generateChristianContentScenario( { - owner, - slug: SECULAR_MODIFIED_CAUSE_SLUG, - parentOwner: SECULAR_CONSERVATIVE_OWNER_ADDRESS, - parentSlug: SECULAR_CONSERVATIVE_CAUSE_SLUG, + channelRegistry: cfAddresses.channelRegistry as `0x${string}`, + channelVerifier: cfAddresses.channelVerifier as `0x${string}`, + creatorContractFactory: cfAddresses.creatorContractFactory as `0x${string}`, + publishedData: cfAddresses.publishedData as `0x${string}` | undefined, + alignmentAttestations: cfAddresses.alignmentAttestations as `0x${string}` | undefined, }, - ], - 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', slug); - return null; + FUNDED_HARDHAT_DEV_KEYS.map((privateKey) => ({ + privateKey, + address: privateKeyToAccount(privateKey).address, + })), + { statementCid: scripture }, + ); + } catch (error) { + console.warn('Christianity content contract failed (channel may already exist):', error); } - const owner = createClients(publisherKey); - const store = createDefaultDocumentStore( - createSDKMachinery({ ipfsConfig: createIPFSConfigInNodeJSFromTheUsualEnvVars() }), - { - clients: owner as WriteClients, - publishedDataContract: { address: publishedData, abi: PublishedDataAbi }, - }, - ); - const publication = await store.publish(doc); - await updateRef( - owner as WriteClients, - { address: mutableRef, abi: MutableRefUpdaterAbi }, - 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} → ${cid}\n Open /cause/${owner.account}/${CHRISTIANITY_CAUSE_SLUG}`, - ); - 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 publishChristianSecularBridgeCluster(cids: Map) { + return publishTinyClusterDocuments(christianSecular, cids); } export async function publishSeedChristianityCause(): Promise<{ @@ -831,70 +256,22 @@ export async function publishSeedChristianityCause(): Promise<{ rosterCid: string | null; plankCids: string[]; } | 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); - - const cfAddresses = { - channelRegistry: CONTRACT_ADDRESSES.channelRegistry, - channelVerifier: CONTRACT_ADDRESSES.channelVerifier, - creatorContractFactory: CONTRACT_ADDRESSES.creatorContractFactory, - publishedData: CONTRACT_ADDRESSES.publishedData, - alignmentAttestations: CONTRACT_ADDRESSES.alignmentAttestations, - }; - const scripture = plankMap.get('scripture/natural-christian'); - if ( - cfAddresses.channelRegistry - && cfAddresses.channelVerifier - && cfAddresses.creatorContractFactory - && scripture - ) { - try { - await generateChristianContentScenario( - { - channelRegistry: cfAddresses.channelRegistry as `0x${string}`, - channelVerifier: cfAddresses.channelVerifier as `0x${string}`, - creatorContractFactory: cfAddresses.creatorContractFactory as `0x${string}`, - publishedData: cfAddresses.publishedData as `0x${string}` | undefined, - alignmentAttestations: cfAddresses.alignmentAttestations as `0x${string}` | undefined, - }, - FUNDED_HARDHAT_DEV_KEYS.map((privateKey) => ({ - privateKey, - address: privateKeyToAccount(privateKey).address, - })), - { statementCid: scripture }, - ); - } catch (error) { - console.warn('Christianity content contract failed (channel may already exist):', error); - } - } - + console.log('\n=== Publishing tiny CauseStarter clusters ===\n'); + const plankMap = new Map(); + await publishAllTinyClusters(plankMap, { publishStatements: true, activity: true }); + await seedChristianPledges(plankMap); + await seedChristianContent(plankMap); + await mergeBookmarks(); 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); - await publishChristianSecularBridgeCluster(plankMap); return { slug: CHRISTIANITY_CAUSE_SLUG, - rosterCid, + rosterCid: christianPlankCids[0] ?? null, plankCids: [...plankMap.values()], }; } -export function secularConservativeRosterFields(plankCids: string[]): SeedCauseRosterFields { - return { - title: SECULAR_CONSERVATIVE_CAUSE_TITLE, - summary: SECULAR_CONSERVATIVE_CAUSE_SUMMARY, - plankCids, - mediatorBlurb: '', - }; -} - export async function publishSeedSecularConservativeCause( existingCids?: Map, ): Promise<{ @@ -902,66 +279,27 @@ export async function publishSeedSecularConservativeCause( rosterCid: string | null; plankCids: string[]; } | null> { - 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 secular-conservative roster.'); - return null; - } - console.log('\n=== Publishing seed CauseStarter roster (secular conservatism) ===\n'); - const owner = createClients(SECULAR_CONSERVATIVE_OWNER_KEY); - const plankCids: string[] = []; - 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); - const store = createDefaultDocumentStore( - createSDKMachinery({ ipfsConfig: createIPFSConfigInNodeJSFromTheUsualEnvVars() }), - { - clients: owner as WriteClients, - publishedDataContract: { address: publishedData, abi: PublishedDataAbi }, - }, - ); - const publication = await store.publish(doc); - await updateRef( - owner as WriteClients, - { address: mutableRef, abi: MutableRefUpdaterAbi }, - SECULAR_CONSERVATIVE_CAUSE_SLUG, - publication.cid, - ); + const cids = existingCids ?? await resolvePlankCids(true); + const result = await publishTinyClusterDocuments(christianSecular, cids); await mergeBookmarks(); - console.log( - ` ✓ Cause ${SECULAR_CONSERVATIVE_CAUSE_SLUG} → ${publication.cid}\n Open /cause/${owner.account}/${SECULAR_CONSERVATIVE_CAUSE_SLUG}`, - ); return { slug: SECULAR_CONSERVATIVE_CAUSE_SLUG, - rosterCid: publication.cid, - plankCids, + rosterCid: result.rosterCids[1] ?? result.rosterCids[0] ?? null, + plankCids: SECULAR_CONSERVATIVE_PLANKS.map((plank) => cids.get(plank.id)).filter( + (cid): cid is IpfsCidV1 => Boolean(cid), + ), }; } if (process.argv[1] === fileURLToPath(import.meta.url)) { const clusterOnly = process.argv.includes('--cluster-only'); const run = clusterOnly - ? resolvePlankCids(false).then((cids) => publishChristianSecularBridgeCluster(cids)) + ? resolvePlankCids(false).then(async (cids) => { + for (const cluster of loadTinyClusterDefs()) { + await publishTinyClusterDocuments(cluster, cids); + } + await mergeBookmarks(); + }) : publishSeedChristianityCause(); run .then(() => process.exit(0)) diff --git a/fake-data-generation/seedTinyCluster.ts b/fake-data-generation/seedTinyCluster.ts new file mode 100644 index 00000000..b009b11e --- /dev/null +++ b/fake-data-generation/seedTinyCluster.ts @@ -0,0 +1,849 @@ +/** + * Generic tiny-seed bridge-cluster publisher. + * + * Issue-specific world-building lives in data/tiny-clusters/*.json (owners, slugs, + * personas, projects). Statement text lives in seed-content/*.json. Adding a + * cluster should not require a new TypeScript module. + */ + +import { readdirSync, readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; +import { privateKeyToAccount } from 'viem/accounts'; +import { + AlignmentAttestationsAbi, + AssuranceContractAbi, + BeliefsAbi, + ImplicationsAbi, + MutableRefUpdaterAbi, + NudgePublicationsAbi, + ProjectFactoryAbi, + PublishedDataAbi, +} from '@commonality/sdk/abis'; +import { + createDefaultDocumentStore, + createDisplayableDocument, +} from '@commonality/sdk/displayable-documents'; +import { attestAlignment, PROJECT_ALIGNMENT_TOPIC, toSubjectId } from '@commonality/sdk/fundingportals'; +import { buyProjectTokens, createProject as sdkCreateProject } from '@commonality/sdk/lazy-giving'; +import { createSDKMachinery } from '@commonality/sdk/machinery'; +import { updateRef } from '@commonality/sdk/mutable-refs'; +import { createIPFSConfigInNodeJSFromTheUsualEnvVars } from '@commonality/sdk/node'; +import { cidToBytes32, type IpfsCidV1, uploadToIPFS, type WriteClients } from '@commonality/sdk/utils'; +import { publishGeneratedStatement } from './generateStatements.js'; +import { CONTRACT_ADDRESSES, loadEnv, RPC_URL } from './loadEnv.js'; +import { createSeedClients } from './seedRpc.js'; +import { parsePaymentTokenUnits } from './paymentTokenUnits.js'; +import { + buildSeedClusterDocument, + buildSeedRosterDocument, + FUNDED_HARDHAT_DEV_KEYS, + type SeedBridgeClusterFields, + type SeedCauseRosterFields, +} from './seedCauseRoster.js'; +import type { SeedCollection } from './seed-content-format.js'; + +loadEnv(); + +const __dirname = dirname(fileURLToPath(import.meta.url)); +export const TINY_CLUSTERS_DIR = join(__dirname, 'data', 'tiny-clusters'); +export const SEED_CONTENT_DIR = join(__dirname, 'seed-content'); + +const BELIEVES = 1; + +const paymentTokenFundingAbi = [ + { + name: 'transfer', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { name: 'to', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool' }], + }, + { + name: 'mintTo', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { name: 'to', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + outputs: [], + }, +] as const; + +export interface TinyClusterPersona { + id: string; + hardhatIndex: number; + side: string; + takesModified: boolean; + signsNaturals: string[]; + aligns: boolean; +} + +export interface TinyClusterProject { + id: string; + name: string; + description: string; + kind: string; + ownerIndex: number; + alignments: string[]; +} + +export interface TinyClusterParent { + id: string; + ownerHardhatIndex: number; + slug: string; + title: string; + summary: string; + naturals: string[]; + modifiedSlug: string; + modifiedTitle: string; + modifiedSummary: string; + modifieds: string[]; + attachMediator?: boolean; +} + +export interface TinyClusterDef { + format: 'commonality-tiny-cluster-v1'; + id: string; + collectionId: string; + clusterSlug: string; + mediatorHardhatIndex: number; + mediatorName: string; + mediatorDescription?: string; + mediatorNote: string; + nudgeReason: string; + attachMediatorServiceUrlEnv?: string; + attachMediatorDefaultUrl?: string; + parents: TinyClusterParent[]; + bridge: { slug: string; title: string; summary: string; planks: string[] }; + personas: TinyClusterPersona[]; + projects: TinyClusterProject[]; + buys?: Array<{ accountIndex: number; projectId: string; count: number }>; +} + +export interface SeedPlank { + id: string; + groupId: string; + statementId: string; + text: string; +} + +function createClients(privateKey: `0x${string}`) { + return createSeedClients(privateKey, RPC_URL); +} + +export function fundedKey(index: number): `0x${string}` { + const key = FUNDED_HARDHAT_DEV_KEYS[index]; + if (!key) throw new Error(`No FUNDED_HARDHAT_DEV_KEYS[${index}]`); + return key; +} + +export function fundedAddress(index: number): `0x${string}` { + return privateKeyToAccount(fundedKey(index)).address; +} + +export function parsePlankRef(ref: string): { groupId: string; statementId: string } { + const slash = ref.lastIndexOf('/'); + if (slash <= 0 || slash === ref.length - 1) { + throw new Error(`Plank ref must be groupId/statementId, got "${ref}"`); + } + return { groupId: ref.slice(0, slash), statementId: ref.slice(slash + 1) }; +} + +export function sideOfAlignment(alignmentId: string): string { + const statementId = alignmentId.split('/')[1] ?? alignmentId; + const match = statementId.match(/^(?:natural|modified)-(.+)$/); + return match?.[1] ?? statementId; +} + +export function modifiedIdForNatural(naturalId: string, side: string): string | null { + const { groupId, statementId } = parsePlankRef(naturalId); + if (!statementId.startsWith('natural-')) return null; + return `${groupId}/modified-${side}`; +} + +export function deriveNudges(cluster: TinyClusterDef): Array<{ target: string; suggested: string }> { + const nudges: Array<{ target: string; suggested: string }> = []; + for (const parent of cluster.parents) { + for (const natural of parent.naturals) { + const suggested = natural.replace('/natural-', '/modified-'); + if (suggested !== natural && parent.modifieds.includes(suggested)) { + nudges.push({ target: natural, suggested }); + } + } + } + return nudges; +} + +export function deriveImplications(cluster: TinyClusterDef): Array<{ from: string; to: string }> { + const pairs: Array<{ from: string; to: string }> = []; + for (const parent of cluster.parents) { + for (const modified of parent.modifieds) { + const { groupId } = parsePlankRef(modified); + const to = `${groupId}/commonality`; + if (cluster.bridge.planks.includes(to)) { + pairs.push({ from: modified, to }); + } + } + } + return pairs; +} + +export function pickAlignmentAttester( + personas: readonly TinyClusterPersona[], + alignmentId: string, + projectOwnerIndex: number, +): TinyClusterPersona | undefined { + const side = sideOfAlignment(alignmentId); + const aligners = personas.filter((persona) => persona.aligns); + const sideAligners = aligners.filter((persona) => persona.side === side); + return ( + sideAligners.find((persona) => persona.hardhatIndex === projectOwnerIndex) + ?? sideAligners[0] + ?? aligners[0] + ); +} + +export function loadSeedCollectionFile(collectionId: string): SeedCollection { + const raw = readFileSync(join(SEED_CONTENT_DIR, `${collectionId}.json`), 'utf8'); + return JSON.parse(raw) as SeedCollection; +} + +export function plankFromCollection(collection: SeedCollection, ref: string): SeedPlank { + const { groupId, statementId } = parsePlankRef(ref); + 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}/${ref}`); + } + return { id: ref, groupId, statementId, text: statement.text }; +} + +function assertSlug(slug: string): void { + if (!slug || slug.length > 64) { + throw new Error(`Cause slug must be 1–64 chars, got "${slug}"`); + } +} + +export function validateTinyCluster(cluster: TinyClusterDef, collection: SeedCollection): void { + if (cluster.format !== 'commonality-tiny-cluster-v1') { + throw new Error(`${cluster.id}: unsupported format`); + } + if (cluster.collectionId !== collection.id) { + throw new Error(`${cluster.id}: collectionId ${cluster.collectionId} != ${collection.id}`); + } + assertSlug(cluster.clusterSlug); + assertSlug(cluster.bridge.slug); + const refs = [ + ...cluster.parents.flatMap((parent) => [...parent.naturals, ...parent.modifieds]), + ...cluster.bridge.planks, + ...cluster.personas.flatMap((persona) => persona.signsNaturals), + ...cluster.projects.flatMap((project) => project.alignments), + ]; + for (const ref of refs) { + plankFromCollection(collection, ref); + } + for (const parent of cluster.parents) { + assertSlug(parent.slug); + assertSlug(parent.modifiedSlug); + fundedKey(parent.ownerHardhatIndex); + } + fundedKey(cluster.mediatorHardhatIndex); +} + +export function loadTinyClusterDefs(dir = TINY_CLUSTERS_DIR): TinyClusterDef[] { + const files = readdirSync(dir).filter((name) => name.endsWith('.json')).sort(); + const clusters = files.map((fileName) => { + const raw = JSON.parse(readFileSync(join(dir, fileName), 'utf8')) as TinyClusterDef; + const collection = loadSeedCollectionFile(raw.collectionId); + validateTinyCluster(raw, collection); + return raw; + }); + const ids = new Set(); + for (const cluster of clusters) { + if (ids.has(cluster.id)) throw new Error(`Duplicate tiny cluster id ${cluster.id}`); + ids.add(cluster.id); + } + return clusters; +} + +export function requireTinyCluster(id: string): TinyClusterDef { + const cluster = loadTinyClusterDefs().find((candidate) => candidate.id === id); + if (!cluster) throw new Error(`Unknown tiny cluster ${id}`); + return cluster; +} + +export function requireParent(cluster: TinyClusterDef, parentId: string): TinyClusterParent { + const parent = cluster.parents.find((candidate) => candidate.id === parentId); + if (!parent) throw new Error(`Cluster ${cluster.id} has no parent ${parentId}`); + return parent; +} + +export function parentPlanks(cluster: TinyClusterDef, parentId: string): SeedPlank[] { + const collection = loadSeedCollectionFile(cluster.collectionId); + return requireParent(cluster, parentId).naturals.map((ref) => plankFromCollection(collection, ref)); +} + +export function mediatorPlanks(cluster: TinyClusterDef): SeedPlank[] { + const collection = loadSeedCollectionFile(cluster.collectionId); + const refs = [...cluster.parents.flatMap((parent) => parent.modifieds), ...cluster.bridge.planks]; + return refs.map((ref) => plankFromCollection(collection, ref)); +} + +export function mediatorAddress(cluster: TinyClusterDef): `0x${string}` { + return fundedAddress(cluster.mediatorHardhatIndex); +} + +export function mediatorServiceUrl(cluster: TinyClusterDef): string { + const fallback = cluster.attachMediatorDefaultUrl ?? 'http://127.0.0.1:3011'; + const envName = cluster.attachMediatorServiceUrlEnv; + const raw = envName ? process.env[envName] : undefined; + return (raw ?? fallback).replace(/\/+$/, ''); +} + +export function parentRosterFields(cluster: TinyClusterDef, parent: TinyClusterParent, plankCids: string[]): SeedCauseRosterFields { + const mediator = { + name: cluster.mediatorName, + description: cluster.mediatorDescription ?? cluster.mediatorNote, + address: mediatorAddress(cluster), + serviceUrl: mediatorServiceUrl(cluster), + }; + return { + title: parent.title, + summary: parent.summary, + plankCids, + mediatorBlurb: parent.attachMediator ? `${mediator.name}: ${mediator.description}` : '', + ...(parent.attachMediator ? { mediator } : {}), + }; +} + +export function modifiedRosterFields(cluster: TinyClusterDef, parent: TinyClusterParent, plankCids: string[]): SeedCauseRosterFields { + return { + title: parent.modifiedTitle, + summary: parent.modifiedSummary, + plankCids, + mediatorBlurb: '', + bridgeCluster: { + clusterOwner: mediatorAddress(cluster), + clusterSlug: cluster.clusterSlug, + role: 'modified', + parentOwner: fundedAddress(parent.ownerHardhatIndex), + parentSlug: parent.slug, + }, + }; +} + +export function bridgeRosterFields(cluster: TinyClusterDef, plankCids: string[]): SeedCauseRosterFields { + return { + title: cluster.bridge.title, + summary: cluster.bridge.summary, + plankCids, + mediatorBlurb: '', + bridgeCluster: { + clusterOwner: mediatorAddress(cluster), + clusterSlug: cluster.clusterSlug, + role: 'bridge', + }, + }; +} + +export function clusterDocumentFields(cluster: TinyClusterDef, cids: Map): SeedBridgeClusterFields { + const owner = mediatorAddress(cluster).toLowerCase() as `0x${string}`; + const pairs = deriveImplications(cluster).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: cluster.mediatorName, + mediatorNote: cluster.mediatorNote, + mediatorAddress: owner, + parents: cluster.parents.map((parent) => ({ + owner: fundedAddress(parent.ownerHardhatIndex), + slug: parent.slug, + })), + modified: cluster.parents.map((parent) => ({ + owner, + slug: parent.modifiedSlug, + parentOwner: fundedAddress(parent.ownerHardhatIndex), + parentSlug: parent.slug, + })), + bridge: { owner, slug: cluster.bridge.slug }, + pairs, + }; +} + +export function clusterBookmarkEntries(cluster: TinyClusterDef): Array<{ owner: string; slug: string }> { + const mediator = mediatorAddress(cluster); + return [ + ...cluster.parents.map((parent) => ({ + owner: fundedAddress(parent.ownerHardhatIndex), + slug: parent.slug, + })), + ...cluster.parents.map((parent) => ({ + owner: mediator, + slug: parent.modifiedSlug, + })), + { owner: mediator, slug: cluster.bridge.slug }, + ]; +} + +export async function publishClusterStatements( + cluster: TinyClusterDef, + cids: Map, + publishOnChain: boolean, +): Promise { + const publishedData = CONTRACT_ADDRESSES.publishedData as `0x${string}` | undefined; + const ipfsConfig = createIPFSConfigInNodeJSFromTheUsualEnvVars(); + const collection = loadSeedCollectionFile(cluster.collectionId); + + const sets: Array<{ planks: SeedPlank[]; domain: string; key: `0x${string}` }> = cluster.parents.map((parent) => ({ + planks: parent.naturals.map((ref) => plankFromCollection(collection, ref)), + domain: parent.slug, + key: fundedKey(parent.ownerHardhatIndex), + })); + sets.push({ + planks: mediatorPlanks(cluster), + domain: cluster.collectionId, + key: fundedKey(cluster.mediatorHardhatIndex), + }); + + for (const set of sets) { + const owner = createClients(set.key); + for (const plank of set.planks) { + if (cids.has(plank.id)) continue; + const cid = await publishGeneratedStatement( + ipfsConfig, + { text: plank.text, domain: set.domain, position: plank.id }, + set.domain, + plank.id, + 'simple', + publishOnChain && publishedData + ? { clients: owner as WriteClients, publishedDataAddress: publishedData } + : {}, + ); + cids.set(plank.id, cid); + console.log(` ${publishOnChain ? 'Published' : 'Resolved'} ${set.domain} ${plank.id} → ${cid}`); + } + } +} + +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', slug); + return null; + } + const owner = createClients(publisherKey); + const store = createDefaultDocumentStore( + createSDKMachinery({ ipfsConfig: createIPFSConfigInNodeJSFromTheUsualEnvVars() }), + { + clients: owner as WriteClients, + publishedDataContract: { address: publishedData, abi: PublishedDataAbi }, + }, + ); + const publication = await store.publish(doc); + await updateRef( + owner as WriteClients, + { address: mutableRef, abi: MutableRefUpdaterAbi }, + slug, + publication.cid, + ); + return publication.cid; +} + +function cidsFor(ids: readonly string[], cids: Map): IpfsCidV1[] { + return ids.map((id) => cids.get(id)).filter((cid): cid is IpfsCidV1 => Boolean(cid)); +} + +async function fundPaymentToken(to: `0x${string}`, amount: bigint): Promise { + const token = process.env.PAYMENT_TOKEN_ADDRESS as `0x${string}` | undefined; + if (!token) throw new Error('PAYMENT_TOKEN_ADDRESS not configured'); + const funder = createClients(FUNDED_HARDHAT_DEV_KEYS[0]!); + try { + const hash = await funder.walletClient.writeContract({ + address: token, + abi: paymentTokenFundingAbi, + functionName: 'transfer', + args: [to, amount], + chain: funder.walletClient.chain, + account: funder.walletClient.account!, + }); + await funder.publicClient.waitForTransactionReceipt({ hash }); + } catch { + const hash = await funder.walletClient.writeContract({ + address: token, + abi: paymentTokenFundingAbi, + functionName: 'mintTo', + args: [to, amount], + chain: funder.walletClient.chain, + account: funder.walletClient.account!, + }); + await funder.publicClient.waitForTransactionReceipt({ hash }); + } +} + +async function publishNudges(cluster: TinyClusterDef, 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 nudges.'); + return; + } + const nudges = []; + for (const pair of deriveNudges(cluster)) { + 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: cluster.nudgeReason, + confidence: 0.9, + }); + } + if (nudges.length === 0) return; + + const mediator = createClients(fundedKey(cluster.mediatorHardhatIndex)); + const batchCid = await uploadToIPFS(createIPFSConfigInNodeJSFromTheUsualEnvVars(), { + kind: 'nudge-batch', + schemaVersion: 1, + nudger: mediator.account, + publishedAt: Math.floor(Date.now() / 1000), + nudges, + revocations: [], + }); + 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(` ✓ ${cluster.id} nudge batch (${nudges.length} parent→modified): ${batchCid}`); +} + +async function attestImplications(cluster: TinyClusterDef, 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 arrows.'); + return; + } + const clients = createClients(attesterKey); + let attested = 0; + for (const pair of deriveImplications(cluster)) { + 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(` ✓ ${cluster.id} replayed ${attested} modified→commonality implications`); +} + +async function signPlanks(cluster: TinyClusterDef, cids: Map): Promise { + const beliefs = CONTRACT_ADDRESSES.beliefs as `0x${string}` | undefined; + if (!beliefs) { + console.warn('Beliefs contract not configured — skipping signatures.'); + return; + } + for (const persona of cluster.personas) { + const key = fundedKey(persona.hardhatIndex); + const toSign = [...persona.signsNaturals]; + if (persona.takesModified) { + for (const naturalId of persona.signsNaturals) { + const modifiedId = modifiedIdForNatural(naturalId, persona.side); + if (modifiedId && cluster.parents.some((parent) => parent.modifieds.includes(modifiedId))) { + toSign.push(modifiedId); + } + } + } + const clients = createClients(key); + 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, + functionName: 'setBelief', + args: [cidToBytes32(cid), BELIEVES], + chain: clients.walletClient.chain, + account: clients.walletClient.account, + }); + await clients.publicClient.waitForTransactionReceipt({ hash }); + signed += 1; + } + console.log(` ✓ HH#${persona.hardhatIndex} (${persona.id}) signed ${signed} ${cluster.id} statements`); + } +} + +interface CreatedProject { + id: string; + name: string; + assuranceContract: `0x${string}`; + erc1155: `0x${string}`; + tokenIds: number[]; + prices: string[]; + alignments: string[]; +} + +async function createProjects(cluster: TinyClusterDef, 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; + if (!factory || !paymentToken) { + console.warn('ProjectFactory or payment token missing — skipping projects.'); + return []; + } + + const created: CreatedProject[] = []; + const latest = await createClients(FUNDED_HARDHAT_DEV_KEYS[0]!).publicClient.getBlock(); + const deadline = latest.timestamp + 30n * 24n * 60n * 60n; + const threshold = parsePaymentTokenUnits('2'); + const tokenIds = [1n, 2n, 3n]; + const maxSupplies = [100n, 500n, 1000n]; + const prices = [ + parsePaymentTokenUnits('0.1'), + parsePaymentTokenUnits('0.05'), + parsePaymentTokenUnits('0.01'), + ]; + + for (const template of cluster.projects) { + const key = fundedKey(template.ownerIndex); + const clients = createClients(key); + const store = createDefaultDocumentStore( + createSDKMachinery({ ipfsConfig: createIPFSConfigInNodeJSFromTheUsualEnvVars() }), + { + clients: clients as WriteClients, + ...(publishedData + ? { publishedDataContract: { address: publishedData, abi: PublishedDataAbi } } + : {}), + }, + ); + const alignedStatementRefs = template.alignments.map((alignment) => { + const { groupId, statementId } = parsePlankRef(alignment); + return { collectionId: cluster.collectionId, groupId, statementId }; + }); + const publication = await store.publish(createDisplayableDocument({ + format: 'markdown-restricted', + content: template.description, + extras: { + statementType: 'lazy-giving-project-metadata', + name: template.name, + description: template.description, + seedProjectKind: template.kind, + alignedStatementRefs, + }, + })); + const { projectDetails } = await sdkCreateProject( + clients as WriteClients, + { address: factory, abi: ProjectFactoryAbi }, + { + metadataURI: `ipfs://${publication.cid}/`, + contractURI: `ipfs://${publication.cid}`, + owner: clients.account, + recipient: clients.account, + paymentToken, + threshold, + deadline, + projectMetadataCid: publication.cid, + tokenIds, + tokenCounts: maxSupplies, + tokenPrices: prices, + }, + ); + created.push({ + id: template.id, + name: template.name, + assuranceContract: projectDetails.assuranceContractAddress, + erc1155: projectDetails.tokenAddress, + tokenIds: tokenIds.map(Number), + prices: prices.map((price) => price.toString()), + alignments: template.alignments, + }); + console.log(` ✓ Project ${template.name} → ${projectDetails.assuranceContractAddress}`); + + const alignment = CONTRACT_ADDRESSES.alignmentAttestations as `0x${string}` | undefined; + 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 = pickAlignmentAttester(cluster.personas, alignmentId, template.ownerIndex); + const attesterKey = attesterPersona + ? fundedKey(attesterPersona.hardhatIndex) + : fundedKey(template.ownerIndex); + 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; +} + +async function buyProjects(cluster: TinyClusterDef, projects: CreatedProject[]): Promise { + const paymentToken = process.env.PAYMENT_TOKEN_ADDRESS as `0x${string}` | undefined; + if (!paymentToken || !cluster.buys) return; + + for (const buy of cluster.buys) { + const project = projects.find((candidate) => candidate.id === buy.projectId); + const buyer = cluster.personas.find((persona) => persona.hardhatIndex === buy.accountIndex); + const key = FUNDED_HARDHAT_DEV_KEYS[buy.accountIndex]; + if (!project || !key || !buyer) continue; + const sides = new Set(project.alignments.map(sideOfAlignment)); + if (!sides.has(buyer.side) && sides.size === 1) { + console.warn(` Skipping buy: HH#${buy.accountIndex} (${buyer.side}) on unique ${project.name}`); + continue; + } + await fundPaymentToken(privateKeyToAccount(key).address, parsePaymentTokenUnits('2000')); + const clients = createClients(key); + const price = BigInt(project.prices[0]!); + try { + await buyProjectTokens( + clients as WriteClients, + { address: project.assuranceContract, abi: AssuranceContractAbi }, + { + buyer: clients.account, + tokenAddress: project.erc1155, + tokenIds: [BigInt(project.tokenIds[0]!)], + tokenCounts: [BigInt(buy.count)], + totalCost: price * BigInt(buy.count), + }, + ); + console.log(` ✓ HH#${buy.accountIndex} bought ${buy.count} on ${project.name}`); + } catch (error) { + console.warn(` Failed buy on ${project.name}:`, error instanceof Error ? error.message : error); + } + } +} + +export async function publishTinyClusterDocuments( + cluster: TinyClusterDef, + cids: Map, +): Promise<{ clusterCid: string | null; rosterCids: string[] }> { + console.log(`\n=== Publishing tiny cluster ${cluster.id} ===\n`); + const rosterCids: string[] = []; + const mediatorKey = fundedKey(cluster.mediatorHardhatIndex); + + for (const parent of cluster.parents) { + const parentCid = await publishDocumentAs( + fundedKey(parent.ownerHardhatIndex), + parent.slug, + buildSeedRosterDocument(parentRosterFields(cluster, parent, cidsFor(parent.naturals, cids))), + ); + if (parentCid) { + rosterCids.push(parentCid); + console.log(` ✓ Parent ${parent.slug} → ${parentCid}`); + } + const modifiedCid = await publishDocumentAs( + mediatorKey, + parent.modifiedSlug, + buildSeedRosterDocument(modifiedRosterFields(cluster, parent, cidsFor(parent.modifieds, cids))), + ); + if (modifiedCid) { + rosterCids.push(modifiedCid); + console.log(` ✓ Modified ${parent.modifiedSlug} → ${modifiedCid}`); + } + } + + const bridgeCid = await publishDocumentAs( + mediatorKey, + cluster.bridge.slug, + buildSeedRosterDocument(bridgeRosterFields(cluster, cidsFor(cluster.bridge.planks, cids))), + ); + if (bridgeCid) { + rosterCids.push(bridgeCid); + console.log(` ✓ Bridge ${cluster.bridge.slug} → ${bridgeCid}`); + } + + const fields = clusterDocumentFields(cluster, cids); + const expectedPairs = deriveImplications(cluster).length; + if (fields.pairs.length !== expectedPairs) { + console.warn(` Cluster has ${fields.pairs.length}/${expectedPairs} modified→CG pairs (missing CIDs).`); + } + const clusterCid = await publishDocumentAs( + mediatorKey, + cluster.clusterSlug, + buildSeedClusterDocument(fields), + ); + if (clusterCid) { + console.log( + ` ✓ Cluster ${cluster.clusterSlug} → ${clusterCid}\n Open /bridge/${mediatorAddress(cluster)}/${cluster.clusterSlug}`, + ); + } + return { clusterCid, rosterCids }; +} + +export async function publishTinyCluster( + cluster: TinyClusterDef, + cids: Map, + options: { activity?: boolean } = {}, +): Promise { + const activity = options.activity !== false; + if (activity) { + await publishNudges(cluster, cids); + await attestImplications(cluster, cids); + await signPlanks(cluster, cids); + const projects = await createProjects(cluster, cids); + await buyProjects(cluster, projects); + } + await publishTinyClusterDocuments(cluster, cids); +} + +export async function publishAllTinyClusters( + cids: Map, + options: { publishStatements?: boolean; activity?: boolean } = {}, +): Promise { + const clusters = loadTinyClusterDefs(); + const publishStatements = options.publishStatements !== false; + for (const cluster of clusters) { + if (publishStatements) { + await publishClusterStatements(cluster, cids, true); + } + await publishTinyCluster(cluster, cids, { activity: options.activity }); + } + return clusters; +} diff --git a/fake-data-generation/statement-generation-exercises/03-compromise-immigration.json b/fake-data-generation/statement-generation-exercises/03-compromise-immigration.json new file mode 100644 index 00000000..4c261cf3 --- /dev/null +++ b/fake-data-generation/statement-generation-exercises/03-compromise-immigration.json @@ -0,0 +1,62 @@ +{ + "format": "commonality-seed-content-v1", + "id": "statement-generation-exercise-03", + "title": "Exercise — left/right compromise in the middle (immigration)", + "description": "Gold copy of the accepted left/right enforcement-overlap triple using the canonical wording from docs/end-user/common-sense-majority/hidden-majority-patterns.md. The live copy is seed-content/compromise-immigration.json; this exercises directory is not loaded by loadSeedCollections. Do not fork a second immigration overlap zone: if wording must change, change all three copies together.", + "notes": [ + "Curriculum step 3 in statement-generation.md (real-gap bridges, one pattern). Abortion remains the canonical gestational-cutoff example; this is the next topic, not a second abortion wording.", + "Pattern: compromise in the middle. Overlap zone is: deport people here illegally who have committed other crimes; accept that some peaceful ones get deported and some do not.", + "Not the culture-vs-race misunderstanding cluster on the same page (different pattern; requires persuasion / civility content).", + "Loop: naturals as speech → modifieds as smallest extra belief → commonality last by omission of camp priority → attester + routing + /critique-triple." + ], + "groups": [ + { + "id": "immigration-criminal-priority-overlap", + "title": "Immigration — deport criminals; accept mixed outcomes for the peaceful", + "notes": [ + "Gap: moderate left's primary concern is leaving peaceful otherwise-law-abiding unauthorized people alone; moderate right's primary concern is that being here illegally is itself enough to deport, with criminals first. Neither natural states the mixed-outcome settlement.", + "Natural → modified is a nudge (extra deal). Modified → commonality should be implication (containment). Natural → commonality is designed no.", + "Sketch on hidden-majority-patterns.md: 'Deport the ones who've committed other crimes; I can accept that some peaceful ones get deported too, and I can accept that some peaceful ones don't.' Modifieds restate that in camp-specific prose; do not paste the commonality paragraph into both." + ], + "statements": [ + { + "id": "natural-left", + "role": "natural-left", + "text": "People who are here illegally but otherwise keep their heads down and work shouldn't be hunted down. Deport the ones who commit other crimes." + }, + { + "id": "natural-right", + "role": "natural-right", + "text": "If you're here illegally you should be deported. Start with the ones who've committed other crimes, then the rest." + }, + { + "id": "modified-left", + "role": "modified-left", + "text": "People who are here illegally but otherwise keep their heads down and work shouldn't be hunted down. I'd prefer we only deport people here illegally who've also committed other crimes, and leave the peaceful ones alone. I can live with some peaceful ones getting deported as a side effect of actually deporting the ones who've committed other crimes, and I already accept that some peaceful ones stay. I'd rather get this settled than keep fighting over it forever." + }, + { + "id": "modified-right", + "role": "modified-right", + "text": "If you're here illegally you should be deported. I'd still rather we deport the peaceful ones too, but I don't feel as strongly about them as about the ones who've committed other crimes. Deporting people who are here illegally and have committed other crimes, while accepting that some peaceful ones get deported and some don't, isn't what I'd write if I were making the law alone, but I'd be okay with that if it meant we got this settled instead of fighting over it forever." + }, + { + "id": "commonality", + "role": "commonality", + "text": "I'd be okay with it if we deport people who are here illegally and have committed other crimes, even though that means some peaceful ones get deported too and some peaceful ones don't. This isn't my ideal outcome, but I'd rather get this settled than keep fighting over it forever." + } + ], + "implicationNotes": [ + "Designed yes: modified-left → commonality; modified-right → commonality.", + "Designed no: natural-left → commonality; natural-right → commonality; natural-left → modified-left; natural-right → modified-right; either modified → the other modified; commonality → either modified.", + "Routing: modified → commonality should annoy as 'I already said that'. Natural → modified should not (the mixed-outcome settlement is extra)." + ], + "loopNotes": [ + "Human: pick pattern (done: compromise-in-the-middle, immigration, reuse the page sketch). Veto if gap is 'same conclusion, different metaphysics' — it is not; this is an overlap-zone deal.", + "Do not paste the commonality sentence into both modifieds to buy subset-by-concatenation.", + "2026-08-31 live attester (deepseek/deepseek-v3.2): both designed-yes modified → commonality arrows bless (high) and all eight designed-no arrows refuse (high). A later rewrite that pasted 'I'd be okay with a policy that…' into modified-left made commonality → modified bless (FAIL); reverted.", + "2026-08-31 /critique-triple (same model, cause-assist module, not Docker /critique-triple which 500'd): two objections that the strategy prompt already forbids treating as failures — (1) routing, claiming modifieds lack first-person settlement (they contain 'I can live with' / 'I'd be okay with that' and 'I'd rather get this settled'); (2) shape, calling the settlement sentence a coalition caption. Same class of pass-commentary as abortion before the routing contract was clarified. Not treated as a wording veto.", + "Adam accepted the triple on 2026-08-31; live copy: seed-content/compromise-immigration.json." + ] + } + ] +} diff --git a/fake-data-generation/statement-generation-exercises/04-crime-repeat-offenders.json b/fake-data-generation/statement-generation-exercises/04-crime-repeat-offenders.json new file mode 100644 index 00000000..df4672bc --- /dev/null +++ b/fake-data-generation/statement-generation-exercises/04-crime-repeat-offenders.json @@ -0,0 +1,63 @@ +{ + "format": "commonality-seed-content-v1", + "id": "statement-generation-exercise-04", + "title": "Exercise — left/right fact-conditional (crime / repeat offenders)", + "description": "Gold copy of the accepted left/right same-values-different-beliefs triple using the canonical wording from docs/end-user/common-sense-majority/hidden-majority-patterns.md. The live copy is seed-content/crime-repeat-offenders.json; this exercises directory is not loaded by loadSeedCollections. Do not fork a second repeat-offender concentration wording: if wording must change, change all three copies together.", + "notes": [ + "Curriculum step 3 in statement-generation.md (real-gap bridges, one pattern). Abortion and immigration remain the canonical compromise-in-the-middle examples.", + "Pattern: same values, different beliefs (fact-conditional). Shared value: dangerous career violent criminals should not be cycling through catch-and-release. Disputed fact: whether a small number of repeat offenders actually account for a disproportionate share of violent crime (right treats this as familiar; left often does not).", + "Proposed overlap policy given the fact: keep those people off the streets (e.g. three-strikes-style laws for violent offenses). Racism, rehabilitation, and the rest of the justice-system fight are deferred, not settled.", + "This is not the policing 'no major controversy' cluster, not the rehabilitation-works conditional, and not the racism-in-the-justice-system conditional — those remain sketches on hidden-majority-patterns.md.", + "Loop: naturals as speech → modifieds as smallest extra belief → commonality last by omission of camp *why-the-fact* → attester + routing + /critique-triple." + ], + "groups": [ + { + "id": "crime-repeat-offender-concentration", + "title": "Crime — if repeat offenders dominate violent crime, keep them off the streets", + "notes": [ + "Gap: moderate left's natural talk is against three-strikes / more incarceration, and does not grant the concentration fact. Moderate right's natural talk asserts the fact and the incapacitation policy as obvious. Neither natural states the *if-true-then-priority* deal that lets the left sign without conceding the fact.", + "Natural → modified is a nudge (left adds the conditional; right adds the conditional framing plus deferring racism/rehab). Modified → commonality should be implication (containment). Natural → commonality is designed no.", + "Do not paste the commonality paragraph into both modifieds." + ], + "statements": [ + { + "id": "natural-left", + "role": "natural-left", + "text": "I'm not signing up for three-strikes laws. A lot of people cycling through the system never got a fair shot, and locking more of them up doesn't fix poverty, addiction, or a stacked justice system." + }, + { + "id": "natural-right", + "role": "natural-right", + "text": "A handful of repeat offenders do most of the violent crime. Stop catch-and-release. Three strikes, you're out." + }, + { + "id": "modified-left", + "role": "modified-left", + "text": "I'm not signing up for three-strikes laws as a general program. I don't believe a small number of repeat offenders actually account for a disproportionate share of violent crime — poverty, addiction, and a stacked system explain more of it, as far as I can tell. But if it turned out that a relatively small number of career violent criminals really do a disproportionate amount of the violent crime, then of course we should focus on keeping those people off the streets, including with something like three-strikes for violent offenses. I still want to argue about racism and rehabilitation, just not as a reason to keep dangerous career criminals in the rotation if that's how the crime is concentrated; keeping them off the streets would be high priority in that case." + }, + { + "id": "modified-right", + "role": "modified-right", + "text": "A handful of repeat offenders do most of the violent crime. Stop catch-and-release. I'd still rather we used three-strikes on repeat burglary and car theft too, not just violent offenses, but I don't feel as strongly about those as about the career violent ones. I already think a relatively small number of repeat offenders do a disproportionate amount of the violent crime, so of course we should focus on keeping those people off the streets, including with three-strikes for violent offenses. If that concentration weren't real, I wouldn't treat incapacitation as the main lever. We can argue about racism and rehabilitation another time; keeping dangerous career criminals off the streets is high priority." + }, + { + "id": "commonality", + "role": "commonality", + "text": "If it's true that a relatively small number of repeat offenders do a disproportionate amount of the violent crime, we should focus on keeping those people off the streets, including with something like three-strikes laws for violent offenses. We can argue about racism and rehabilitation another time; keeping dangerous career criminals off the streets is high priority if that's how the crime is concentrated." + } + ], + "implicationNotes": [ + "Designed yes: modified-left → commonality; modified-right → commonality.", + "Designed no: natural-left → commonality; natural-right → commonality; natural-left → modified-left; natural-right → modified-right; either modified → the other modified; commonality → either modified.", + "Routing: modified → commonality should annoy as 'I already said that'. Natural → modified should not (the if-true-then-priority deal, and on the right the deferral of the rest of the fight, are extra)." + ], + "loopNotes": [ + "Human: pick pattern (done: same-values-different-beliefs, crime via repeat-offender concentration, not the rehab sketch). Veto if gap is 'same conclusion, different metaphysics' — it is not; the camps disagree on the fact.", + "Do not paste the commonality sentence into both modifieds to buy subset-by-concatenation.", + "2026-08-31 live attester (deepseek/deepseek-v3.2): both designed-yes modified → commonality arrows bless (high) and all eight designed-no arrows refuse (high). An earlier modified-right that only restated the conditional plus 'I already think that's true' made commonality → modified-right bless (FAIL); thickened with a camp extra (would still rather three-strikes on repeat burglary/car theft too, but feels less strongly than about career violent offenders).", + "2026-08-31 /critique-triple (same model, cause-assist module): three objections in the same class as immigration before the routing contract was treated as pass-commentary — (1) shape, calling 'we can argue about racism and rehabilitation another time' a coalition caption (it is a first-person deferral of the rest of the fight, which is part of the shared deal); (2) shape, multi-register (conditional plus that deferral); (3) routing, treating successful modified-right containment as a failure. Not treated as a wording veto.", + "Adam accepted the triple on 2026-08-31; live copy: seed-content/crime-repeat-offenders.json." + ] + } + ] +} diff --git a/fake-data-generation/statement-generation-exercises/05-lgbt-schools.json b/fake-data-generation/statement-generation-exercises/05-lgbt-schools.json new file mode 100644 index 00000000..cc508b0d --- /dev/null +++ b/fake-data-generation/statement-generation-exercises/05-lgbt-schools.json @@ -0,0 +1,62 @@ +{ + "format": "commonality-seed-content-v1", + "id": "statement-generation-exercise-05", + "title": "Exercise — left/right fact-conditional (LGBT / schools pushing)", + "description": "Gold copy of the accepted left/right same-values-different-beliefs triple. Canonical wording lives with hidden-majority-patterns.md and seed-content/lgbt-schools.json; this exercises directory is not loaded by loadSeedCollections. Commonality wording is Adam's 2026-08-31 clarification. Do not fork a second schools fact-conditional: if wording must change, change all three copies together.", + "notes": [ + "Curriculum step 3 in statement-generation.md (real-gap bridges, one pattern). Abortion and immigration remain the canonical compromise-in-the-middle examples; crime remains the canonical repeat-offender fact-conditional.", + "Pattern: same values, different beliefs (fact-conditional). Shared value: if schools are actually pushing kids toward LGBT identities, that is wrong. Disputed fact: whether schools are doing that (right treats it as happening; left treats 'support' as not a push).", + "This is not the Christian×secular LGBT unbundle in seed-content/christian-secular-bridge.json, and not the LGB-vs-T coalition-unbundling sketch on hidden-majority-patterns.md (that seam can be a later cluster).", + "Loop: naturals as speech → modifieds as smallest extra belief → commonality last by omission of camp *why-the-fact* / camp extras → attester + routing + /critique-triple." + ], + "groups": [ + { + "id": "lgbt-schools-pushing", + "title": "LGBT — if schools are pushing kids toward LGBT identities, that's wrong", + "notes": [ + "Gap: moderate left's natural talk denies the push (support is not a push; calling it one is a smear). Moderate right's natural talk asserts the push and condemns it as obvious. Neither natural states the *if-true-then-wrong* deal that lets the left sign without conceding the fact.", + "Natural → modified is a nudge (left adds the conditional; right adds the conditional framing plus a camp extra). Modified → commonality should be implication (containment). Natural → commonality is designed no.", + "Do not paste the commonality paragraph into both modifieds." + ], + "statements": [ + { + "id": "natural-left", + "role": "natural-left", + "text": "Schools aren't pushing kids toward LGBT identities. They're just being supportive of kids who already worked that out themselves. Calling that a push is a smear." + }, + { + "id": "natural-right", + "role": "natural-right", + "text": "Schools are actively pushing kids toward LGBT identities. That's wrong. Stop it." + }, + { + "id": "modified-left", + "role": "modified-left", + "text": "I don't think schools are pushing kids toward LGBT identities. They're being supportive of kids who already worked it out themselves, and calling that a push is a smear. But if it turned out schools really are pushing kids in that direction, that's wrong — of course they shouldn't be doing it. I still want to argue about what counts as support versus a push; I just wouldn't defend an actual push if that's what's happening." + }, + { + "id": "modified-right", + "role": "modified-right", + "text": "Schools are actively pushing kids toward LGBT identities. That's wrong. I'd still rather we also stopped social transition at school without parents even in cases that look like 'just support,' but I don't feel as strongly about borderline support as about an actual push. I already think schools are pushing, so of course that's wrong. If they really weren't pushing, I wouldn't treat this as the same fight. We can argue about curriculum details another time; if schools are pushing kids toward LGBT identities, that's wrong." + }, + { + "id": "commonality", + "role": "commonality", + "text": "If schools really are pushing kids toward LGBT identities, that's wrong. We can argue about what counts as 'supporting kids who are LGBT' versus 'pushing kids towards LGBT identities' another time, but to the extent that the latter is what's happening, that is not okay." + } + ], + "implicationNotes": [ + "Designed yes: modified-left → commonality; modified-right → commonality.", + "Designed no: natural-left → commonality; natural-right → commonality; natural-left → modified-left; natural-right → modified-right; either modified → the other modified; commonality → either modified.", + "Routing: modified → commonality should annoy as 'I already said that'. Natural → modified should not (the if-true-then-wrong deal, and on the right the parent-bypass social-transition extra, are extra)." + ], + "loopNotes": [ + "Human: pick pattern (done: same-values-different-beliefs, LGBT via schools pushing, not the Christian×secular unbundle, not LGB-vs-T unbundling). Veto if gap is 'same conclusion, different metaphysics' — it is not; the camps disagree on the fact.", + "Do not paste the commonality sentence into both modifieds to buy subset-by-concatenation.", + "2026-08-31 live attester (deepseek/deepseek-v3.2): both designed-yes modified → commonality arrows bless (high) and all eight designed-no arrows refuse (high), including after Adam's commonality reword.", + "2026-08-31 /critique-triple (same model, cause-assist module, pre-reword commonality): three objections in the same class as crime/immigration — (1) routing, treating modified-left's hypothetical as not containing the conditional; (2) routing, treating modified-right's 'I already think that's true, so of course that's wrong' as not containing the conditional; (3) shape, calling the deferral a coalition caption. Live attester already blessed both designed-yes arrows; not treated as a wording veto.", + "Adam accepted the triple on 2026-08-31; live copy: seed-content/lgbt-schools.json." + ] + } + ] +} diff --git a/fake-data-generation/statement-generation-exercises/README.md b/fake-data-generation/statement-generation-exercises/README.md index 1c919d88..12f2cb21 100644 --- a/fake-data-generation/statement-generation-exercises/README.md +++ b/fake-data-generation/statement-generation-exercises/README.md @@ -7,4 +7,8 @@ Draft corpora for the loop in [`../statement-generation.md`](../statement-genera | File | Exercise | |---|---| | [`01-simple-causes.json`](./01-simple-causes.json) | OSS + local food. Gold-set texts. Live copy: [`../seed-content/simple-causes.json`](../seed-content/simple-causes.json). | -| [`02-compromise-abortion.json`](./02-compromise-abortion.json) | One left/right compromise-in-the-middle triple. Canonical wording from hidden-majority-patterns.md. Awaiting human accept + attester/critique. | +| [`02-compromise-abortion.json`](./02-compromise-abortion.json) | Left/right abortion compromise. **Accepted** into [`../seed-content/compromise-abortion.json`](../seed-content/compromise-abortion.json) (2026-08-30). Gold copy stays here. | +| [`03-compromise-immigration.json`](./03-compromise-immigration.json) | Left/right immigration compromise. **Accepted** into [`../seed-content/compromise-immigration.json`](../seed-content/compromise-immigration.json) (2026-08-31). Gold copy stays here. | +| [`04-crime-repeat-offenders.json`](./04-crime-repeat-offenders.json) | Left/right crime fact-conditional (repeat-offender concentration). **Accepted** into [`../seed-content/crime-repeat-offenders.json`](../seed-content/crime-repeat-offenders.json) (2026-08-31). Gold copy stays here. | +| [`05-lgbt-schools.json`](./05-lgbt-schools.json) | Left/right LGBT schools fact-conditional. **Accepted** into [`../seed-content/lgbt-schools.json`](../seed-content/lgbt-schools.json) (2026-08-31). Gold copy stays here. | +| Next (not drafted) | Left/right **LGB-vs-T coalition unbundling** if Adam still wants that seam after accepting (or vetoing) the schools cluster. See [`../PLAN.md`](../PLAN.md). | diff --git a/fake-data-generation/statement-generation.md b/fake-data-generation/statement-generation.md index 80442f67..e2be385e 100644 --- a/fake-data-generation/statement-generation.md +++ b/fake-data-generation/statement-generation.md @@ -41,10 +41,12 @@ Generate in this order. Do not skip ahead to mass triples. 2. **Easy implication inside one camp.** Close / medium / distant variants (`gen:proliferation`). Locks the attester, not bridges. 3. **Real-gap bridges, one hidden-majority pattern at a time.** Compromise in - the middle (canonical left/right abortion or immigration — reuse + the middle (canonical left/right abortion and immigration are accepted, plus crime and LGBT-schools fact-conditionals; + remaining optional: LGB-vs-T unbundling — reuse [hidden-majority-patterns](/docs/end-user/common-sense-majority/hidden-majority-patterns.md), - do not fork a second abortion wording); costly unbundling; fact-conditionals; + do not fork a second wording of an accepted topic); costly unbundling; fact-conditionals; different-problems-same-solution with first-person limits, not mediator-meta. + Remaining work: [`PLAN.md`](./PLAN.md). 4. **Non-political public-goods bridges** only after 1–3 work. Copyleft vs permissive; replication vs novel discovery; privacy vs open data. @@ -153,14 +155,20 @@ attester + routing + “read aloud as a signature” without prose wordsmithing. ## Wiring to production Mass seed and user-facing cause-assist must share this pipeline so production -cannot drift from what we bless in seed. Next engineering (not this exercise): -refuse to show `/atomize` / `/sharpen-plank` / `/critique-triple` output that -failed the same checks. +cannot drift from what we bless in seed. **Gated (2026-08-31):** `/atomize` +drops taxonomy / pay-the-work / slogan / heuristic-safety planks; +`/sharpen-plank` withholds a reword that fails those checks; `/critique-triple` +adds live-attester refusals on modified→bridge as `routing:` objections. See +`cause-assist/src/statementQualityGate.ts`. ## Exercises | # | Status | What | |---|---|---| | 1 | **In `seed-content/simple-causes.json`**. Gold set still in the exercises file. List not complete. Nested-place rollup is board inclusion (settled). | Simple causes: wants, earmark grain (kind + place). Ontario-wide planks are genuine wants, not implication parents. `npm run gen:seed:simple-causes-implications`. | -| 2 | **In [`seed-content/compromise-abortion.json`](./seed-content/compromise-abortion.json)** after Adam accepted it on 2026-08-30. Gold copy remains in the exercises file; canonical wording remains on hidden-majority-patterns.md. Live check (2026-08-28, deepseek-v3.2): both designed-yes arrows bless and all eight designed-no arrows refuse with high confidence; `/critique-triple` returns no objections or leak warnings. Production-default v4-flash stalled rather than returning JSON. | One left/right abortion compromise-in-the-middle triple. Do not fork wording. | -| 3 | Not started | Gate cause-assist suggestions on the same checks. | +| 2 | **In [`seed-content/compromise-abortion.json`](./seed-content/compromise-abortion.json)** after Adam accepted it on 2026-08-30. Gold copy remains in the exercises file; canonical wording remains on hidden-majority-patterns.md. Live check (2026-08-28, deepseek-v3.2): both designed-yes arrows bless and all eight designed-no arrows refuse with high confidence; `/critique-triple` returns no objections or leak warnings. Production-default v4-flash stalled rather than returning JSON. | One left/right abortion compromise-in-the-middle triple. Do not fork wording. Not yet the featured tiny-seed cluster. | +| 3 | Done 2026-08-31 | Gate cause-assist suggestions on the same checks (`statementQualityGate.ts`). | +| 4 | **In [`seed-content/compromise-immigration.json`](./seed-content/compromise-immigration.json)** after Adam accepted it on 2026-08-31. Gold copy remains in the exercises file; canonical wording remains on hidden-majority-patterns.md. Live attester (deepseek-v3.2): both designed-yes modified → commonality arrows bless and all eight designed-no arrows refuse with high confidence. | Left/right immigration compromise-in-the-middle triple. Do not fork wording. Not a tiny-seed cluster. | +| 5 | **In [`seed-content/crime-repeat-offenders.json`](./seed-content/crime-repeat-offenders.json)** after Adam accepted it on 2026-08-31. Gold copy remains in the exercises file; canonical wording remains on hidden-majority-patterns.md. Live attester (deepseek-v3.2): both designed-yes modified → commonality arrows bless and all eight designed-no arrows refuse with high confidence. | Left/right crime fact-conditional (repeat-offender concentration). Do not fork wording. Not a tiny-seed cluster. | +| 6 | **In [`seed-content/lgbt-schools.json`](./seed-content/lgbt-schools.json)** after Adam accepted it on 2026-08-31 (commonality wording is his clarification). Gold copy remains in the exercises file; canonical wording remains on hidden-majority-patterns.md. Live attester after the reword (deepseek-v3.2): both designed-yes arrows bless and all eight designed-no arrows refuse with high confidence. | Left/right LGBT schools fact-conditional. Not the Christian×secular unbundle. Do not fork wording. Not a tiny-seed cluster. | +| 7+ | Not started | Optional later: LGB-vs-T coalition unbundling as its own exercise file. | diff --git a/fake-data-generation/test/seedMetadata.test.ts b/fake-data-generation/test/seedMetadata.test.ts index 45301837..e5025fec 100644 --- a/fake-data-generation/test/seedMetadata.test.ts +++ b/fake-data-generation/test/seedMetadata.test.ts @@ -48,11 +48,22 @@ import { christianSecularClusterFields, campOfAlignment, pickAlignmentAttester, -} from '../seedChristianityCause.js'; -import { BLESSED_MODIFIED_TO_COMMONALITY, NATURAL_TO_MODIFIED_NUDGES, -} from '../christianSecularBridge.js'; +} from '../seedChristianityCause.js'; +import { + clusterDocumentFields, + deriveImplications, + deriveNudges, + fundedAddress, + loadTinyClusterDefs, + modifiedRosterFields, + parentRosterFields, + pickAlignmentAttester as pickTinyAlignmentAttester, + requireParent, + requireTinyCluster, + sideOfAlignment, +} from '../seedTinyCluster.js'; import { seedChristianContentAlignmentCanonicalIds } from '../contentFundingActions.js'; import { createStatementDocumentFromSeed, flattenSeedStatements, loadSeedCollections } from '../seed-content-format.js'; @@ -175,6 +186,54 @@ test('simple-causes seed collection copies the accepted exercise-1 plank texts', assert.equal(marketsOntario?.statement.role, 'unique'); }); +test('compromise-immigration seed collection copies the accepted exercise-3 statement texts', async () => { + const exercise = JSON.parse(await readFile( + new URL('../statement-generation-exercises/03-compromise-immigration.json', import.meta.url), + 'utf8', + )) as { groups: Array<{ statements: Array<{ id: string; role: string; text: string }> }> }; + const accepted = exercise.groups[0]?.statements ?? []; + const live = flattenSeedStatements(await loadSeedCollections()) + .filter((record) => record.collection.id === 'compromise-immigration') + .map((record) => record.statement); + + assert.deepEqual( + live.map(({ id, role, text }) => ({ id, role, text })), + accepted.map(({ id, role, text }) => ({ id, role, text })), + ); +}); + +test('crime-repeat-offenders seed collection copies the accepted exercise-4 statement texts', async () => { + const exercise = JSON.parse(await readFile( + new URL('../statement-generation-exercises/04-crime-repeat-offenders.json', import.meta.url), + 'utf8', + )) as { groups: Array<{ statements: Array<{ id: string; role: string; text: string }> }> }; + const accepted = exercise.groups[0]?.statements ?? []; + const live = flattenSeedStatements(await loadSeedCollections()) + .filter((record) => record.collection.id === 'crime-repeat-offenders') + .map((record) => record.statement); + + assert.deepEqual( + live.map(({ id, role, text }) => ({ id, role, text })), + accepted.map(({ id, role, text }) => ({ id, role, text })), + ); +}); + +test('lgbt-schools seed collection copies the accepted exercise-5 statement texts', async () => { + const exercise = JSON.parse(await readFile( + new URL('../statement-generation-exercises/05-lgbt-schools.json', import.meta.url), + 'utf8', + )) as { groups: Array<{ statements: Array<{ id: string; role: string; text: string }> }> }; + const accepted = exercise.groups[0]?.statements ?? []; + const live = flattenSeedStatements(await loadSeedCollections()) + .filter((record) => record.collection.id === 'lgbt-schools') + .map((record) => record.statement); + + assert.deepEqual( + live.map(({ id, role, text }) => ({ id, role, text })), + accepted.map(({ id, role, text }) => ({ id, role, text })), + ); +}); + test('compromise-abortion seed collection copies the accepted exercise-2 statement texts', async () => { const exercise = JSON.parse(await readFile( new URL('../statement-generation-exercises/02-compromise-abortion.json', import.meta.url), @@ -314,9 +373,9 @@ test('christian-secular seed cluster documents match CauseStarter extras', () => 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 }, + { id: 'christian-organizer', hardhatIndex: 0, side: 'christian', takesModified: false, signsNaturals: [], aligns: true }, + { id: 'secular-nudge-taker', hardhatIndex: 5, side: 'secular', takesModified: true, signsNaturals: [], aligns: true }, + { id: 'secular-natural-only', hardhatIndex: 6, side: 'secular', takesModified: false, signsNaturals: [], aligns: true }, ]; assert.equal(campOfAlignment('scripture/natural-christian'), 'christian'); assert.equal(campOfAlignment('colorblind-merit/natural-secular'), 'secular'); @@ -337,3 +396,70 @@ test('christian-secular bridge has parent→modified nudges and blessed modified assert.match(pair.to, /\/commonality$/); } }); + +test('compromise-abortion tiny cluster is a second real-gap bridge, not a second Christianity', () => { + const def = requireTinyCluster('compromise-abortion'); + const left = requireParent(def, 'left'); + const right = requireParent(def, 'right'); + assert.equal(def.projects.length, 2); + assert.equal(left.slug, 'abortion-left'); + assert.equal(right.slug, 'abortion-right'); + assert.equal(def.clusterSlug, 'compromise-abortion'); + assert.equal(left.modifiedSlug.length <= 64, true); + assert.equal(right.modifiedSlug.length <= 64, true); + assert.notEqual(fundedAddress(left.ownerHardhatIndex), fundedAddress(right.ownerHardhatIndex)); + + const leftModified = modifiedRosterFields(def, left, ['bafyml']); + assert.equal(leftModified.bridgeCluster?.role, 'modified'); + assert.equal(leftModified.bridgeCluster?.parentSlug, left.slug); + assert.equal(leftModified.bridgeCluster?.clusterSlug, def.clusterSlug); + + const rightModified = modifiedRosterFields(def, right, ['bafymr']); + assert.equal(rightModified.bridgeCluster?.parentSlug, right.slug); + + const bridge = parentRosterFields(def, left, ['bafycg']); + assert.equal(def.bridge.slug, 'compromise-abortion-bridge'); + assert.equal(bridge.bridgeCluster?.role, undefined); + + const cids = new Map([ + ['abortion-gestational-cutoff/modified-left', 'bafyml'], + ['abortion-gestational-cutoff/modified-right', 'bafymr'], + ['abortion-gestational-cutoff/commonality', 'bafycg'], + ]); + const cluster = clusterDocumentFields(def, cids); + assert.equal(cluster.pairs.length, 2); + assert.ok(cluster.pairs.every((pair) => pair.role === 'modified-to-bridge')); + const clusterDoc = buildSeedClusterDocument(cluster); + assert.equal(clusterDoc.extras?.kind, BRIDGE_CLUSTER_KIND); + assert.match(clusterDoc.content, /Natural parents/); + + assert.equal(deriveNudges(def).length, 2); + assert.equal(deriveImplications(def).length, 2); +}); + +test('compromise-abortion alignment attesters follow left/right poles', () => { + const personas = [ + { id: 'left-nudge-taker', hardhatIndex: 3, side: 'left', takesModified: true, signsNaturals: [], aligns: true }, + { id: 'right-nudge-taker', hardhatIndex: 6, side: 'right', takesModified: true, signsNaturals: [], aligns: true }, + ]; + assert.equal(sideOfAlignment('abortion-gestational-cutoff/modified-left'), 'left'); + assert.equal(sideOfAlignment('abortion-gestational-cutoff/modified-right'), 'right'); + assert.equal( + pickTinyAlignmentAttester(personas, 'abortion-gestational-cutoff/modified-left', 3)?.id, + 'left-nudge-taker', + ); + assert.equal( + pickTinyAlignmentAttester(personas, 'abortion-gestational-cutoff/modified-right', 6)?.id, + 'right-nudge-taker', + ); +}); + +test('tiny clusters load from JSON without issue-specific TypeScript modules', () => { + const ids = loadTinyClusterDefs().map((cluster) => cluster.id).sort(); + assert.deepEqual(ids, ['christian-secular', 'compromise-abortion']); + for (const cluster of loadTinyClusterDefs()) { + assert.equal(cluster.format, 'commonality-tiny-cluster-v1'); + assert.ok(deriveImplications(cluster).length > 0); + assert.ok(cluster.parents.every((parent) => parent.naturals.length > 0)); + } +}); diff --git a/inbox.md b/inbox.md index 1b8c0e2b..0e942ac7 100644 --- a/inbox.md +++ b/inbox.md @@ -21,7 +21,11 @@ Also, don't let any of the items get too long; usually there's a separate .md fi - **(Tell)** Production OpenRouter services (attesters, service-host, cause-assist, coherence-badge-worker) now default to `deepseek/deepseek-v4-flash-0731` via `PRODUCTION_OPENROUTER_MODEL`. Laptop scripts use the same id through a separate `DEV_OPENROUTER_MODEL` env / `fake-data-generation/devOpenRouter.ts`. Cause-assist prefers OpenRouter over xAI when both keys exist. Update Render dashboard if those env vars were set by hand. -- **(Tell)** Statement-generation exercise 2: first attester pass refused modified-right → commonality (no cutoff). Thickened modified-right on [hidden-majority-patterns.md](docs/end-user/common-sense-majority/hidden-majority-patterns.md) (also bridge-creator + exercise JSON). Re-run: both modifieds → commonality yes/high; both naturals → commonality no/high. Still not in `seed-content/`; `/critique-triple` not run. +- **(Tell)** Crime left/right triple accepted into [`fake-data-generation/seed-content/crime-repeat-offenders.json`](fake-data-generation/seed-content/crime-repeat-offenders.json) (2026-08-31). Not a tiny-seed cluster. + +- **(Tell)** LGBT left/right **schools** fact-conditional accepted into [`fake-data-generation/seed-content/lgbt-schools.json`](fake-data-generation/seed-content/lgbt-schools.json) (2026-08-31). Adam's commonality reword still attester-clean. Not a tiny-seed cluster. LGB-vs-T unbundling not drafted. + +- **(Tell)** Fake/seed data now has a standing plan: [`fake-data-generation/PLAN.md`](fake-data-generation/PLAN.md) (tiny UI world vs real statements vs stress traffic). Adam asked for more left/right bridges on LGBT, immigration, crime — all four (plus abortion) are accepted. - **(Tell)** Nested-place rollup is settled as board inclusion, not implication. Statement-generation gold set, cause-assist guidance, seed garden/roster, and the implication attester prompt now follow that (Grey County → Ontario is a worked reject). `seed-implication-evaluations` still has the old prompt fingerprint; a v4-flash refresh stalled on empty completions. Handoff: [continuity/2026-08-27-statement-generation.md](continuity/2026-08-27-statement-generation.md). diff --git a/specs/tech/subsystems/conceptspace/seed-content/README.md b/specs/tech/subsystems/conceptspace/seed-content/README.md index c552a30f..0b88087d 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. +**This is job C: real Conceptspace statements** (findable causes for early users). It is not the tiny fake UI world, not demo worker fixtures, and not mass random user activity. Those other jobs, current state, and the next LLM step: [`fake-data-generation/PLAN.md`](/fake-data-generation/PLAN.md). + Wording is not free-form slogans: see [why statements are peculiar](/specs/product/statements-are-peculiar-for-good-reasons.md). How to generate more of them without hand-wordsmithing: [statement-generation.md](/fake-data-generation/statement-generation.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. @@ -49,7 +51,7 @@ It may also help to have high-level statements like "I care about education" tha The showcase statements demonstrating the system's ability to find consensus (see [hidden-majority.md](./hidden-majority.md)). Each includes pole positions, moderate positions, and a commonality statement. -The accepted [abortion compromise-in-the-middle triple](./compromise-abortion.md) demonstrates the newer natural → modified nudge and modified → commonality implication shape with a specific 12–16 week settlement. +The accepted [abortion](./compromise-abortion.md) and [immigration](./compromise-immigration.md) compromise-in-the-middle triples, and the [crime / repeat-offender](./crime-repeat-offenders.md) and [LGBT / schools](./lgbt-schools.md) fact-conditional triples, demonstrate the newer natural → modified nudge and modified → commonality implication shape. ### Cross-cutting meta-statements @@ -78,8 +80,14 @@ The fake-data system in `universe.json` uses a different set of statements optim ## Relationship to fake-data-generation -The fake-data system uses statements optimized for testing system mechanics (spectrum positions, randomized signing, etc.). It's simulation data — short and generic. +Do not collapse these: + +| Kind | Purpose | +|---|---| +| Tiny / demo **on-chain seed** (`data.sh --seed`) | Fake users, projects, signs, a few statements so local UI and tests have shape | +| **This catalog** (`seed-content/*.json`) | Real (or real-shaped) statements for discovery; no requirement to invent users or projects | +| **Simulation** (`gen:medium` / `gen:large`) | Random actions for stress; statement text can be generic | -The seed content here is different: curated for real early users, focused on areas where fundable projects plausibly exist, and written to demonstrate the system's coalition-building power. +The simulation historically used short generic `universe.json` templates. Formal seed-content JSON can be converted into that shape (`gen:seed:universe`) so mechanics tests can reuse realistic texts without mixing the jobs. Tiny still does **not** publish a random universe slice. -After the real system launches, the fake-data system can be updated to use these statements (or a superset) for more realistic simulations. +Living plan: [`fake-data-generation/PLAN.md`](/fake-data-generation/PLAN.md). diff --git a/specs/tech/subsystems/conceptspace/seed-content/compromise-abortion.md b/specs/tech/subsystems/conceptspace/seed-content/compromise-abortion.md index 7560b3ea..0a47d9a7 100644 --- a/specs/tech/subsystems/conceptspace/seed-content/compromise-abortion.md +++ b/specs/tech/subsystems/conceptspace/seed-content/compromise-abortion.md @@ -25,3 +25,4 @@ Statements Expected implication links - Expect yes: modified-left → commonality; modified-right → commonality. - Expect no: natural-left → commonality; natural-right → commonality; natural-left → modified-left; natural-right → modified-right; either modified → the other modified; commonality → either modified. + diff --git a/specs/tech/subsystems/conceptspace/seed-content/compromise-immigration.md b/specs/tech/subsystems/conceptspace/seed-content/compromise-immigration.md new file mode 100644 index 00000000..330bb4a9 --- /dev/null +++ b/specs/tech/subsystems/conceptspace/seed-content/compromise-immigration.md @@ -0,0 +1,29 @@ +# Immigration — compromise in the middle + +> Auto-generated from [`../../../../../fake-data-generation/seed-content/compromise-immigration.json`](../../../../../fake-data-generation/seed-content/compromise-immigration.json). Do not edit this file by hand; edit the JSON source instead. + +Accepted left/right enforcement-overlap bridge triple. Copied from statement-generation-exercises/03-compromise-immigration.json after Adam accepted it on 2026-08-31. + +Collection notes +- Curriculum step 3: a real-gap compromise-in-the-middle bridge. Process: fake-data-generation/statement-generation.md. +- Naturals state each camp's concern without the deal. Modifieds reaffirm that concern and add the smallest mixed-outcome settlement each camp can sign. Commonality contains only the shared settlement. +- Not the culture-vs-race misunderstanding cluster on hidden-majority-patterns.md. +- Live attester on 2026-08-31 with deepseek/deepseek-v3.2: both designed-yes arrows blessed and all eight designed-no arrows refused with high confidence. + +--- + +## Immigration — deport criminals; accept mixed outcomes for the peaceful +Note: Gap: the moderate left's primary concern is leaving peaceful otherwise-law-abiding unauthorized people alone; the moderate right's primary concern is that being here illegally is itself enough to deport, with criminals first. Neither natural states a willingness to settle. +Note: Modified → commonality is implication containment. Natural → modified remains a nudge because the mixed-outcome settlement is extra beliefs. + +Statements +- **natural-left:** "People who are here illegally but otherwise keep their heads down and work shouldn't be hunted down. Deport the ones who commit other crimes." +- **natural-right:** "If you're here illegally you should be deported. Start with the ones who've committed other crimes, then the rest." +- **modified-left:** "People who are here illegally but otherwise keep their heads down and work shouldn't be hunted down. I'd prefer we only deport people here illegally who've also committed other crimes, and leave the peaceful ones alone. I can live with some peaceful ones getting deported as a side effect of actually deporting the ones who've committed other crimes, and I already accept that some peaceful ones stay. I'd rather get this settled than keep fighting over it forever." +- **modified-right:** "If you're here illegally you should be deported. I'd still rather we deport the peaceful ones too, but I don't feel as strongly about them as about the ones who've committed other crimes. Deporting people who are here illegally and have committed other crimes, while accepting that some peaceful ones get deported and some don't, isn't what I'd write if I were making the law alone, but I'd be okay with that if it meant we got this settled instead of fighting over it forever." +- **commonality:** "I'd be okay with it if we deport people who are here illegally and have committed other crimes, even though that means some peaceful ones get deported too and some peaceful ones don't. This isn't my ideal outcome, but I'd rather get this settled than keep fighting over it forever." + +Expected implication links +- Expect yes: modified-left → commonality; modified-right → commonality. +- Expect no: natural-left → commonality; natural-right → commonality; natural-left → modified-left; natural-right → modified-right; either modified → the other modified; commonality → either modified. + diff --git a/specs/tech/subsystems/conceptspace/seed-content/crime-repeat-offenders.md b/specs/tech/subsystems/conceptspace/seed-content/crime-repeat-offenders.md new file mode 100644 index 00000000..bdef4be3 --- /dev/null +++ b/specs/tech/subsystems/conceptspace/seed-content/crime-repeat-offenders.md @@ -0,0 +1,29 @@ +# Crime — repeat-offender concentration (fact-conditional) + +> Auto-generated from [`../../../../../fake-data-generation/seed-content/crime-repeat-offenders.json`](../../../../../fake-data-generation/seed-content/crime-repeat-offenders.json). Do not edit this file by hand; edit the JSON source instead. + +Accepted left/right same-values-different-beliefs bridge triple. Copied from statement-generation-exercises/04-crime-repeat-offenders.json after Adam accepted it on 2026-08-31. + +Collection notes +- Curriculum step 3: a real-gap same-values-different-beliefs bridge. Process: fake-data-generation/statement-generation.md. +- Naturals state each camp's talk without the if-true-then-priority deal. Modifieds add that deal in camp-specific prose. Commonality is only the shared conditional plus deferring the rest of the justice-system fight. +- Not the rehabilitation-works sketch, not policing 'no major controversy', and not the racism-in-the-justice-system conditional on hidden-majority-patterns.md. +- Live attester on 2026-08-31 with deepseek/deepseek-v3.2: both designed-yes arrows blessed and all eight designed-no arrows refused with high confidence. + +--- + +## Crime — if repeat offenders dominate violent crime, keep them off the streets +Note: Gap: the moderate left's natural talk is against three-strikes / more incarceration and does not grant the concentration fact. The moderate right's natural talk asserts the fact and the incapacitation policy as obvious. Neither natural states the if-true-then-priority deal that lets the left sign without conceding the fact. +Note: Modified → commonality is implication containment. Natural → modified remains a nudge because the conditional deal (and on the right, a camp extra plus deferring racism/rehab) is extra beliefs. + +Statements +- **natural-left:** "I'm not signing up for three-strikes laws. A lot of people cycling through the system never got a fair shot, and locking more of them up doesn't fix poverty, addiction, or a stacked justice system." +- **natural-right:** "A handful of repeat offenders do most of the violent crime. Stop catch-and-release. Three strikes, you're out." +- **modified-left:** "I'm not signing up for three-strikes laws as a general program. I don't believe a small number of repeat offenders actually account for a disproportionate share of violent crime — poverty, addiction, and a stacked system explain more of it, as far as I can tell. But if it turned out that a relatively small number of career violent criminals really do a disproportionate amount of the violent crime, then of course we should focus on keeping those people off the streets, including with something like three-strikes for violent offenses. I still want to argue about racism and rehabilitation, just not as a reason to keep dangerous career criminals in the rotation if that's how the crime is concentrated; keeping them off the streets would be high priority in that case." +- **modified-right:** "A handful of repeat offenders do most of the violent crime. Stop catch-and-release. I'd still rather we used three-strikes on repeat burglary and car theft too, not just violent offenses, but I don't feel as strongly about those as about the career violent ones. I already think a relatively small number of repeat offenders do a disproportionate amount of the violent crime, so of course we should focus on keeping those people off the streets, including with three-strikes for violent offenses. If that concentration weren't real, I wouldn't treat incapacitation as the main lever. We can argue about racism and rehabilitation another time; keeping dangerous career criminals off the streets is high priority." +- **commonality:** "If it's true that a relatively small number of repeat offenders do a disproportionate amount of the violent crime, we should focus on keeping those people off the streets, including with something like three-strikes laws for violent offenses. We can argue about racism and rehabilitation another time; keeping dangerous career criminals off the streets is high priority if that's how the crime is concentrated." + +Expected implication links +- Expect yes: modified-left → commonality; modified-right → commonality. +- Expect no: natural-left → commonality; natural-right → commonality; natural-left → modified-left; natural-right → modified-right; either modified → the other modified; commonality → either modified. + diff --git a/specs/tech/subsystems/conceptspace/seed-content/lgbt-schools.md b/specs/tech/subsystems/conceptspace/seed-content/lgbt-schools.md new file mode 100644 index 00000000..743dcfc4 --- /dev/null +++ b/specs/tech/subsystems/conceptspace/seed-content/lgbt-schools.md @@ -0,0 +1,29 @@ +# LGBT — schools pushing (fact-conditional) + +> Auto-generated from [`../../../../../fake-data-generation/seed-content/lgbt-schools.json`](../../../../../fake-data-generation/seed-content/lgbt-schools.json). Do not edit this file by hand; edit the JSON source instead. + +Accepted left/right same-values-different-beliefs bridge triple. Copied from statement-generation-exercises/05-lgbt-schools.json after Adam accepted it on 2026-08-31 (commonality wording is Adam's clarification). + +Collection notes +- Curriculum step 3: a real-gap same-values-different-beliefs bridge. Process: fake-data-generation/statement-generation.md. +- Naturals state each camp's talk without the if-true-then-wrong deal. Modifieds add that deal in camp-specific prose. Commonality is only the shared conditional plus deferring support-versus-push. +- Not the Christian×secular LGBT unbundle, not LGB-vs-T coalition unbundling. +- Live attester on 2026-08-31 with deepseek/deepseek-v3.2, including after Adam's commonality reword: both designed-yes arrows blessed and all eight designed-no arrows refused with high confidence. + +--- + +## LGBT — if schools are pushing kids toward LGBT identities, that's wrong +Note: Gap: the moderate left's natural talk denies the push (support is not a push; calling it one is a smear). The moderate right's natural talk asserts the push and condemns it as obvious. Neither natural states the if-true-then-wrong deal that lets the left sign without conceding the fact. +Note: Modified → commonality is implication containment. Natural → modified remains a nudge because the conditional deal (and on the right, a camp extra) is extra beliefs. + +Statements +- **natural-left:** "Schools aren't pushing kids toward LGBT identities. They're just being supportive of kids who already worked that out themselves. Calling that a push is a smear." +- **natural-right:** "Schools are actively pushing kids toward LGBT identities. That's wrong. Stop it." +- **modified-left:** "I don't think schools are pushing kids toward LGBT identities. They're being supportive of kids who already worked it out themselves, and calling that a push is a smear. But if it turned out schools really are pushing kids in that direction, that's wrong — of course they shouldn't be doing it. I still want to argue about what counts as support versus a push; I just wouldn't defend an actual push if that's what's happening." +- **modified-right:** "Schools are actively pushing kids toward LGBT identities. That's wrong. I'd still rather we also stopped social transition at school without parents even in cases that look like 'just support,' but I don't feel as strongly about borderline support as about an actual push. I already think schools are pushing, so of course that's wrong. If they really weren't pushing, I wouldn't treat this as the same fight. We can argue about curriculum details another time; if schools are pushing kids toward LGBT identities, that's wrong." +- **commonality:** "If schools really are pushing kids toward LGBT identities, that's wrong. We can argue about what counts as 'supporting kids who are LGBT' versus 'pushing kids towards LGBT identities' another time, but to the extent that the latter is what's happening, that is not okay." + +Expected implication links +- Expect yes: modified-left → commonality; modified-right → commonality. +- Expect no: natural-left → commonality; natural-right → commonality; natural-left → modified-left; natural-right → modified-right; either modified → the other modified; commonality → either modified. + diff --git a/specs/user-docs.md b/specs/user-docs.md index 0de745f0..e5dd94c0 100644 --- a/specs/user-docs.md +++ b/specs/user-docs.md @@ -29,6 +29,6 @@ Write the docs for humans (narrative, plain language). Then add a block to the d **User-facing docs live in:** - Role-based how-tos live on the site where the role is actually performed (e.g. `lazyGiving/get-your-project-funded.md`, `alignment/become-a-delegate.md`, `tally/express-what-you-care-about.md`). The cross-ecosystem index is in [docs/end-user/commonality/index.md](/docs/end-user/commonality/index.md) under "What can I do across the ecosystem?". Each role doc ends with an "On other sites" footer pointing at the cross-site connections. -- CauseStarter’s in-app docs (`ui/src/causestarter` `/docs/*`) bundle `docs/end-user/causestarter/`, `shared/`, and `commonality/`. The everyday pitch is [the-jobs.md](/docs/end-user/causestarter/the-jobs.md). +- CauseStarter’s in-app docs (`ui/src/causestarter` `/docs/*`) bundle `docs/end-user/causestarter/`, `shared/`, and `commonality/`. The spoken briefing is [causestarter/index.md](/docs/end-user/causestarter/index.md) (bulletin board + two twists; the rest is objections). The jobs catalog is [the-jobs.md](/docs/end-user/causestarter/the-jobs.md). - [docs/end-user/shared/use-case-walkthroughs/](/docs/end-user/shared/use-case-walkthroughs/README.md) — concrete scenarios - [docs/end-user/shared/key-ideas/](/docs/end-user/shared/key-ideas/README.md) — concept reference pages diff --git a/ui/src/causestarter/pages/DocsPage.test.tsx b/ui/src/causestarter/pages/DocsPage.test.tsx index 13203b1a..214ea5ea 100644 --- a/ui/src/causestarter/pages/DocsPage.test.tsx +++ b/ui/src/causestarter/pages/DocsPage.test.tsx @@ -28,6 +28,20 @@ describe('DocsPage', () => { 'href', '/docs/start-a-cause', ) + expect(screen.getByRole('link', { name: /Questions people actually ask/i })).toHaveAttribute( + 'href', + '/docs/faq', + ) + }) + + it('renders the FAQ', () => { + renderDocs('/docs/faq') + expect(screen.getByRole('heading', { name: /Questions people actually ask/i })).toBeInTheDocument() + }) + + it('renders the eye-roll objections page', () => { + renderDocs('/docs/why-this-isnt-lame') + expect(screen.getByRole('heading', { name: /Why this isn’t lame/i })).toBeInTheDocument() }) it('renders the jobs catalog', () => { diff --git a/workflow/local-development.md b/workflow/local-development.md index adbcec11..02cce0a7 100644 --- a/workflow/local-development.md +++ b/workflow/local-development.md @@ -34,7 +34,15 @@ 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, no random universe 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. + +**Which seed:** these flags are different jobs (plan: [`fake-data-generation/PLAN.md`](/fake-data-generation/PLAN.md)): + +- `--seed` / `--seed=tiny` (default) — small **fake** world for UI and basic tests (CauseStarter boards, personas, a handful of well-shaped statements). Not a real-user catalog. Not a load test. +- `--seed=demo` — formal seed-content statements plus replayed Alignment explorer/nudge fixtures (still fake activity). +- `--seed=small` / `--seed=medium` — more **random fake users and actions** (stress-ish). `npm run gen:large` in `fake-data-generation` is 100 users; thousands of users is not a seed flag. + +Curated **real statements** for Conceptspace live in `fake-data-generation/seed-content/` and are authored separately; they are not “more fake users.” For a clean local reset, use: