diff --git a/docs/architecture/ArchitectureBlueprint.md b/docs/architecture/ArchitectureBlueprint.md index 9b2411e..8e84676 100644 --- a/docs/architecture/ArchitectureBlueprint.md +++ b/docs/architecture/ArchitectureBlueprint.md @@ -273,6 +273,7 @@ See **[spike-checklist.md](./spike-checklist.md)** — executable evaluation tas - `docs/architecture/2026-08-12-content-substrate-exploration.md` - `docs/architecture/transclusion-and-chunks.md` — page-first model; chunks as extract/transclude - `docs/architecture/authoring-modes-and-lazy-projections.md` — top-down/bottom-up; lazy Title/Abstractor/Visual + lock +- `docs/architecture/stale-derived-content.md` — stale badge; alternative; replace|keep; includes/pipelines - `docs/capability/stage-1-cutover.md` - `docs/decisions/2026-08-12-feature-cut-stage-1.md` - kontextual: `docs/architecture-blueprint.md`, `asset-registry-implementation.md`, `blob-storage-content-streaming-workplan.md`, `markitect-tool-reuse-boundary.md` diff --git a/docs/architecture/authoring-modes-and-lazy-projections.md b/docs/architecture/authoring-modes-and-lazy-projections.md index 7eace9f..67c8863 100644 --- a/docs/architecture/authoring-modes-and-lazy-projections.md +++ b/docs/architecture/authoring-modes-and-lazy-projections.md @@ -170,9 +170,29 @@ Notes: UI: per-field overflow (✎ edit · ↻ regenerate · 🔒 lock) on cards and page chrome — unobtrusive, always available. -### Staleness (optional, unlocked derived only) +### Staleness and alternatives (default: badge + explicit choice) -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. +If `source_hash` ≠ current body/deps hash (or recipe version policy says so): + +1. Show **May be stale** on the field (and optionally on the card). +2. **Never** auto-overwrite locked or `user` values; do not auto-overwrite + unlocked `derived` without consent (default). +3. Primary actions from the badge: + - **Generate updated version** → compute **alternative** (side-by-side with **current**) + - **Keep current** → dismiss compare; optional **ack** (update fingerprint only, clear badge until inputs change again) + - **Use alternative** → **replace** stored value with alternative; refresh hash/timestamps; state → `derived` + - **Edit** / **Lock** as usual + +```text +[May be stale] + → Generate updated version + Current | Alternative + [ Keep current ] [ Use alternative ] [ Cancel ] +``` + +This is the same interaction grammar as for **pinned includes and pipeline +blocks** — see `stale-derived-content.md` and markitect-tool proposal +`proposed-stale-derived-and-include-pinning.md`. --- diff --git a/docs/architecture/stale-derived-content.md b/docs/architecture/stale-derived-content.md new file mode 100644 index 0000000..190aa58 --- /dev/null +++ b/docs/architecture/stale-derived-content.md @@ -0,0 +1,268 @@ +# 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 “Don’t 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) + +```markdown + +``` + +At render/export, markitect resolves from **current** `src.md`. +“Stale” is less about the marker and more about **viewer cache**. +Still useful: badge if **last rendered snapshot** ≠ current resolve hash +(for offline cards that show a cached excerpt). + +### B. Pinned / materialised include (snapshot in host page) + +For “user owns a frozen copy but can refresh”: + +```markdown + +…frozen markdown of the included region… + +``` + +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 + +…generated summary… + +``` + +```markdown + +| metric | value | +…table… + +``` + +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) diff --git a/docs/architecture/transclusion-and-chunks.md b/docs/architecture/transclusion-and-chunks.md index 34dd750..3047e26 100644 --- a/docs/architecture/transclusion-and-chunks.md +++ b/docs/architecture/transclusion-and-chunks.md @@ -161,8 +161,21 @@ From markitect docs themselves (not blockers for S1): - Variable / conditional transclusion not yet - Includes currently path-first; content-reference-as-include path is designed for later - Dependency graph invalidation for caches is planned (WP-0007 hooks) +- **Pinned derived snapshots + stale → alternative → replace|keep** — proposed in + markitect-tool `docs/proposed-stale-derived-and-include-pinning.md` + (coulomb product note: `stale-derived-content.md`) -Coulomb can start with **path + selector includes** and **section/region addressing**. +Coulomb can start with **path + selector includes** and **section/region addressing**, +then pin/freeze excerpts when “my version vs live source” matters. + +### Live include vs pinned snapshot + +| Mode | Behavior | +|------|----------| +| Live `mkt:include` | Always resolve current source at render | +| Pinned `mkt:derived` (proposed) | Store snapshot; badge when source/inputs change; user generates alternative and chooses replace or keep | + +Both are valid; co-creation often wants **pin + explicit refresh**. --- diff --git a/docs/capability/model-v0.yaml b/docs/capability/model-v0.yaml index c0ebf65..4bb5eaf 100644 --- a/docs/capability/model-v0.yaml +++ b/docs/capability/model-v0.yaml @@ -117,6 +117,13 @@ capabilities: - id: feat.projection-regenerate-lock title: Per-field regenerate and lock for projections status: todo + - id: feat.projection-stale-alternative + title: Stale badge with generate alternative then replace or keep + status: todo + notes: "docs/architecture/stale-derived-content.md; markitect derived slots" + - id: feat.derived-include-pin + title: Pinned include/pipeline snapshots with same stale/replace grammar + status: todo - id: feat.content-fail-closed title: Fail closed on missing binding/fetch status: shipped