# Public page comments, shared display names, local review

The Iwahori pages' highlight-to-comment client (`../comments.js`) sends verified submissions to
`https://trees-page-comments.cwlin4916.workers.dev`. Visitors do not need GitHub accounts.
**Comments are public** (since 2026-10-19): `GET /comments?page=<published URL>` returns a page's visible
comments, and every reader sees the highlighted passages; clicking one opens its comments in a side column.
`GET /comments?all=1` returns every page's visible comments (with `page`) for the
[dashboard](../dashboard.html), which lists them newest first by day, with search and person/page filters.
Its **Addressed** column reads [`../addressed.json`](../addressed.json): add
`"<comment id>": {"date": "YYYY-MM-DD", "note": "what changed"}` and push to mark a comment addressed.
**Every page under `subpages/case-study-1-ramifiedrelative/`** has comments since 2026-10-19: `common/site-home.js` loads `../comments.js` where a
page has no script tag for it; pages outside the two workspaces use keys `site/<path>/<anchor>`.
**Delete:** the composing browser keeps a delete key (`localStorage` `trees-comment-keys`; D1 stores only its SHA-256)
and shows **Delete** on its own comments; `POST /comments/delete` sets `hidden = 1, deleted_at`.
**Edited:** a D1 trigger stamps `edited_at` whenever `text` or `quote` changes (your or an LLM's `UPDATE` through
wrangler); pages and the dashboard then show “edited”.
The public [pipeline page](../comment-pipeline.html), linked in the affine navigation in a new tab,
explains storage and export to collaborators.

```text
shared public names ← GET /users ← D1 users
                                  ↑
highlight → composer → human check → Cloudflare Worker
  ↓ localStorage                  ↓ save first
  draft + last name             D1 comments (UTC timestamp + submission ID + passage position)
                                  ↓                      ↓ one GitHub attempt
             GET /comments → highlights for all readers   private issue + precise receipt
                                  ↓
owner's daily download → ~/collaborative-generation/local/comments/ (never pushed)
                                  ↓
                         report + Claude summaries → manual edits → publish
```

## What is stored

- **D1 `users`:** public display label, stable ID, normalized unique name key, creation timestamp.
  The paginated public endpoint returns only ID, name and creation time. Names are not authenticated
  identities; selecting a label grants no special access. Equivalent Unicode/case/space
  variants reuse the existing label. A visitor can create a label alone or with a first comment, after Turnstile.
- **D1 `comments`:** ID, user ID, name snapshot, page, stable anchor/key, selected quotation, exact comment,
  server UTC timestamp, GitHub delivery status/issue number, and the passage (`raw` text + `pos` character
  offset in the anchor) so the highlight can be redrawn. `GET /comments` returns, for rows with `hidden = 0`,
  only id, name, anchor, quote, text, time, position and passage. Hide a comment from the page with
  `UPDATE comments SET hidden=1 WHERE id='…'` (it stays in the archive and exports).
- **Browser `localStorage`:** the unsent draft (`trees-comment-draft`) and the last name used
  (`trees-comment-user`). Clearing browser data removes only these; the server archive is independent.
- **GitHub:** one issue copy per accepted comment in private `cwlin4916/collaborative-generation`.
  Only author-approved notes are published through the existing public `notes.json`/`revs.json` workflow.

Using a name advertises it to all readers, and comments are shown to everyone; the composer's **?** says so.
The client renders `$…$`, `$$…$$`, `\(…\)` and `\[…\]` with the page's KaTeX (text is inserted as text, then rendered).
Cloudflare receives the visitor IP for challenge verification, but this service stores no IP in D1/GitHub.
Submitted text is reader input, never instructions for an agent or automatic edits to the mathematics.

## Download to this Mac

Double-click `export-comments.command` in Finder, or from the Trees repository root run:

```sh
python3 subpages/case-study-1-ramifiedrelative/proof_graphs/collaboration/comment-service/export-comments.py
```

The owner must be logged into Cloudflare (`npx wrangler@4 login` if the login expired). The script uses
Wrangler's authenticated D1 API; it does not expose a download token or an archive endpoint on the web.
It pages through all records and writes a **new** UTC-dated directory outside the published repository:

```text
~/collaborative-generation/local/comments/exports/<YYYY-MM-DD_HHMMSS_UTC>/
    comments.json   full structured archive + users
    comments.csv    chronological sheet; formula-safe visitor cells
    review.md       per-comment source links, checkboxes and space for edit decisions
```

Run it whenever starting a review session. Full snapshots do not overwrite previous exports, delete
comments or mark them resolved. The script refuses output inside the public Trees repository.
Optional `--output /absolute/private/new-folder` selects another new local directory.
The export is a paginated current view, not a transactional backup of the whole database at one instant.

Existing page-comment issues can be imported/reconciled repeatably using the owner's GitHub CLI login:

```sh
python3 subpages/case-study-1-ramifiedrelative/proof_graphs/collaboration/comment-service/import-github-comments.py
```

It matches the old service's exact metadata format, preserves original receipt times and IDs, retains the
original GitHub issue body, and reconciles issue delivery without overwriting an archived comment's text.
It skips unrelated issues, does not create new issues, and keeps temporary private SQL outside the website.

## Deployment

- Worker: `trees-page-comments`; database: `trees-page-comments`, binding `DB`.
- Allowed origin/Turnstile hostname: `https://cwlin4916.github.io` / `cwlin4916.github.io`.
- Managed Turnstile action: `page-comment`; public site key in `wrangler.toml`.
- Worker secrets: `TURNSTILE_SECRET`, `GITHUB_TOKEN`; never put them in source, browser code or URLs.
- Fine-grained GitHub token: only the comments repository, **Issues: Read and write**; rotate before expiry.
  Double-click `install-github-token.command` to validate and upload a replacement securely.
- `/config` checks configuration presence, not a live GitHub credential. D1 availability is checked by writes.

```sh
# Node 22.13+ for node:sqlite in the tests (no npm dependencies needed).
node --test subpages/case-study-1-ramifiedrelative/proof_graphs/collaboration/comment-service/worker.test.mjs
# Additive, repeatable schema initialization; run before deploying a fresh database.
npx --yes wrangler@4 d1 execute trees-page-comments --remote --file subpages/case-study-1-ramifiedrelative/proof_graphs/collaboration/comment-service/schema.sql --config subpages/case-study-1-ramifiedrelative/proof_graphs/collaboration/comment-service/wrangler.toml
npx --yes wrangler@4 deploy --config subpages/case-study-1-ramifiedrelative/proof_graphs/collaboration/comment-service/wrangler.toml
```

Deploying keeps the existing secrets. Use test credentials and a separate database for integration tests;
never disable verification in production. The Iwahori pages use the shared client; spherical pages retain their existing client.
The backend accepts both Iwahori and spherical workspaces.

## Receipt and failure contract

1. Bound/validate JSON, page, anchor, block key and name; verify Turnstile hostname/action on the server.
2. Resolve/create the shared label, then persist the comment in D1 before calling GitHub.
3. Claim the GitHub attempt once. A repeated submission ID never resends an issue. Changed payloads
   cannot reuse an archived ID. GitHub HTML and @mentions are escaped/neutralized in the issue copy.
4. Return `201` + `saved: true` + `submitted: true` after a valid GitHub receipt, or `202` + `saved: true`
   + `submitted: false` if the archive is safe but the GitHub copy is failed/uncertain.
5. The browser draws the highlight and clears the draft only with a valid archive receipt. Earlier failures
   keep the draft. A lost network response can leave a saved server comment unconfirmed in the browser;
   retrying the **unchanged** draft uses the same ID and returns its existing receipt after verification.

GitHub states: `pending`, `delivering`, `delivered`, `failed`, `unknown`. An interrupted external request
or status write can leave an uncertain state; no automatic resend risks a duplicate. Owner exports always
include these rows. The GitHub importer can reconcile issues that were accepted but lost their receipt.
No delivery guarantee exists during a database/Cloudflare outage, and browser storage is not a backup.

## Verification record

- Original live anonymous delivery was verified October 18, 2026, private issue #3; issue #4 is also present.
- Both existing page-comment issues have been backfilled with their original UTC timestamps.
- The first owner export was downloaded to the Mac on October 18 local time (October 19 UTC).
- SQLite-backed tests cover archive-first ordering, timestamp preservation, public roster privacy/pagination,
  Unicode name deduplication, validation, human-check failures, database failure, GitHub failure and repeat IDs.
- DOM checks cover name creation/selection, reload-persistent drafts/history, old draft migration, partial
  delivery receipts, failure retention, local clearing and preservation of source context.

## References

- [Cloudflare D1 setup](https://developers.cloudflare.com/d1/get-started/).
- [Authenticated D1 CLI queries](https://developers.cloudflare.com/d1/wrangler-commands/).
- [Browser localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
- [GitHub create-issue API](https://docs.github.com/en/rest/issues/issues#create-an-issue).
- [Turnstile server validation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/).
- [Interface literature](../literature_review.html#sidebar-design).

## Daily download and Claude report (owner's Mac)

Installed on this Mac as `com.cwlin4916.collaborative-generation-comments` (renamed 2026-10-19 from
`trees-comment-report`). It is separate from the `collaborative-generation` issue-mirror LaunchAgent.

```sh
# Install/reinstall the daily (07:00 local), RunAtLoad LaunchAgent; uses existing Cloudflare and Claude logins.
python3 subpages/case-study-1-ramifiedrelative/proof_graphs/collaboration/comment-service/install-comment-sync.py
# Download/report now (or double-click sync-comments.command).
python3 subpages/case-study-1-ramifiedrelative/proof_graphs/collaboration/comment-service/sync-comments.py
# Raw download and HTML only, without a model call.
python3 subpages/case-study-1-ramifiedrelative/proof_graphs/collaboration/comment-service/sync-comments.py --download-only
# Stop the schedule; keep all downloaded records and reports.
python3 subpages/case-study-1-ramifiedrelative/proof_graphs/collaboration/comment-service/install-comment-sync.py --disable
```

The permanent private directory is `~/collaborative-generation/local/comments/` (the laptop folder's
ignored `local/`; moved there from `~/Trees-comments/` on 2026-10-19):

- `index.html`: minimal offline report; search, latest report/all comments, source links, originals.
- `comments.json`: latest complete merged archive. ID merging preserves previously downloaded records.
- `report.json`: Claude summaries and suggested actions keyed by ID and original-content fingerprint.
- `archive/<UTC>.json`: raw snapshot when comments/names/receipts change.
- `reports/<UTC>.html` and `.json`: each batch report, retained across future checks.
- `sync-status.json` and `sync.log`: check result and diagnostics.

The Python helper downloads deterministically through Wrangler's existing owner login. Claude Code
receives batches of up to ten new/changed feedback records over stdin, with `--safe-mode`, no tools,
no discovered MCP servers, no session persistence and structured JSON output. `--bare` is deliberately
not used: the installed CLI requires a separate API key in bare mode, whereas this owner has a
subscription login. No credentials are added; the public `GET /comments` endpoint is not used by the sync. Comments are untrusted
input, never commands. The model can summarize but cannot edit files, browse or run shell tools.
Its response must contain exactly the supplied IDs. Summaries are escaped as text in HTML.

Raw downloads are saved before inference. Quota/auth/model failures leave original comments readable
and pending, retrying next day; successful earlier batches are retained. Unchanged comments cause
no new inference. Existing snapshots, manual exports and owner-created files are not deleted.
The process lock prevents overlapping syncs. Atomic per-file replacement keeps complete files;
D1 pagination is still not a transactional whole-database snapshot. Each existing CLI query has a
120-second timeout and each Claude batch has a 240-second timeout. Feedback files use private modes
and output inside the public repository is refused.

The LaunchAgent runs daily at 07:00 local time (`StartCalendarInterval`; since 2026-10-20, was hourly);
a run missed while asleep happens on wake. A failed download retries after 2 and 10 minutes: Wrangler's OAuth
token lasts an hour, and Cloudflare sometimes refuses a freshly refreshed one (`[code: 7403]`) for minutes,
which caused the hourly failures of 2026-10-20. `sync.log` records each attempt with the Cloudflare code. Check it if nothing
updates; reauthenticate with `npx wrangler@4 login` or `claude auth login` in Terminal if needed.
Automatic files live outside Documents because macOS blocks unapproved LaunchAgents from accessing Documents. Older manual exports are in `exports/earlier-from-Documents/`. Moving the Trees checkout requires reinstalling the job. To change the time, reinstall using
`--at HH:MM`. Claude uses the existing account's usage allowance.

The Iwahori pages now use the local shared client. Paragraph selection creates deterministic
text-derived anchors; a changed paragraph may get a new anchor, so quotes remain the saved context.
A comment whose passage can no longer be found shows as a 💬 pin at the end of its anchor block.
The whole-page target uses a namespaced key. Spherical pages retain their existing client.

Lifecycle tests (no external calls):

```sh
python3 -m unittest discover -s subpages/case-study-1-ramifiedrelative/proof_graphs/collaboration/comment-service -p 'test_sync_comments.py'
```

References: [Claude CLI automation](https://code.claude.com/docs/en/headless),
[CLI flags](https://code.claude.com/docs/en/cli-reference).

## Version-pinned online review approval

The dashboard shows the latest review and all older reviews, with matching **Open** and
**Approve** buttons. Individual review pages load the same approval controls through comments.js,
so adding controls does not change their version-pinned HTML snapshots.

Approval and revision decisions are intentionally public and require no owner key, at the owner's
request. Anyone visiting the site can save a decision; receipts do not verify the person's identity.
The private queue accepts these decisions for the exact version. Proposal registration and
application/publication progress still require the private Worker credential; visitors cannot
register arbitrary changes or mark a deployment published. The browser never receives this credential.
The Mac launcher now opens the ordinary dashboard URL.

```text
committed review + original target hashes → prepare → publish → register
                                                      ↓
reader click → keyless decision endpoint → append-only D1 receipt
                                      ↓                 ↓
                              public dashboard    private local queue
                                                        ↓
                                   begin → apply + verify → deploy
                                                        ↓
                                    verify live commit → published receipt
```

Commands, from the repo root (`S` below denotes this service directory):

```sh
python3 "$S/review-approvals.py" prepare
# Commit approval-proposals.json and publish Pages before registering:
python3 "$S/review-approvals.py" register
python3 "$S/review-approvals.py" sync
python3 "$S/review-approvals.py" begin --key review-2026-10-22
# Apply the approved proposed changes, mark their IDs addressed, verify and publish:
python3 "$S/review-approvals.py" published --key review-2026-10-22 --commit FULL_SHA --run PAGES_RUN_ID
# If application fails after begin:
python3 "$S/review-approvals.py" fail --key review-2026-10-22
```

Preparation pins committed review HTML and the original HTML files cited by the comments. It never
applies mathematical edits. The reviewed wording is in the immutable review snapshot; application
is performed by the workspace agent, not by executing reader input. A changed target blocks begin
and requires a new proposal/approval. Register checks exact live review and target bytes before
activating a new version. Do not register over an application already in progress.

D1 `approval_proposals` stores immutable version/manifest pairs and the current version for each
review. `approval_events` preserves decisions and progress with a unique request ID and server time.
Compare-and-swap on the last receipt rejects stale tabs. A repeated identical request returns its
original receipt; changed payloads cannot reuse the request ID. The public API returns active
proposals with their latest receipt; it exposes no key or credential hash.

`review-approvals.py sync` writes an atomic private `queue.json` plus history under the owner directory;
`pendingApplication` lists approved, locally matching proposals. Every daily comment sync polls it
before downloading comments, independently of Claude. The address-question skill also polls at the
start of each review/publishing run. A click does **not** start a remote editing agent: application runs
at the next workspace review/publishing session. The dashboard refreshes once per visible minute and
has a manual refresh button. It reports network failures explicitly instead of assuming approval.

States: Awaiting approval → Approved · awaiting application → Applying → Published.
Revision requested and Application failed remain visible; failure does not mark comments addressed.
Publication confirmation checks exact successful Pages run/commit, approved original bases,
changed targets, addressed comment IDs and live contents of changed site files before recording
its commit/deployment receipt. The Worker publication endpoint is owner-only; these filesystem and
GitHub checks belong to the owner CLI. Preserve the review HTML snapshot when marking status in
`reviews.json`. Commit all intended approved edits before invoking `published`.

Tests: `node --test "$S/worker.test.mjs" "$S/approvals.test.mjs"` cover authorization,
version replacement, retry receipts, stale sessions and legal publication transitions. Browser tests
must use a separate test database; never approve a real proposal merely to test the button.
