Merge branch 'mantra' into claude/focused-brattain-433b3b

Takes the projects line, the chapter uploads, the tablet room selection and the
adoption fixes -- thirty-eight commits, 204 tests.

**Renumbered, for the third time in this window.** `mantra` reached 22 with the
projects tables, having itself renumbered from 21 after the parentage table took
that; this branch had already written 22 and 23. So the ritual keying becomes 22->23
and the membership split 23->24, by the rule in docs/curated-to-mantra-profiles.md
that a version belongs to whichever tree lands first. Nothing had shipped on either.

Schema 23 had to be regenerated rather than kept: the old 22.json described
`mantra@21 + the ritual keying`, and the intermediate shape is now
`mantra@22 + the ritual keying`, which includes the three project tables. Exported by
putting the entities briefly back to that shape, since Room only ever emits the
current version.

**Projects added three more cascading children, so ChatRoom now has twenty-two.** All
three of `MantraProject`, `MantraProjectArtifactVersion` and
`MantraProjectTranslationArtifactVersion` reference the room `ON DELETE CASCADE`. The
split's migration never enumerated them -- it depends on Room disabling foreign keys,
which is asserted rather than assumed -- so it needed no change, but every count in
its prose did.

`ChatRoomSplitThroughRoomJvmTest` is better for it: starting from a v22 file now runs
both of this branch's migrations in one upgrade, which is what a device coming off the
projects release will actually do.

Conflicts resolved toward mantra's shape with this branch's change applied inside it:
`roomDerivedBy` keeps its new extracted helper and reads the ceremony's own threshold
key through `getCeremonyThresholdPublicKey`, and `ChatRoomDetailViewModel` keeps the
`parents` rename with the per-profile MLS read. The new projects and chapters fixtures
needed the same membership seeding as the rest.

1212 tests, 0 failures.
This commit is contained in:
Kgothatso Ngako committed 2026-10-02 22:56:41 +02:00
commit deaf36f688
132 files changed
+22980 -691

No files matched your search

+20
View File
@@ -22,6 +22,8 @@ silent, or a decision that looked arbitrary and was not.
| [multiple-profiles.md](./multiple-profiles.md) | several profiles on one device — why a profile is a credential and a seed is a wallet attached to one, why a switch is a restart rather than a swap, the two entrances the app lacks, and the inbox a switch would silently lose |
| [jvm-target.md](./jvm-target.md) | what desktop support cost, phased — why the native chain was already done, why an empty source set in our phoenix fork was the real blocker, and why DAO tests need none of it |
| [npub-profile-preview.md](./npub-profile-preview.md) | showing the person before a direct message is started from a pasted npub — why the preview is a screen keyed by public key, what "found" means when a row can be a placeholder, and the phase that makes the button honest about a key package that never comes |
| [chapter-uploads.md](./chapter-uploads.md) | picking files off a device and having the group sign each one in as a chapter — why N files can never be one signing session, and the four things the single-chapter path gets away with |
| [project-feature.md](./project-feature.md) | bringing the server schema's `project` and its two join models into a group that has no accounts and no server — a creator tag of its own and owners as a signed set of `p` tags, the role the parser has to be told about or every project lands ownerless, why a membership is its own signed event rather than a list, and the kind band that is already double-booked |
| [material-design-conformance.md](./material-design-conformance.md) | what the M3 foundations actually require, measured against all 43 screens — the colour pairing that renders the app's own proposals invisible, and eight phases that put the decisions back in the theme |
| [curated-to-mantra.md](./curated-to-mantra.md) | pulling the Curated fork's thirty-nine commits back under Mantra's names — which lines of work to take, the three decisions, and a measured way to replay a twice-rebranded history without touching seven hundred files by hand |
| [curated-to-mantra-profiles.md](./curated-to-mantra-profiles.md) | the second pull: the fork's several-profiles line, ten commits and its first schema migration — which of it is a fix Mantra has today, who allocates a schema version number when two trees share one history, and the one conflict, which was Mantra's own |
@@ -77,3 +79,21 @@ smallest of the plans: it changes the entrance to the direct-message flow and
nothing past it, so read it with `StartDirectMessageToNpubOrNip05Dialog` and
`ChatRoomMessagingViewModel.initiateNewChat` open, its four decisions before its
four phases, and its table of where the build chose differently first.
The chapter uploads note is a phased plan that has been built, and is the
only one that starts from another codebase: it mirrors the mantra.press web app's
multi-file upload under this app's rule that the group, not the uploader, is the
author. Read the batch-signing note first — its three limits are the whole shape
of this one — and read its four-defects section even if the feature is never
built, because each of those is wrong in the single-chapter path today. Its two
splitter phases are separable from the rest and worth reading on their own: what
a chunk is, measured against the parser the web app translates with rather than
assumed.
The projects note is a phased plan that has been built as far as its seventh
phase, and is the only one
whose starting point is outside this repository: the platform's server schema,
`mantra-db-models`, has had the three models for years. It reads as the
translation domain's unanswered question — the app can say what a chapter is and
who translated it, and cannot say what any of it is for — so read it after the
nip30303 conventions are familiar, its decisions before its phases, and its scope
fork before either, because that one changes the shape of the rest, and its
table of where the build chose differently first.
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+177
View File
@@ -0,0 +1,177 @@
#!/usr/bin/env node
//
// Generates the golden fixtures for `Markdown.splitBlocks`, from markdown-it.
//
// `Markdown.splitBlocks` is hand-rolled, and its output is hashed into chunk
// event ids that a quorum signs -- so it is pinned by measurement against the
// reference implementation rather than by anybody's reading of CommonMark. This
// script is that measurement. It runs markdown-it over the corpus below and
// writes the expected chunks as a Kotlin source file in commonTest.
//
// node docs/scripts/markdown-block-fixtures.js \
// --markdown-it <path to a markdown-it install> \
// [--out composeApp/src/commonTest/kotlin/.../MarkdownBlockFixtures.kt]
//
// mantra.press has one: its node_modules/markdown-it. Pass that path.
//
// THE THREE DEPARTURES are applied here, in the oracle, so the Kotlin test is a
// plain equality check and the departures are visible in one place:
//
// 1. A fenced or indented code block yields no chunk. markdown-it's `fence`
// and `code_block` tokens carry content, so mantra.press turns `code();`
// into a chunk -- with a trailing newline in its text -- and asks a
// translator to translate it and the group to sign it as prose.
// 2. A table is one chunk of its own source, not one per cell. Four chunks
// for a 2x2 table is markdown-it's honest reading, but each costs an event
// envelope to carry one word and a table's alignment is structure that
// per-cell translation cannot preserve.
// 3. A link reference definition yields no chunk. This one is free: it is
// markdown-it's own behaviour with the `reference` block rule left
// enabled. mantra.press disables it -- reaching for the wrong rule name to
// stop links rendering -- which drops `[ref]: https://...` through to
// `paragraph` and makes a translatable chunk of a URL.
//
// `html: false` is markdown-it's default and is kept: an HTML block comes back
// as a paragraph, which is what the Kotlin subset does too.
const fs = require('fs')
const path = require('path')
function arg(name, fallback) {
const i = process.argv.indexOf(name)
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback
}
const mdPath = arg('--markdown-it', null)
if (!mdPath) {
console.error('--markdown-it <path to a markdown-it install> is required')
process.exit(2)
}
const outPath = arg(
'--out',
'composeApp/src/commonTest/kotlin/press/mantra/compose/text/MarkdownBlockFixtures.kt'
)
const md = require(path.resolve(mdPath))()
/**
* The corpus. Each entry is a shape that either appears in real chapters or has
* already caught something. The converter shapes come from mantra.press's own
* scripts/convert-btc-tex-to-markdown.js, which is what the files being
* uploaded are made by.
*/
const CORPUS = {
'one paragraph': 'A single paragraph.\n',
'two paragraphs': 'First paragraph.\n\nSecond paragraph.\n',
'lazy continuation': 'First line.\nSecond line of the same paragraph.\n',
'atx heading apart': '# Chapter one\n\nThe first paragraph.\n',
'atx heading glued': '# Chapter one\nThe first paragraph.\n',
'deep atx heading': '#### A fourth-level heading\n\nBody.\n',
'converter shape': '# Chapter one\nFirst paragraph here.\n\nSecond paragraph here.\n',
'setext h1': 'Title\n=====\n\nBody.\n',
'setext h2': 'Subtitle\n--------\n\nBody.\n',
'tight bullet list': 'Shopping:\n\n- milk\n- bread\n- eggs\n',
'ordered list': '1. first\n2. second\n',
'nested list': '- outer\n - inner\n',
'blockquote two lines': '> line one\n> line two\n',
'two blockquotes': '> first\n\n> second\n',
'thematic break': 'One.\n\n---\n\nTwo.\n',
'fenced code': 'Text.\n\n```\ncode();\n```\n\nMore.\n',
'fenced code with info': 'Text.\n\n```kotlin\nval x = 1\n```\n',
'indented code': 'Text.\n\n indented code\n\nMore.\n',
'table': '| a | b |\n|---|---|\n| 1 | 2 |\n',
'table with prose': 'Before.\n\n| a | b |\n|---|---|\n| 1 | 2 |\n\nAfter.\n',
'link reference definition': 'See [it][ref].\n\n[ref]: https://example.com\n',
'inline markup survives': 'A *word* and a [link](https://example.com) and `code`.\n',
'trailing hard break': 'Line one \nLine two\n',
'no trailing newline': 'No newline at the end.',
'leading blank lines': '\n\nA paragraph after blank lines.\n',
'crlf already normalised': 'One.\n\nTwo.\n',
'html block falls back to a paragraph': '<div>html block</div>\n',
'heading then list then quote':
'# Title\n\n- one\n- two\n\n> quoted\n\nEnd.\n',
'multi paragraph blockquote': '> first para\n>\n> second para\n',
'unicode': 'Ke a leboga.\n\nሰላም ለዓለም።\n',
}
/** The expected chunks for one input, with the three departures applied. */
function expected(source) {
const lines = source.split('\n')
const tokens = md.parse(source, {})
const out = []
let tableDepth = 0
for (const token of tokens) {
if (token.type === 'table_open') {
// Departure 2: one chunk of the table's own source.
const [start, end] = token.map
out.push(lines.slice(start, end).join('\n'))
tableDepth++
continue
}
if (token.type === 'table_close') {
tableDepth--
continue
}
if (tableDepth > 0) continue
// Departure 1: code carries content and is not prose.
if (token.type === 'fence' || token.type === 'code_block') continue
// Departure 3 needs no code: with the `reference` rule enabled, a link
// reference definition produces no token at all.
if (token.type === 'inline' && token.content) out.push(token.content)
}
return out
}
function kotlinString(value) {
return '"' + value
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\$/g, '\\$')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t') + '"'
}
const entries = Object.entries(CORPUS).map(([name, source]) => {
const chunks = expected(source)
return ` Fixture(
name = ${kotlinString(name)},
markdown = ${kotlinString(source)},
chunks = listOf(${chunks.length === 0 ? '' : '\n' + chunks.map(c => ' ' + kotlinString(c)).join(',\n') + ',\n '}),
),`
}).join('\n')
const file = `package press.mantra.compose.text
// GENERATED by docs/scripts/markdown-block-fixtures.js -- do not edit by hand.
//
// The expected output of [Markdown.splitBlocks] for ${Object.keys(CORPUS).length} inputs, taken from
// markdown-it with the three departures recorded in docs/chapter-uploads.md
// applied by the generator. Regenerate with:
//
// node docs/scripts/markdown-block-fixtures.js \\
// --markdown-it <path>/node_modules/markdown-it
//
// A change here is a change to what a chunk is, and so to the ids a quorum
// signs. It should arrive with a reason.
data class Fixture(
val name: String,
val markdown: String,
val chunks: List<String>,
)
val MARKDOWN_BLOCK_FIXTURES: List<Fixture> = listOf(
${entries}
)
`
fs.mkdirSync(path.dirname(outPath), { recursive: true })
fs.writeFileSync(outPath, file)
console.log(`wrote ${Object.keys(CORPUS).length} fixtures to ${outPath}`)