Skip to content

Pause duration, a daily reminder, cool.jacoblin.particeps, and a documentation pass - #29

Merged
JacobLinCool merged 8 commits into
mainfrom
a/pause-since-and-daily-reminder
Aug 7, 2026
Merged

Pause duration, a daily reminder, cool.jacoblin.particeps, and a documentation pass#29
JacobLinCool merged 8 commits into
mainfrom
a/pause-since-and-daily-reminder

Conversation

@JacobLinCool

@JacobLinCool JacobLinCool commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Eight commits, in the order they were made.

App

Report when a pause started and how long it has lasted. A pause is the one state a participant can leave a study in by accident: nothing on the phone changes and no notification is showing. The status line gains a second row — Paused since Aug 7, 3:16 AM, 0m ago — rendered with Android's DateUtils, so it follows the participant's locale and their 12/24-hour setting. The date is always shown, because a line reading "3:16" is indistinguishable from one three days old, which is the case it exists to catch.

One reminder a day, running or paused. A low-importance notification, replacing yesterday's rather than stacking. It names the application and the state — never the study, never a count — because a lock screen is readable by whoever is holding the phone. Scheduled when collection starts, re-established on session init, deliberately not cancelled on pause, retired when the study ends. Starting or stopping collection retracts a standing one, so the shade never keeps asserting "Still collecting" after collection stopped.

Both branches were verified on an emulator against the real notification record.

Move to cool.jacoblin.particeps. The previous namespace carried a personal handle the product does not use elsewhere, and the release certificate's subject still named the pre-rename product. Neither is correctable later. Doing both now cost nothing: every tag published so far is a pre-1.0 release candidate and Developer Verification had not been registered. The signing key was rotated with the corrected subject CN=Particeps, O=Particeps, L=Taipei, ST=Taiwan, C=TW.

tools/retired_identity_audit.py now knows two dead namespaces and refuses a reference to either, with a repository-wide pattern for the second — verified by injection into a Python tool, a workflow, and TypeScript.

Documentation

An audit compared all 18 documents against the code.

Correctness first. The participant guide documented one way to enrol while the application ships two: the researcher guide tells researchers to recruit with a particeps://join/v1 link or QR code, and the participant scanning it held the one document that never mentioned it. Its four result codes are now in the troubleshooting table, one of them marked as not a fault. A limitation nobody had written down is now stated: the join URI carries signer_fingerprint, so a participant who arrives by link is shown a fingerprint that came from that same link, and comparing the two shows only that the link agrees with itself.

The collector implementation guide was wrong in six ways that would each mislead someone writing a collector — EmitResult has four members and it documented three, AccessKind has ten and it listed seven, two lifecycle signatures had the wrong return types, a module was said to hold two files and holds five, and two members were named that do not exist. Verified against the source, member by member.

The daily reminder is now in the threat model as a disclosure surface a bystander reads without decrypting anything, in the system design, and in the researcher guide — with its limits: best-effort timing, no configuration field to switch it off, no guarantee anyone was reminded.

Then structure. docs/p0-p2-implementation-contract.md is retired: a plan written in the future tense about work that shipped three releases ago. docs/component-boundaries.md is retired: every contract in it restated the system design's module table. CHANGELOG.md is new, and owns the release history that four documents were each telling in full.

Then duplication. The upload receipt contract was told in nine documents and is now in the protocol specification with a sentence and a link elsewhere. Same for the HPKE framing, the mechanism under signer trust, the demonstration-key warning, and the negative field lists. The caveats all stay — "a signature proves the file is unchanged since it was signed, not who wrote it" appears in five documents on purpose.

Sentences that were both long and clause-dense fall from 96 to 38; the longest in the set from 78 words to 57.

before after
documents 18 17
words 56,300 57,109
hard sentences 96 38
files explaining the receipt contract 9 2

Net words barely move, and that is honest: the correctness pass added a section the participant guide was missing and three descriptions of the reminder. Deduplication paid for them.

A pause is the one state a participant can leave a study in by accident. Nothing
on the phone changes, no notification is showing, and a study meant to run for a
fortnight quietly records nothing until someone thinks to open the app. The
running case is not padding either: a study that collects for weeks should keep
saying so, because consent nobody is reminded of is consent in name only.

One notification a day, replacing yesterday's rather than stacking, on its own
low-importance channel. Daily is long enough that anything noisier would be a
reason to uninstall, which would end a study far more effectively than a missed
reminder would.

It reports state and nothing else — no counts, no collector names, no study
content — because a notification is readable on a lock screen by whoever is
holding the phone. The paused text names when the pause started, since a
reminder that only said "Paused" leaves no way to tell five minutes from a
fortnight.

Periodic rather than the self-renewing chain used for delivery: a day is far
above WorkManager's fifteen-minute floor, so nothing is silently clamped, and
the platform re-establishes periodic work across reboots. Scheduled when
collection starts and re-established on session init, so a study already under
way before this existed also gets it; deliberately not cancelled on pause, and
retired with the rest of the collection work when the study ends.
The previous namespace carried a personal handle that the product does not use
anywhere else, and the release certificate's subject still named the pre-rename
product. Neither can be corrected later: an applicationId is how Android
identifies an installed application, and a certificate is signed over its own
subject, so changing either issues something a device treats as a different app.
Doing both now costs nothing, because every tag published so far is a pre-1.0
release candidate and Developer Verification has not been registered against any
of them. After a released build reaches a participant it would cost a great deal,
so this is the last time.

The audit that pins the fresh-install boundary now knows about two dead
namespaces rather than one, and refuses a reference to either. The release
guide, the README, and invariant 8 no longer claim the signing key was
deliberately preserved, which was true of the first rename and is not true now.
@changeset-bot

changeset-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 1c3cddd

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Pre-merge review found two defects in the reminder, both mine.

The notification put the researcher-supplied study title in its content title,
while the same file's comment claimed it disclosed no study content. A study
called "Smartphone use and depressive symptoms in adolescents" would have
rendered as the bold first line of a lock-screen notification, every day, for
the study's whole duration — including while paused, a state in which the phone
previously showed nothing at all. The title is now the app's own name, which the
launcher and Android's settings already display. The ongoing collection
notification still carries the study title, but only while collection is really
running and only as its secondary line.

Pausing also left the already-posted notification standing, so the lock screen
could keep asserting "Still collecting" for up to a day after collection
stopped — the exact opposite of what the reminder is for. Starting or stopping
collection now retracts it; the next daily run posts the truth. Re-posting
immediately would turn a daily reminder into a notification on every pause.

The audit gained a repository-wide pattern for cool.linc.particeps. Unlike the
first retired namespace it contains no ADC spelling, so it had been enforced
only on the six suffixes the boundary check reads, and a regression in a Python
tool, a workflow, or TypeScript would have passed. Verified by injection into
all three.

The participant guide now describes the reminder. It enumerates every
notification a participant sees, and this one was missing from that list and
from the notification-permission section.
It was a plan, written in the future tense about work that shipped three
releases ago, and it had become a de facto changelog nobody updates. Every
section restated a document that owns the subject: the invariants compress
protocol/v1/README.md and system-design.md, the responsibility map repeats
system-design's module table, the upload transaction was a third copy of the
receipt contract, and the P2 design limits are covered in more detail by the
researcher guide.

One section was not a duplicate. The five phase-review criteria are a standard
for finishing work, so they move to CONTRIBUTING.md, where the person about to
open a pull request will see them.

CONTRIBUTING.md also claimed the collector capability boundary is enforced by
review rather than by a check. It is enforced by tools/collector_assurance.py on
every pull request. The claim is replaced by what the check actually covers, and
by the gap it does not — the manifest, which issue #11 tracks.
…sing

An audit compared every document to the tree it describes. This fixes what it
found wrong, and adds three things that were true but written down nowhere. It
changes nothing for style; duplication and sentence length are a later pass.

The most important gap was in the participant guide. It documented one way to
enrol — the system file picker — while the application also ships a
particeps://join/v1 handler and the researcher guide tells researchers to
recruit with a link or a QR code. The participant scanning that code held the
one document that never mentioned it, and none of the four codes that path can
produce appeared in its troubleshooting section. One of those four is not a
fault: a join is refused while another study or a pending deletion exists.

That path also carries a limitation nobody had written down. The join URI
carries signer_fingerprint as a query parameter, so a participant who arrives by
link is shown a fingerprint that came from that same link. Comparing the two
shows only that the link agrees with itself. The guide now says so where it
explains the check.

The daily status reminder existed in code and in one sentence of the participant
guide. It is now in the threat model as a disclosure surface a bystander can
read without decrypting anything, in the system design's background-execution
section, and in the researcher guide where a researcher plans participant
contact — with its limits: best-effort timing, no configuration field to switch
it off, and no guarantee anyone was reminded.

The collector implementation guide was wrong in six places that would each have
misled someone writing a collector: EmitResult has four members and the guide
documented three, AccessKind has ten and the guide listed seven, two lifecycle
signatures were quoted with the wrong return types, the module was said to hold
two files and holds five, a guard was named that does not exist, and a label
hook was named that does not exist. Verified against the source, member by
member.

Smaller corrections: the release guide presented a finished cutover as pending
work; the system design over-claimed what the participant dashboard shows per
collector; the data dictionary promised every exported field and omitted the
transition history; README listed six of seven CLI commands and one of two
distribution paths; the protocol specification said Python does not read the
join corpus three lines after telling the reader to run a script that does.
The rename was told at full length in four documents, about a thousand words
between them, because no document owned "what changed and what it asks of you".
CHANGELOG.md owns it now. The README and the release guide keep the paragraph
their own reader needs and link for the rest; the participant guide keeps its
own, because a participant should not have to open a changelog.

component-boundaries.md is retired. Every contract in it restated a row of the
system design's module table, its invariants restated that document's security
and privacy invariants, and its ASCII dependency sketch was a worse version of
the mermaid diagram three sections earlier. Nothing was lost: the two claims
worth checking — that a collector cannot see storage or the runtime, and that
reclaiming runs between 80% and 60% of quota — were already stated there, in
more detail.

The system design listed six of the seven researcher-tools commands. Same
omission the README had, in a document the earlier correctness pass did not
cover: personalize was missing.
…nces

The upload receipt contract was told in full in nine documents; it is now in the
protocol specification, with a sentence of consequence and a link everywhere
else. The same treatment for the HPKE and PTCEXP01 framing, the mechanism under
signer trust, the demonstration-key warning, and the per-collector negative
field lists: each has one owner, and the documents that used to repeat it keep
only what their own reader needs.

The caveat is not the mechanism, and the caveats all stay. "A signature proves
the file is unchanged since it was signed, not who wrote it" appears in five
documents on purpose, because five different readers need it where they are
standing. What moved is the explanation of how the key travels in the signed
bytes and how the fingerprint is derived.

Sentences that were both long and clause-dense fall from 96 to 38, and the
longest in the set from 78 words to 57. Nothing was shortened by deleting
content: the count of sentences rises, because the fix for a 78-word sentence is
three sentences rather than a shorter one.

Net words barely move. The correctness pass before this added a section the
participant guide was missing and three descriptions of the daily reminder, and
splitting a sentence costs a few words. Deduplication paid for both.
@JacobLinCool JacobLinCool changed the title Show how long a pause has lasted, remind daily, and move to cool.jacoblin.particeps Pause duration, a daily reminder, cool.jacoblin.particeps, and a documentation pass Aug 7, 2026
@JacobLinCool
JacobLinCool merged commit 0157b22 into main Aug 7, 2026
5 checks passed
@JacobLinCool
JacobLinCool deleted the a/pause-since-and-daily-reminder branch August 7, 2026 00:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant