Design stale badge with replace-or-keep for projections and includes
Extend authoring model: generate alternative current version, keep or replace. Unify with pinned transclusions/pipelines; document markitect-tool gap and point at cross-repo proposal.
This commit is contained in:
parent
e2aa4045f4
commit
f6c9756b26
5 changed files with 312 additions and 3 deletions
|
|
@ -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/2026-08-12-content-substrate-exploration.md`
|
||||||
- `docs/architecture/transclusion-and-chunks.md` — page-first model; chunks as extract/transclude
|
- `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/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/capability/stage-1-cutover.md`
|
||||||
- `docs/decisions/2026-08-12-feature-cut-stage-1.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`
|
- kontextual: `docs/architecture-blueprint.md`, `asset-registry-implementation.md`, `blob-storage-content-streaming-workplan.md`, `markitect-tool-reuse-boundary.md`
|
||||||
|
|
|
||||||
|
|
@ -170,9 +170,29 @@ Notes:
|
||||||
|
|
||||||
UI: per-field overflow (✎ edit · ↻ regenerate · 🔒 lock) on cards and page chrome — unobtrusive, always available.
|
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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
268
docs/architecture/stale-derived-content.md
Normal file
268
docs/architecture/stale-derived-content.md
Normal file
|
|
@ -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
|
||||||
|
<!-- mkt:include path="src.md" selector="sections[heading=API]" -->
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
<!-- 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)
|
||||||
|
|
@ -161,8 +161,21 @@ From markitect docs themselves (not blockers for S1):
|
||||||
- Variable / conditional transclusion not yet
|
- Variable / conditional transclusion not yet
|
||||||
- Includes currently path-first; content-reference-as-include path is designed for later
|
- Includes currently path-first; content-reference-as-include path is designed for later
|
||||||
- Dependency graph invalidation for caches is planned (WP-0007 hooks)
|
- 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**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,13 @@ capabilities:
|
||||||
- id: feat.projection-regenerate-lock
|
- id: feat.projection-regenerate-lock
|
||||||
title: Per-field regenerate and lock for projections
|
title: Per-field regenerate and lock for projections
|
||||||
status: todo
|
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
|
- id: feat.content-fail-closed
|
||||||
title: Fail closed on missing binding/fetch
|
title: Fail closed on missing binding/fetch
|
||||||
status: shipped
|
status: shipped
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue