Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ services/implication-attester/dist
published-data-ipfs-mirror/dist/
hardhat/deployments/
deployments/localhost.env
deployments/localhost.contracts-manifest.json
deployments/operator-addresses.env
/data
sdk/src/generated/
Expand Down
6 changes: 3 additions & 3 deletions cause-assist/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ See `src/statementGuidance.ts` and the Implication Attester evaluator prompt for
| POST | `/sharpen-plank` | `{ plank, causeDescription? }` | Critique + optional reword against the attestable + signable bar (callers should treat `plank` as a suggestion, not auto-apply) |
| POST | `/draft-anchor` | `{ planks[] }` | Deterministic disjunctive anchor with verbatim planks and plank→anchor check payloads |
| POST | `/suggest-mediator-scaffold` | `{ foundingStatement, name? }` | Editable mediator identity, side labels, and complete starting anchor triples; never a strategy prompt |
| POST | `/draft-modified-plank` | `{ parentPlanks[], currentDraft?, sideLabel?, mustNotConcede?, complaint? }` | One modified-plank proposal for a human-authored bridge cluster. Not a chat turn. Refuses empty parents. |
| POST | `/draft-modified-plank` | `{ parentPlanks[], currentDraft?, sideLabel?, mustNotConcede?, complaint?, intendedBridge? }` | One modified-plank proposal for a human-authored bridge cluster. Not a chat turn. Refuses empty parents. |
| POST | `/draft-stand-in-sliver` | `{ sideLabel, bullets?, mustNotCaricature?, complaint?, currentDraft? }` | Thin roster for a camp with no published cause. Not a modified-plank call. |
| POST | `/draft-bridge-plank` | `{ modifiedSides[{ label?, planks[] }], currentDraft?, complaint? }` | One shared-platform plank from ≥2 modified sides. Strips justifications. |
| POST | `/critique-triple` | `{ modifiedPlanks[], bridgePlank }` | Objections and justification-leak warnings only — no rewrite |
| POST | `/draft-bridge-plank` | `{ modifiedSides[{ label?, planks[] }], currentDraft?, complaint? }` | One shared-platform plank from ≥2 modified sides. Strips justifications and coalition captions. |
| POST | `/critique-triple` | `{ modifiedPlanks[], bridgePlank, parentPlanks? }` | Objections (including `routing:` and `shape:`), justification-leak warnings — no rewrite |
| POST | `/check-implications` | `{ mainStatement, supportingStatements[] }` | Per-pair implies / confidence / reasoning |
| POST | `/safety-check` | `{ items: [{ text, fieldLabel? }] }` | Per-item allow/deny + user-facing explanation |
| POST | `/check-coherence` | `{ rosterCid, title, summary, planks[], mediatorBlurb? }` | Positive-only construction check for a would-be roster CID (preview; no chain write; may use heuristic without an API key) |
Expand Down
12 changes: 12 additions & 0 deletions cause-assist/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ export function createCauseAssistApp(config: CauseAssistConfig): express.Express
invalidRequest(res, `complaint must be a valid statement when provided`)
return
}
if (body.intendedBridge !== undefined && !validStatement(body.intendedBridge)) {
invalidRequest(res, `intendedBridge must be a valid statement when provided`)
return
}
res.json(await draftModifiedPlank(body, config))
} catch (error) { next(error) }
})
Expand Down Expand Up @@ -314,6 +318,14 @@ export function createCauseAssistApp(config: CauseAssistConfig): express.Express
invalidRequest(res, `bridgePlank is required and must be at most ${MAX_STATEMENT_LENGTH} characters`)
return
}
if (body.parentPlanks !== undefined && (
!Array.isArray(body.parentPlanks)
|| body.parentPlanks.length > MAX_EXISTING_STATEMENTS
|| body.parentPlanks.some((item) => !validStatement(item))
)) {
invalidRequest(res, `parentPlanks must be 0–${MAX_EXISTING_STATEMENTS} valid statements when provided`)
return
}
res.json(await critiqueTriple(body, config))
} catch (error) { next(error) }
})
Expand Down
16 changes: 16 additions & 0 deletions cause-assist/src/bridgeClusterAssist.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,34 @@ import assert from 'node:assert/strict'
import { describe, it } from 'mocha'
import type { LlmJsonRequest } from '@commonality/attester-core'
import { critiqueTriple, draftBridgePlank, draftModifiedPlank, draftStandInSliver } from './bridgeClusterAssist.js'
import { BRIDGE_STATEMENT_GUIDANCE, STATEMENT_QUALITY_GUIDANCE } from './statementGuidance.js'
import type { CauseAssistConfig } from './types.js'

const config: CauseAssistConfig = {
apiKey: 'key', apiBaseUrl: 'https://example.test/v1', suggestModel: 'model',
safetyModel: 'model', implicationModel: 'model', coherenceModel: 'test', port: 0,
}

describe('statement guidance routing', () => {
it('keeps signer-annoyance routing on bridge drafts, not ordinary cause verbs', () => {
assert.doesNotMatch(STATEMENT_QUALITY_GUIDANCE, /annoyed at being asked/)
assert.match(BRIDGE_STATEMENT_GUIDANCE, /annoyed at being asked to also sign the shared plank/)
})
})

describe('bridge cluster wording verbs', () => {
it('drafts a modified plank from parent texts without writing a strategy prompt', async () => {
const result = await draftModifiedPlank({
parentPlanks: ['Marriage is a covenant and children are a blessing.'],
sideLabel: 'practising Christians',
mustNotConcede: 'Do not reduce this to outcome data.',
intendedBridge: 'It should be easier to marry and raise children.',
}, config, async <T>(request: LlmJsonRequest) => {
assert.match(request.systemPrompt, /human remains the publisher/i)
assert.doesNotMatch(request.systemPrompt, /strategy prompt you should write/i)
assert.match(request.systemPrompt, /Containment is a check after drafting/i)
assert.match(request.userPrompt, /must_not_concede/)
assert.match(request.userPrompt, /intended_bridge/)
return { plank: 'Marriage and children are among the best things God gives us, and I want family formation to be a normal, achievable thing.', rationale: 'Keeps covenant language.', warnings: [] } as T
})
assert.equal(result.source, 'llm')
Expand All @@ -33,6 +44,7 @@ describe('bridge cluster wording verbs', () => {
],
}, config, async <T>(request: LlmJsonRequest) => {
assert.match(request.systemPrompt, /justifications/i)
assert.match(request.systemPrompt, /coalition caption/i)
return { plank: 'It should be easier than it currently is for people to marry and raise children.', rationale: 'Conclusion only.', warnings: [] } as T
})
assert.equal(result.source, 'llm')
Expand All @@ -46,8 +58,12 @@ describe('bridge cluster wording verbs', () => {
'Kids do better with two committed parents.',
],
bridgePlank: 'Marriage is a gift from God and also the data says so.',
parentPlanks: ['Marriage is a covenant.', 'Kids do better with two parents.'],
}, config, async <T>(request: LlmJsonRequest) => {
assert.match(request.systemPrompt, /Do not rewrite/)
assert.match(request.systemPrompt, /routing:/)
assert.match(request.systemPrompt, /shape:/)
assert.match(request.userPrompt, /parent_planks/)
return {
objections: ['Shared plank requires a theological premise.'],
leakWarnings: ['God-talk leaked into the bridge plank.'],
Expand Down
24 changes: 20 additions & 4 deletions cause-assist/src/bridgeClusterAssist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
type StatementStrategy,
} from '@commonality/bridge-creator/strategy-engine'
import type { RequestJsonCompletionFn } from '@commonality/attester-core'
import { STATEMENT_QUALITY_GUIDANCE } from './statementGuidance.js'
import { BRIDGE_STATEMENT_GUIDANCE, STATEMENT_QUALITY_GUIDANCE } from './statementGuidance.js'
import type {
CauseAssistConfig,
CritiqueTripleRequest,
Expand Down Expand Up @@ -58,15 +58,20 @@ export const draftModifiedStrategy: StatementStrategy<

${STATEMENT_QUALITY_GUIDANCE}

${BRIDGE_STATEMENT_GUIDANCE}

${MEDIATION_RULES}

If an intended shared plank is provided, do not copy its sentences into the modified. Check whether the parent already says that civic claim; if it does, warn. If it does not, warn that the extra is a real ask. First-person limits are fine; do not talk about the other camp.

Return JSON only: {"plank":"...","rationale":"why this camp would still sign and what was not conceded","warnings":["..."]}.`,
renderInput: (input) => ({
parent_planks: input.parentPlanks,
current_draft: input.currentDraft ?? null,
side_label: input.sideLabel ?? null,
must_not_concede: input.mustNotConcede ?? null,
organizer_complaint: input.complaint ?? null,
intended_bridge: input.intendedBridge ?? null,
}),
normalize: draftNormalize,
}
Expand Down Expand Up @@ -122,10 +127,12 @@ export const draftBridgeStrategy: StatementStrategy<
{ plank: string; rationale: string; warnings: string[] }
> = {
name: 'cause-assist-draft-bridge-plank',
systemPrompt: `You propose one shared (bridge) plank that each modified wording can independently imply. Strip both sides' justifications. If a justification leaked in, refuse that wording.
systemPrompt: `You propose one shared (bridge) plank that each modified wording can independently imply. Strip both sides' justifications. If a justification leaked in, refuse that wording. If a coalition caption leaked in (whose reasons, whose maximalism, "we come from different places"), refuse that wording.

${STATEMENT_QUALITY_GUIDANCE}

${BRIDGE_STATEMENT_GUIDANCE}

${MEDIATION_RULES}

Return JSON only: {"plank":"...","rationale":"why neither side's why is required","warnings":["..."]}.`,
Expand All @@ -144,17 +151,26 @@ export const critiqueTripleStrategy: StatementStrategy<
name: 'cause-assist-critique-triple',
systemPrompt: `You critique a proposed bridge triple. Do not rewrite. List objections a fair-minded person on each side would raise, and flag any justification leak into the shared plank (theology in a secular-signable claim, or reducing a faith claim to "studies show").

Also apply the implication-vs-nudge routing test. For each modified plank → bridge plank: if a reasonable signer of the modified would be annoyed at being asked to explicitly sign the bridge ("I already said that"), the pair should be an implication (containment). If they would not be annoyed, the modified does not contain the shared claim yet — object. If they would be annoyed but a different reasonable person would see a real extra claim in the bridge, do not treat that as containment; object that the pair is a nudge (or that the wording hides the delta), not an implication. Unreasonable annoyance is not a reason to bless an arrow.

Shape failures the attester will not catch (prefix with "shape:"):
- Identical or near-identical shared sentences pasted into both modifieds so subset fires (subset-by-concatenation). A bless is necessary, not sufficient.
- Shared plank still one camp's rant with the other camp's theology deleted, or a coalition caption ("we come from different places," commentary on whose reasons or maximalism).
- Multi-register or too long to sign as a paragraph.
- Parent/natural already contains the shared claim (triple decorative), or the modified introduces a civic program the parent never held without reaffirming the rest of the bundle (withhold-from-natural / belief jump).

${MEDIATION_RULES}

Return JSON only: {"objections":["..."],"leakWarnings":["..."]}. Empty arrays mean you found nothing load-bearing to flag.`,
Return JSON only: {"objections":["..."],"leakWarnings":["..."]}. Empty arrays mean you found nothing load-bearing to flag. Prefix routing failures with "routing:" and shape failures with "shape:".`,
renderInput: (input) => ({
modified_planks: input.modifiedPlanks,
bridge_plank: input.bridgePlank,
parent_planks: input.parentPlanks ?? [],
}),
normalize: (value) => {
const record = value && typeof value === 'object' ? value as Record<string, unknown> : {}
return {
objections: stringList(record.objections).slice(0, 8),
objections: stringList(record.objections).slice(0, 12),
leakWarnings: stringList(record.leakWarnings).slice(0, 8),
}
},
Expand Down
13 changes: 12 additions & 1 deletion cause-assist/src/statementGuidance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const STATEMENT_QUALITY_GUIDANCE = `What a statement is (Commonality / Ca
- Statements must be self-contained. Do not use slogans, tribe-markers, or shorthand that needs unstated background context (e.g. reject "I am pro-choice" as not clear enough by itself).
- Prefer concrete, signable claims over marketing fluff, mission slogans, or vague aspirations.
- Statements are public and permanent. Do not invent illegal, fraudulent, hateful, doxxing, sanctions-evading, or election-campaign-fundraising content. No personal contact details or private identifiers.
- Prefer 1–2 sentences per statement.
- Prefer 1–2 sentences per statement. This bar is for ordinary cause planks and uniques. Modified/bridge wording may be longer when the extra words are load-bearing — see bridge guidance if this task is mediation.

Implication rule for supporting statements (critical):
- The main statement (S1) must logically imply each supporting statement (S2).
Expand All @@ -20,3 +20,14 @@ Implication rule for supporting statements (critical):
- Do not reject merely because S2 is broad, permits multiple implementations, or leaves details unsettled.
- Implication is stronger than topical relatedness. Do not draft "drivers," "principles," or "why it matters" extras unless they are already entailed by the main wording.
- When in doubt, do not suggest the supporting statement.`

/** Extra rules for human-authored bridge clusters. Do not use this as a drafting algorithm for attester subset. */
export const BRIDGE_STATEMENT_GUIDANCE = `Modified and shared (bridge) planks:
- Signature, not column: one register, one speech act, short enough that a real person would sign the paragraph. Not an op-ed. Not three slogans stacked.
- Name the gap first. If both camps already share the civic conclusion, the shared plank is that conclusion with both *whys* omitted. Do not invent a compromise, a deal, or a narrator to make the implication system look busy.
- Containment is a check after drafting, not a method. Do not paste the shared sentences into each modified so the attester's subset rule fires.
- Parents/naturals are how that camp talks. Do not withhold a civic line from the parent so the modified can "add" it. If the parent already contains the shared claim, say so in warnings (the triple may be decorative).
- If the shared claim is not in the parent, that extra is a real ask. Warn. Do not disguise a belief jump as a small edit. Unbundling must reaffirm the rest of that camp's bundle.
- First-person limits belong on that side's modified ("I am not asking the state to make anyone pray"). Do not put coalition captions on the shared plank — not "we come from different places," "I don't need your reasons," "people who get here from biology are not my enemy," or "the civic job is not to impose a church / wait for religion to disappear."
- The shared plank must not require either side's justification (no theology a secular signer must affirm; no reducing faith to "studies show"). Also strip commentary on whose project this is.
- Routing: a reasonable signer of the modified should be annoyed at being asked to also sign the shared plank ("I already said that"). If they would not, the modified does not contain it — thicken the modified or keep it a nudge. Do not fatten the shared plank.`
4 changes: 4 additions & 0 deletions cause-assist/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export interface DraftModifiedPlankRequest {
mustNotConcede?: string
/** Organizer complaint about the current draft, if any. */
complaint?: string
/** Optional intended shared plank — check containment; do not paste it into the modified. */
intendedBridge?: string
}

export interface DraftModifiedPlankResponse {
Expand Down Expand Up @@ -113,6 +115,8 @@ export interface DraftBridgePlankResponse {
export interface CritiqueTripleRequest {
modifiedPlanks: string[]
bridgePlank: string
/** Parent/natural texts when known — needed to catch withhold-from-natural. */
parentPlanks?: string[]
}

export interface CritiqueTripleResponse {
Expand Down
1 change: 1 addition & 0 deletions causestarter/docker-entrypoint.d/40-causestarter-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ write_kv VITE_CONTENT_REGISTRY_ADDRESS "${VITE_CONTENT_REGISTRY_ADDRESS:-}"
write_kv VITE_CHANNEL_REGISTRY_ADDRESS "${VITE_CHANNEL_REGISTRY_ADDRESS:-}"
write_kv VITE_CHANNEL_ESCROW_ADDRESS "${VITE_CHANNEL_ESCROW_ADDRESS:-}"
write_kv VITE_CREATOR_CONTRACT_FACTORY_ADDRESS "${VITE_CREATOR_CONTRACT_FACTORY_ADDRESS:-}"
write_kv VITE_PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS "${VITE_PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS:-}"
write_kv VITE_PROJECT_FACTORY_CONTRACT_ADDRESS "${VITE_PROJECT_FACTORY_CONTRACT_ADDRESS:-}"
write_kv VITE_PAYMENT_TOKEN_ADDRESS "${VITE_PAYMENT_TOKEN_ADDRESS:-}"
write_kv VITE_CHAIN_ID "${VITE_CHAIN_ID:-}"
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,7 @@ services:
VITE_CHANNEL_REGISTRY_ADDRESS: ${VITE_CHANNEL_REGISTRY_ADDRESS:-}
VITE_CHANNEL_ESCROW_ADDRESS: ${VITE_CHANNEL_ESCROW_ADDRESS:-}
VITE_CREATOR_CONTRACT_FACTORY_ADDRESS: ${VITE_CREATOR_CONTRACT_FACTORY_ADDRESS:-}
VITE_PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS: ${VITE_PROSPECTIVE_CONTENT_ROUND_FACTORY_ADDRESS:-}
VITE_PROJECT_FACTORY_CONTRACT_ADDRESS: ${VITE_PROJECT_FACTORY_CONTRACT_ADDRESS:-}
VITE_PAYMENT_TOKEN_ADDRESS: ${VITE_PAYMENT_TOKEN_ADDRESS:-}
VITE_CHAIN_ID: ${VITE_CHAIN_ID:-31337}
Expand Down
Loading
Loading