coulomb-social/docs/migration/corpus-import.md
tegwick 0881b9896a
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 2m43s
Plan accepted-space date corrections and bind routine recovery to scoped APIs
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a17f16-d99c-7ea3-95f0-8212897fba6b
2026-10-28 01:33:08 +01:00

291 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Local corpus import and recovery prerequisites
`import_content_corpus` imports prepared ADR-0004 trees. It does not contact
Bubble, provision accounts or verify the completeness of a Bubble export.
Use private local trees and manifests outside the checkout. Never commit member
mappings, corpus content, DB dumps or backup archives.
## All-space order and original dates — 2026-10-27
The user specifies eventual transfer of **all** coulomb.social spaces, using
deterministic tooling and newest space creation first (LIFO guidance). Preserve
creation dates of spaces, pages and every nested chunk. Source modification
date, export time and filesystem mtime must not determine this order.
Plan an existing private capture without contacting Bubble or an inference service:
```bash
uv run python scripts/plan-bubble-transfer.py /private/capture --output /private/new-plan.json
```
Input is the exporter's source-ID-keyed `spaces.json` and `articles.json`.
The new output is mode 0600 and refuses overwrite; its parent directory should
be private. It contains private IDs and normalized/raw dates, no article bodies
or identities. Input hashes bind it to the exact capture. Every space appears,
sorted by creation descending with a stable ID tie-breaker. Missing dates sort
last and are flagged. Recursively follow `chunks_list_custom_article`, including
children without their own space reference; cycles do not duplicate records.
Report missing children/parents, foreign-space chunks, missing bodies/dates and
unassigned captured articles. Never fabricate dates from Bubble IDs or import time.
`captured_graph_complete` means only that those checks pass for captured records.
It does not prove pagination/source completeness, asset/link fidelity, original
access rules, approved grants or productive acceptance. Fresh source capture,
all-tree conversion and per-space asset/link reconciliation remain required.
The August retained inventory has 71 spaces/109 articles, with 11 incomplete
captured graphs: 16 missing chunk references, 29 missing parent references and
one space missing both creation/modification dates. All remain in the plan.
Prepared Markdown uses `source_created_at` for each space index/page/chunk.
The importer accepts timezone-aware ISO values or Bubble epoch milliseconds,
rejects invalid present dates before writes, copies page/chunk metadata unchanged,
and restores the space's SQL `created_at` (including repeat import). Normal edits
retain page/chunk source metadata. Bubble-marked trees require original creation
dates for the space index and every page/chunk before any writes; the importer refuses to synthesize an index
with an unknown original space date. Generic hand-authored, non-Bubble trees
can still omit source metadata.
The capture exporter now includes original space dates in its inventory/index,
alongside the existing original article dates.
When refreshing an older inventory, add
`--inventory-spaces /private/old-capture/spaces.json` to the planning command.
It keeps every known space queued, including absent records, explicitly marked
`source_record_fresh: false` and `space_not_refreshed`. Retained timestamps may
guide ordering; they never certify current source completeness or transfer readiness.
After renewing the private Bubble session with `scripts/capture-auth-state.sh`,
the read-only capture can discover every visible space root instead of using
the historical fixed space-name list:
```bash
COULOMB_SOCIAL_ALL_SPACES=1 COULOMB_SOCIAL_CORPUS_DIR=/private/new-capture \
node scripts/export-bubble-corpus.mjs
```
This mode never seeds old responses, ranks discovered roots by their captured
space creation date, retains unknown dates last and records each visited/missing/
failed root in the private index. It still uses bounded UI scrolling and does
not certify pagination/source completeness. The source JSON capture covers all
visited roots; the legacy exporter still produces only one sample tree. Run the
planner on the JSON and reconcile missing references before all-tree conversion;
do not interpret that one sample tree as migration of the entire capture.
Optionally set `COULOMB_SOCIAL_ORDER_SPACES_FILE=/private/old-capture/spaces.json`
to rank by retained original creation dates before every space's fresh metadata
has been fetched. That file is used for ordering only, with its hash recorded;
older articles/bodies are never seeded in all-space mode. Private source JSON
and `capture-state.json` checkpoints preserve partial successful capture. Three
consecutive navigation failures stop the pass with explicit private step diagnostics.
Bubble's hidden root anchors identify the correct card; capture clicks its visible
title rather than the hidden anchor. A visited card is not pagination proof.
Resume a partial capture through explicit missing references, without another full
space walk or using older article bodies:
```bash
node scripts/capture-bubble-references.mjs --capture /private/checkpoint \
--inventory-spaces /private/old-capture/spaces.json --output /private/new-reference-pass
```
This follows only source-referenced article/root addresses, including missing
chunk/parent records, absent bodies/dates and unrefreshed space roots. Input hashes,
attempts and unresolved references remain in private checkpoints. Output must be
new and outside the repository. Response reads are bounded; a completed pass is
not a completeness claim. Plan its resulting source files again. The October27
pass captured 70 fresh spaces/162 articles; all 71 known spaces remain represented,
with 67 captured graphs passing and the remaining source/ownership gaps explicit.
Prepare **all** captured trees and a private reconciliation bundle offline:
```bash
uv run python scripts/prepare-bubble-bundle.py /private/checkpoint \
--inventory-spaces /private/old-capture/spaces.json --output /private/new-review-bundle
```
The output directory must be new and outside the checkout. Its mode is 0700;
files are 0600. `reconciliation.json` is written last: without that completed
receipt the output is incomplete. Capture hashes and exact input snapshots bind
the bundle to the source. Every known space remains queued in LIFO order.
Only spaces whose captured graph checks pass receive `review-trees/`; blocked
spaces retain their gaps. `source-bodies/` preserves every captured text body
exactly as UTF-8, including blocked/unassigned records and leading/trailing
whitespace. Source-ID-derived slugs remain stable across label changes and
subsequent captures. These are proposed destinations; reconcile the already
accepted cohort and its local edits/copies/aliases before using any mapping.
Each draft index preserves the **space** creation date and links to a separate
source root article. Each article/chunk preserves its own creation/modification
dates and parent/chunk/contents references; ordered lists retain duplicates.
The private report includes per-page body/file hashes and raw/UTC dates. The
root article's birthdate never substitutes for the space's birthdate.
The bundle inventories ordinary Markdown inline/reference links and images,
HTML link/resource attributes and source Visuals, without fetching URLs or
rewriting bodies. Internal Bubble article links receive candidate destinations;
ambiguous, absent, queried and fragment targets remain for reconciliation.
External URLs, local resources, unsupported HTML/style/srcset, attachment flags
and uncaptured `contents_list_custom_article` relations remain explicit review
items. This is **not an exhaustive parser for custom Bubble conventions**, an
asset download, or proof of rendering/relationship fidelity. Source capture can
now follow missing contents relations as well as chunks/parents; their product
semantics require review before conversion.
No import manifest or membership mapping is generated. Every space remains
`import_ready: false` until source completeness, assets/links, rendering/workflows,
existing destinations and explicit tenant/identity/grants are reconciled. Keep
all drafts private. Do not feed the whole bundle to `import_content_corpus` or
replace a live tree to backfill dates.
Prepare private source/product rendering cards without contacting Bubble:
```bash
uv run python scripts/audit-bubble-rendering.py /private/checkpoint/articles.json \
--output /private/new-rendering-review
```
The tool invokes the actual product renderer under integration-disabled test
settings. It records body/render/card hashes and structural sanitizer changes,
retains missing-body records, and writes private `index.html`/cards plus a final
`audit.json` completion receipt. Output is new, outside the checkout, mode 0700
with files mode 0600. Samples have a restrictive resource policy and inert body
links/forms. The original Bubble view opens only through an explicit review link.
An unchanged sanitizer structure is not an original-rendering comparison or
productive fidelity acceptance. Use the UC-003–006 review walkthrough for verdicts.
## Existing accepted content: date reconciliation without replacement
Use `scripts/plan-source-date-backfill.py` for an explicitly selected **existing**
destination. It plans corrections; it has no execution/import mode:
```bash
uv run python scripts/plan-source-date-backfill.py /private/fresh-capture \
--observation /private/current-date-observation.json \
--selection /private/accepted-space-selection.json --output /private/new-date-plan.json
```
The private observation schema is `coulomb.source-date-observation.v1`: `spaces`
contains observed SQL `id`, `slug`, `tenant_id`, `created_at`, and `pages` containing
`slug`, full-file `sha256` and source frontmatter `meta`. Capture it through an
authorized read-only application context without emitting bodies/identities into
logs. This observation is not a quiesced recovery point. The selection must pin
exactly `space_id`, `space_slug`, `tenant_id`, `source_space_id`; neither a title
nor source provenance on a copied page is authority to select a destination.
The planner normalizes old typed/prefixed Bubble IDs, binds all four inputs by
hash and refuses mismatched SQL/tenant/slug/index/source identities. It compares
article creation dates but never proposes article changes, including when a
copy retains duplicate provenance. Local pages remain excluded. Existing index
date conflicts/malformed values and source/article date gaps require review.
For a missing index field it proposes **inserting only** `source_created_at`,
preserving every existing metadata field and body byte. SQL changes target only
the selected space's `created_at`, with its observed value as a precondition.
Output is a new mode-0600 JSON outside the checkout. It contains private IDs,
hashes and proposed values; stdout contains counts only. Every plan remains
`ready_to_apply: false`: apply requires a fresh paired rollback/recovery point,
quiesced writers, live file-hash/SQL preconditions and verified postflight. Never
use tree replacement/reimport to perform this backfill.
The October28 accepted-space observation found 36 imported article dates already
matching fresh source, plus two preserved local pages. Its plan has only the SQL
space date correction and missing index field insertion; no productive writes.
Counts/hashes: `docs/release/accepted-source-date-reconciliation-2026-10-28.json`.
Packaged SQL migration rehearsal (internal disposable PostgreSQL, no production):
```bash
uv run python scripts/rehearse-storage-migration.py \
--app-image forgejo.coulomb.social/coulomb/coulomb-social:c11300a --packaged-source
```
Packaged mode uses the image's application and Django; only test tools are copied
into a read-only mount. Its test database/container/network are disposable.
## Manifest
Version 1 JSON. Tree paths are relative to the manifest directory or absolute.
Every space requires an existing active member mapped as owner. Identity is
matched by **tenant + issuer + subject**, never email or a password.
```json
{
"version": 1,
"spaces": [
{
"tree": "trees/example",
"slug": "example",
"tenant": "tenant:example",
"memberships": [
{"issuer": "https://issuer.example", "subject": "existing-subject", "role": "owner"}
]
}
]
}
```
Roles: `owner`, `member`, `viewer`. Product authorization denies writes for viewer grants and permits
owner/member writes; reads require an explicit membership or creator link.
Tenant-wide bootstrap visibility has been removed. The manifest supplies owner
and explicit grants, but does not certify that they match original Bubble
privacy. Review the effect on existing spaces before executing a real cohort migration.
## Run
First provision accounts through NetKingdom and have users sign in to create
local Members. Reconcile the corpus and approved grants. Pause all application
writers; run with the deployment tenant's private CONTENT_ROOT and DB settings.
```bash
uv run manage.py import_content_corpus /private/path/manifest.json --dry-run
uv run manage.py import_content_corpus /private/path/manifest.json
# Explicit replacement only after reviewing the existing content and backup:
uv run manage.py import_content_corpus /private/path/manifest.json --replace
```
Preflight validates **all** entries before any writes: local pages, normalized
filenames, metadata bounds, source symlinks, source/destination overlap,
destination collisions, active identity/tenant mapping and at least one owner.
A missing owner or identity in the last tree prevents the first tree being
written. Dry-run performs that same preflight and creates no directories or rows.
Execution stages each tree beside its destination, then applies metadata and
memberships and publishes by rename inside a DB transaction. Ordinary errors
restore the prior tree and roll back that space's DB changes. **The whole batch
is not atomic**: previously committed spaces remain if a later space fails.
Receipts give the completed count. Reconcile local state and either import a
manifest containing only remaining entries, or deliberately use `--replace`.
Replacement removes the previous content tree, including assets not in source;
it adds/updates mapped memberships and **does not revoke** other existing grants
or overwrite a pre-existing creator. Review those extra accesses explicitly.
Writers must remain paused. A process crash, rename/DB commit race or failed
recovery filesystem operation requires manual recovery; this is not a crash-safe
journal or concurrent import protocol. Keep content-root and instance-per-client
tenant boundaries intact: filesystem paths remain slug-keyed.
The single-tree command shares validation and staging. `--member-subject` must
resolve to exactly one tenant-local Member; use the corpus manifest when issuer
ambiguity is possible. Single-tree imports may still omit membership,
in which case the space is hidden unless it has an existing membership/creator
grant. They are not a replacement for approved cohort mapping.
## Paired backup/restore protocol (acceptance still pending)
1. Quiesce app and import writers, including Forgejo refresh consumers where
relevant. Record recovery point, exact image SHA, migration level and tenant.
2. Through the DB owner's documented credential lane, capture metadata DB;
separately snapshot/archive the **entire** CONTENT_ROOT (pages and assets,
including archived trees). Keep private data in the approved backup store.
3. Record SHA-256 file inventory, row counts and backup identities privately.
Do not log credentials, content bodies or identity lists into this repo/hub.
4. Restore DB and CONTENT_ROOT together into an isolated instance. Disable
outgoing integrations and use restore-specific settings and credentials.
5. Compare Space/Member/membership counts, file hashes, page list/render, visual
assets, copied/moved content and denied reads by an ungranted member. Measure
achieved recovery time and recovery-point age; record the non-secret receipts.
6. Resume writers only after accepted recovery checks. If import fails, restore
both stores from that same recovery point rather than only rolling back DB.
This is an operational protocol, **not** evidence that scheduled backup or a
restore drill exists. CSOC-WP-0008-T02 owns automation and measured acceptance;
T01 owns storage admission, T03 the actual private cohort rehearsal.