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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions cause-assist/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
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 @@ -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 <T>(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:/)
Expand All @@ -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 <T>(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',
Expand Down
20 changes: 19 additions & 1 deletion cause-assist/src/bridgeClusterAssist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',
}
}
21 changes: 21 additions & 0 deletions cause-assist/src/plankStrategies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T>() => ({
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 <T>(request: LlmJsonRequest) => {
assert.match(request.systemPrompt, /Hedge explicitly/i)
Expand All @@ -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 <T>() => ({
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)
Expand Down
7 changes: 5 additions & 2 deletions cause-assist/src/plankStrategies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<SharpenPlankResponse> {
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[]) {
Expand Down
60 changes: 60 additions & 0 deletions cause-assist/src/statementQualityGate.test.ts
Original file line number Diff line number Diff line change
@@ -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 <T>(_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/)
})
})
78 changes: 78 additions & 0 deletions cause-assist/src/statementQualityGate.ts
Original file line number Diff line number Diff line change
@@ -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<string[]> {
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
}
5 changes: 4 additions & 1 deletion causestarter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/`,
Expand Down
Loading
Loading