coulomb-social/docs/architecture/stale-derived-content.md
tegwick d953aed4dd Record architecture decisions: thin git plane, live default, upgrades
D1 thin dir+git with shard-wiki then kontextual upgrade path; D2 defer
asset-store; D3 NetKingdom authz consolidation; D4 outline explode/implode
and live-by-default includes with pin as exception.
2026-08-13 00:48:33 +02:00

270 lines
9.3 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.

# Stale badge, alternatives, and derived includes
| Field | Value |
|-------|--------|
| Date | 2026-08-13 |
| Status | design proposal (coulomb product + markitect-tool contract) |
| Related | authoring modes, transclusion, markitect include/provenance |
## Intent
Any **derived** content — projection fields (title, abstractor, visual),
**transclusions/includes**, or outputs of **rules / external data / research /
pipelines / classic compute / AI** — can become **stale** relative to its inputs.
The product (and markitect-tool) should:
1. **Detect** staleness via input fingerprints (content hashes, dependency set).
2. **Surface** a clear **“may be stale”** badge (never silent rewrite by default).
3. Let the user **generate an alternative** (more current candidate).
4. Let the user **replace** the stored version **or keep** it (and optionally lock).
Same interaction grammar everywhere: projections, frozen include snapshots,
and pipeline-produced blocks.
---
## What markitect-tool has today (gap analysis)
| Building block | Status |
|----------------|--------|
| Include / transclusion markers + `resolve_includes` | **Yes** |
| Selectors / partial include | **Yes** |
| Operation provenance on include/transform (path, selector, deps metadata) | **Yes** (partial) |
| Content hashes on units / explode entries / processing provenance | **Yes** (scattered) |
| Cache fingerprint + `mkt cache status` (file changed?) | **Yes** |
| Backend `refresh-plan` (snapshot vs files) | **Yes** (backend fabric) |
| Context package **refresh** recipes | **Yes** (agent memory) |
| **Stale badge semantics** for a *stored* derived snapshot vs live resolve | **No** productized UX contract |
| **Generate alternative → replace or keep** for includes/projections | **No** |
| **Pinned / locked** include snapshot in the document | **No** first-class marker |
| Unified “derived block” model (rule, AI, external, include) | **No** — needs design |
**Conclusion:** markitect has **plumbing for change detection and provenance**,
not yet a **unified stale → alternative → replace|keep** workflow for
in-document derived content. That belongs in markitect-tool as a reusable
contract; coulomb UI implements the badge and choices.
Cross-repo proposal:
`markitect-tool/docs/proposed-stale-derived-and-include-pinning.md`
---
## Unified model: derived slot
A **derived slot** is any value that:
- has a **current stored representation** (frontmatter field, pinned body region,
or last accepted snapshot), and
- has a **recipe** that can recompute a candidate from inputs.
```text
DerivedSlot
id
kind: projection | include | pipeline | external | ai | rule
stored_value # what readers see by default
recipe # how to recompute
input_fingerprint # hashes of inputs when stored_value was accepted
state: derived | user | locked
locked: bool
generated_at
generator / recipe_version
```
**Stale** ⇔ recompute `current_fingerprint(inputs)``input_fingerprint`
(or a dependency file is missing / recipe version bumped if policy says so).
### User actions when stale (or anytime if unlocked)
| Action | Result |
|--------|--------|
| **Keep** | Leave `stored_value`; optionally refresh fingerprint (“accept as current”) or leave stale badge |
| **Generate alternative** | Run recipe → **candidate** (side-by-side; does not overwrite yet) |
| **Replace with alternative** | `stored_value ← candidate`; update fingerprint; state → `derived` |
| **Edit stored** | Manual edit; state → `user` |
| **Lock / Unlock** | Block regenerate and auto-refresh |
Default policy: **never auto-replace** locked or `user` slots;
**never auto-replace** stale `derived` without consent (badge + actions only).
---
## Coulomb: projection fields
Extends `authoring-modes-and-lazy-projections.md`.
When `projections.title.source_hash` (etc.) ≠ current body/deps hash:
1. Show **May be stale** on the field/card.
2. Actions: **Generate updated version** · **Keep current** · **Edit** · **Lock**.
3. **Generate** opens compare UI:
```text
┌ Current (stored) ┌ Alternative (fresh)
│ … │ …
└──────────────────────┴──────────────────
[ Keep current ] [ Use alternative ] [ Cancel ]
```
4. **Use alternative** writes frontmatter and updates `source_hash` / `generated_at`.
5. **Keep current** dismisses compare; optional “Dont warn until body changes again”
= update fingerprint only (`ack_stale`) without changing text.
Same for abstractor and visual (visual compare: two images or image vs placeholder).
---
## Includes / transclusions
Two legitimate modes (both needed):
### A. Live resolve (dynamic) — **product default**
```markdown
<!-- mkt:include path="src.md" selector="sections[heading=API]" -->
```
At render/export, markitect resolves from **current** `src.md`
(**spreadsheet-like**: formula results stay up to date).
“Stale” for pure live includes is mostly **viewer/cache** (optional badge if
a cached card excerpt lags live resolve). No replace/keep needed unless the
host cached a display snapshot.
### B. Pinned / materialised include (snapshot in host page) — **exception**
For “freeze this excerpt / paste values” (explicit pin):
```markdown
<!-- mkt:derived id="api-excerpt"
kind="include"
path="src.md"
selector="sections[heading=API]"
input_hash="sha256:…"
locked="false"
generator="include@v1" -->
…frozen markdown of the included region…
<!-- /mkt:derived -->
```
Or shorter pin attributes on include + adjacent snapshot region.
| Event | Behavior |
|-------|----------|
| Source file changes | Slot becomes **stale** (hash mismatch) |
| User generates alternative | Resolve include live → candidate body |
| Replace | Write new snapshot into the derived region; update `input_hash` |
| Keep | Leave snapshot; optional ack |
| Lock | No refresh until unlock |
**Live vs pinned:** product default for co-creation cards may be **pinned**
(exportable, offline, “my version of this excerpt”); docs/wiki sites may prefer
**live**. Coulomb should support both; stage-1 can start with live resolve for
view and add pin when “freeze excerpt” is needed.
### Pipeline / AI / external derived blocks
Same `mkt:derived` envelope:
```markdown
<!-- mkt:derived id="summary-1"
kind="ai"
recipe="summarize@v2"
inputs="body"
input_hash="sha256:…"
locked="false" -->
…generated summary…
<!-- /mkt:derived -->
```
```markdown
<!-- mkt:derived id="metrics"
kind="pipeline"
recipe="research.pull@v1"
inputs="config/query.yaml,data/…"
input_hash="sha256:…" -->
| metric | value |
…table…
<!-- /mkt:derived -->
```
Kinds (open set): `include` | `rule` | `pipeline` | `external` | `ai` | `compute` | `projection`.
---
## markitect-tool contract (proposed)
Expose library APIs (names illustrative):
```text
fingerprint_inputs(recipe, context) -> str
is_stale(slot, context) -> bool
evaluate_recipe(recipe, context) -> Candidate # markdown + provenance
diff_or_pair(stored, candidate) -> Comparison
apply_replace(document, slot_id, candidate) -> document
apply_keep(document, slot_id, *, ack_fingerprint: bool) -> document
```
CLI sketches:
```bash
mkt derived status page.md # list slots + stale?
mkt derived refresh page.md --id api-excerpt --dry-run # print candidate
mkt derived refresh page.md --id api-excerpt --replace
mkt derived refresh page.md --id api-excerpt --keep --ack
mkt derived lock page.md --id summary-1
```
Include path should record **target content hash** in provenance today;
stale detection reuses cache fingerprints + per-slot `input_hash`.
Implementation can land incrementally in markitect-tool (see proposal doc);
coulomb consumes the API for badges and compare UI.
---
## UX summary
```text
[May be stale]
→ Generate updated version
→ side-by-side Current | Alternative
→ Keep current (optional: clear badge / ack)
→ Use alternative (replace stored)
→ Cancel
→ Lock field/block
```
Applies to: title, abstractor, visual, outline stub, pinned includes,
AI/pipeline regions — **one mental model**.
---
## Relation to lock
| | Unlocked derived | Locked | User-edited |
|--|------------------|--------|-------------|
| Stale badge | yes if fingerprint mismatch | yes if mismatch (informational) | optional / policy |
| Generate alternative | yes | no (must unlock) | yes (warns overwrite of user text) |
| Auto-replace | never (default) | never | never |
---
## Stage-1 vs later
| Slice | When |
|-------|------|
| Stale badge + regenerate/replace/keep for **title/abstractor** | Stage-1 authoring |
| Visual same | Stage-1 if visual exists |
| Live include resolve only | With markitect integration |
| Pinned `mkt:derived` include snapshots | Soon after includes land |
| Full pipeline/AI recipes | As generators are added |
---
## Related
- `docs/architecture/authoring-modes-and-lazy-projections.md`
- `docs/architecture/transclusion-and-chunks.md`
- markitect-tool: `docs/transform-compose-include.md`, `docs/cache-incremental.md`, `docs/backend-fabric.md`
- markitect-tool: `docs/proposed-stale-derived-and-include-pinning.md` (proposal for implementation there)