coulomb-social/docs/architecture/authoring-modes-and-lazy-projections.md
tegwick e2aa4045f4 Design dual authoring modes and lazy projection fields
Document top-down and bottom-up writing, lazy title/abstractor/visual that
persist after generation, and per-field regenerate plus lock. Wire into
capability model and stage-1 notes.
2026-08-13 00:07:32 +02:00

236 lines
9.6 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.

# Authoring modes and lazy projection fields
| Field | Value |
|-------|--------|
| Date | 2026-08-13 |
| Status | design proposal (product + data model) |
| Related | page-first model, Abstractor/Title/Visual, stage-1 cut, markitect structure |
## Intent
Coulomb should make **both** styles of knowledge work feel natural:
| Mode | Flow |
|------|------|
| **Top-down** | Title → short description (abstractor) → outline → elaborate body → optional visual |
| **Bottom-up** | Plunge into body text → title / abstractor / structure / visual emerge after or while writing |
Neither mode may force the other. Empty or content-only entities must still **display** as first-class cards (title, description, illustration) without blocking the writer.
## Principles
1. **Body is never blocked** on metadata. A page with only prose is valid.
2. **Display needs projections** — card/list UI needs title, abstractor, visual when showing an entity.
3. **Projections may start virtual** — generated on first need (lazy), then **persisted as normal field values**.
4. **After persistence, fields behave like user-authored data** — editable, exportable in frontmatter, same SoR as hand-written values.
5. **Regenerate** is an explicit action, not ambient rewriting.
6. **Lock** freezes a field against automatic (and optionally against bulk) regeneration.
7. **Structure stays page-first** — outline is headings/sections in the markdown body (or a derived outline view), not a separate chunk ontology. See `transclusion-and-chunks.md`.
---
## Field set (stage-1 aligned)
| Field | Role | Display use |
|-------|------|-------------|
| **title** | Name of the entity | Card headline, browser title, lists |
| **abstractor** | Short description (Bubble: Abstractor) | Card subtitle, previews, search snippet |
| **visual** | Illustration / cover | Card face, headers |
| **body** | Main markdown content | Full page view |
| **outline** | Derived (usually) from body headings | Top-down scaffold; navigation |
`outline` is typically **not** a separate SoR blob: it is **read from** `#` / `##` structure, or optionally written back as a stub section when the user accepts a generated outline in top-down mode.
---
## Dual mode UX
### Top-down path
```text
New page
→ focus title (optional visual placeholder)
→ abstractor
→ "Generate outline" → body becomes heading skeleton
→ user elaborates under headings
→ visual optional (upload or generate)
```
- Empty body with title/abstractor is fine.
- Outline generation **writes body** (or a dedicated outline region) only when the user asks — not on every keystroke.
### Bottom-up path
```text
New page / open blank body
→ write freely
→ on first card/list display (or idle debounce): lazy-fill missing title/abstractor/visual
→ user may edit, regenerate, or lock any field
→ optional "Propose outline" restructures headings without deleting prose carelessly
```
- Creating a page must not require a title dialog.
- Autosave body continuously; projections fill **lazily on display need** or on explicit "Suggest metadata".
### Mode switching
There is **no mode toggle the user must set**. The product infers intent:
- If title/abstractor present and body empty → treat as top-down.
- If body present and title empty → bottom-up; lazy projections allowed.
- Both filled → normal edit; regenerate only on demand.
---
## Lazy projections (virtual → real)
### Lifecycle of a projection field
```text
missing
→ (display needs field) generate candidate [virtual / ephemeral]
→ persist to page frontmatter [real]
→ user edit | regenerate | lock
```
| State | Meaning |
|-------|---------|
| **absent** | No value in SoR; may generate |
| **derived** | Value present; `provenance: generated` (or equivalent); unlocked |
| **user** | Value present; last material edit was human (or import); unlocked |
| **locked** | Value fixed; no auto/lazy regenerate; explicit unlock required |
**Rule:** Once generated and saved, the value is **kept** as if the user had typed it — cards and export see real frontmatter, not a live prompt every time.
**Lazy trigger examples:**
- First render of a card in a list/deck when `title` or `abstractor` or `visual` is absent
- Opening page header chrome when fields missing
- Explicit “Fill missing metadata”
**Not triggers (by default):**
- Every keystroke in the body
- Background jobs that rewrite locked or recently user-edited fields
- Silent overwrites of `user` or `locked` values
### Generation sources (implementation-agnostic)
| Field | Cheap deterministic options | Richer options (optional) |
|-------|----------------------------|---------------------------|
| title | First H1, first non-empty line, filename/slug | LLM short title from body |
| abstractor | First paragraph, first section under H1 | LLM 12 sentence summary |
| visual | Palette placeholder from hash of id; first image in body/assets | Image model / pick from assets |
| outline | Existing headings; or insert H2 stubs from summary | LLM outline from abstractor or body |
Prefer **deterministic first** for offline/dev and predictable tests; LLM behind the same regenerate/lock contract.
### Persist shape (illustrative frontmatter)
```yaml
---
id: page-…
title: "Working title"
abstractor: "One-line sense of the page."
visual: "assets/visuals/page-….webp" # or sha256:… asset ref
projections:
title:
state: derived # absent | derived | user | locked
locked: false
generated_at: "2026-08-13T12:00:00Z"
generator: "first-h1@v1"
source_hash: "sha256:…" # body hash when generated — optional staleness hint
abstractor:
state: user
locked: false
visual:
state: locked
locked: true
generated_at: "2026-08-13T11:00:00Z"
generator: "placeholder-hash@v1"
---
Body markdown…
```
Notes:
- **Display values** live in top-level `title` / `abstractor` / `visual` so export and non-coulomb tools still see normal fields.
- **`projections.*`** holds lifecycle only (state, lock, generator, hashes) — not a second title.
- If `projections` is missing (imported Bubble/markdown), treat present fields as `user` unlocked; absent fields as `absent`.
### Actions on each field
| Action | Behavior |
|--------|----------|
| **Edit** | Set value; set state → `user`; clear stale generator metadata or keep last generator for audit |
| **Regenerate / Update** | Allowed if not locked; re-run generator; overwrite value; state → `derived`; refresh `generated_at` / `source_hash` |
| **Lock** | `locked: true`, state → `locked`; blocks regenerate and lazy auto-fill |
| **Unlock** | `locked: false`; state → `user` or `derived` (keep last provenance) |
UI: per-field overflow (✎ edit · ↻ regenerate · 🔒 lock) on cards and page chrome — unobtrusive, always available.
### Staleness (optional, unlocked derived only)
If `source_hash` ≠ current body hash, UI may show “metadata may be outdated” and offer regenerate — **never** auto-regenerate locked or `user` fields without consent. Policy for `derived` + stale can be: badge only (default) or auto-refresh if product later opts in.
---
## Interaction with page-first / transclusion
- **Outline** = structure of **this** pages body (headings), not a chunk table.
- Top-down “generate outline” writes headings into body; elaboration is normal writing under those headings.
- Bottom-up “propose outline” may reorganize into headings (explicit action; preview/diff recommended).
- A card for a **transcluded excerpt** uses the **source pages** projections unless the excerpt is promoted to its own page.
---
## Capability / feature mapping (rebuild)
Under `cap.space-content` (and any entity shown as a card):
| Feature id (proposed) | Description |
|----------------------|-------------|
| `feat.authoring-top-down` | Title/abstractor/outline-first create path |
| `feat.authoring-bottom-up` | Body-first create; no required metadata gate |
| `feat.projection-lazy-fill` | Lazy generate missing title/abstractor/visual on display |
| `feat.projection-persist` | Persist generated values into frontmatter |
| `feat.projection-regenerate` | Explicit per-field regenerate |
| `feat.projection-lock` | Per-field lock against auto/regenerate |
Stage-1: ship dual authoring + lazy title/abstractor at minimum; visual generation can start as placeholder art; LLM optional.
---
## Anti-patterns
| Avoid | Why |
|-------|-----|
| Requiring title before first save | Blocks bottom-up |
| Regenerating on every body keystroke | Noisy, fights the writer |
| Virtual-only fields that never save | Breaks export and “users data” |
| One global “AI mode” toggle for the whole page | Prefer per-field lock/regenerate |
| Separate chunk entities only to hold outline | Outline is page structure |
---
## Open product choices
1. **Default generator set** — deterministic only vs LLM when configured.
2. **Stale derived fields** — badge-only vs optional auto-refresh.
3. **Visual generation** — placeholder vs asset pipeline vs external image model.
4. **Outline write-back** — always mutate body vs ephemeral outline panel until “Apply”.
5. **Space-level projections** — same contract for space cards (title/abstractor/visual).
---
## One-liner
**Top-down and bottom-up are both first-class; display fields can be born lazy, then live as ordinary locked-or-editable metadata so tooling never blocks the writing.**
## Related
- `docs/architecture/transclusion-and-chunks.md`
- `docs/architecture/ArchitectureBlueprint.md`
- `docs/capability/stage-1-cutover.md`
- `docs/capability/existing-bubble-capability-map.md` (Title, Abstractor, Visual)