Merge loops/content into architecture: content-on-sx CMS on Smalltalk
Block-based documents as message-passing on Smalltalk-on-SX: typed block objects, ordered tree, render boundary (html/sx/md/text), persist op-log + versioning, flat + nested-tree CvRDT with durable replication, Ghost sync + trust-gated federation, plus extensions (tables/callouts/media, deep tree edits, data/wire serialization, query/transform, TOC/outline, page wrappers). 746/746 across 41 suites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ injected adapter, not core.
|
||||
|
||||
## Status (rolling)
|
||||
|
||||
`bash lib/content/conformance.sh` → **0/0** (not yet started)
|
||||
`bash lib/content/conformance.sh` → **746/746** (Phases 1–4 COMPLETE + ~34 extensions, hardened: HTML/SX escaping, Markdown render + import/export incl. tables & frontmatter (full round-trip), CvRDT flat + nested-tree + durable replication, tree-aware validation, snapshot cache, doc metadata, plain-text render, nested block trees + deep editing + flatten + relative reorder, doc stats + summary + multi-doc index, table + callout + media blocks, HTML page wrapper + SEO page, doc composition + id-remap, portable data + wire serialization, block query + transforms + find/replace, TOC + anchored headings + outline, normalization)
|
||||
|
||||
## Ground rules
|
||||
|
||||
@@ -57,26 +57,368 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─
|
||||
```
|
||||
|
||||
## Phase 1 — Block document model
|
||||
- [ ] `block.sx` — typed block objects
|
||||
- [ ] `doc.sx` — ordered tree, apply edit op, structural moves
|
||||
- [ ] `render.sx` — block tree → HTML/SX
|
||||
- [ ] `api.sx` + tests + scoreboard + conformance.sh
|
||||
- [x] `block.sx` — typed block objects
|
||||
- [x] `doc.sx` — ordered tree, apply edit op, structural moves
|
||||
- [x] `render.sx` — block tree → HTML/SX
|
||||
- [x] `api.sx` + tests + scoreboard + conformance.sh
|
||||
|
||||
## Phase 2 — Op log + versioning
|
||||
- [ ] edit ops as `persist` events; replay to any version
|
||||
- [ ] `(content/history doc)`, diff between versions
|
||||
- [x] edit ops as `persist` events; replay to any version
|
||||
- [x] `(content/history doc)`, diff between versions
|
||||
|
||||
## Phase 3 — Collaborative merge (CRDT)
|
||||
- [ ] commutative/idempotent op merge
|
||||
- [ ] concurrent-edit tests (any order, double-apply → identical)
|
||||
- [x] commutative/idempotent op merge
|
||||
- [x] concurrent-edit tests (any order, double-apply → identical)
|
||||
|
||||
## Phase 4 — External sync + federation
|
||||
- [ ] Ghost/CMS sync via injected adapter (import/export)
|
||||
- [ ] federated documents (peer-authored blocks) — trust-gated stub
|
||||
- [ ] tests: round-trip import/export, conflict on concurrent external edit
|
||||
- [x] Ghost/CMS sync via injected adapter (import/export)
|
||||
- [x] federated documents (peer-authored blocks) — trust-gated stub
|
||||
- [x] tests: round-trip import/export, conflict on concurrent external edit
|
||||
|
||||
## Extensions (post-roadmap)
|
||||
- [x] HTML escaping at the render boundary (`String>>htmlEscaped`: & < > ")
|
||||
- [x] asSx wire string-escaping (`String>>sxEscaped`: \ and " in SX literals)
|
||||
- [x] Markdown render mode (`asMarkdown:` / `content/render doc "md"`)
|
||||
- [x] durable CRDT replication (`crdt-store.sx`: ops on persist, replay + converge)
|
||||
- [x] document validation (`validate.sx`: ids, per-type fields, duplicate ids; tree-aware — descends into sections, tree-wide dup ids, section field check)
|
||||
- [x] Markdown import adapter (`md-import.sx`: text → blocks, round-trips export; incl. pipe tables + frontmatter → metadata)
|
||||
- [x] Markdown doc export (`md-doc.sx`: content/markdown-doc, frontmatter from metadata, full round-trip)
|
||||
- [x] snapshot cache over replay (`snapshot.sx`: cache-not-primary, transparent)
|
||||
- [x] document metadata (`meta.sx`: title/slug/tags + Ghost title plumbing)
|
||||
- [x] plain-text render + excerpt (`text.sx`: asText, content/excerpt)
|
||||
- [x] nested block trees (`section.sx`: CtSection container, recursive render, deep-find)
|
||||
- [x] document statistics (`stats.sx`: word/char/block counts, reading time)
|
||||
- [x] table block (`table.sx`: CtTable, renders html/sx/text/md, validated)
|
||||
- [x] callout block (`callout.sx`: CtCallout note/warning/tip, renders html/sx/text/md, validated)
|
||||
- [x] media block (`media.sx`: CtMedia video/audio, renders html/sx/text/md, validated)
|
||||
- [x] list-card summary (`summary.sx`: content/summary — title/excerpt/words/reading/cover)
|
||||
- [x] multi-doc index (`index.sx`: content/index + index-by-tag + all-tags + has-tag?)
|
||||
- [x] nested-tree CvRDT (`crdt-tree.sx`: parent-aware, sections merge collaboratively)
|
||||
- [x] HTML page wrapper (`page.sx`: content/page, escaped title from metadata)
|
||||
- [x] SEO page (`page-full.sx`: content/page-full, lang + meta description from excerpt)
|
||||
- [x] document composition (`compose.sx`: concat/prepend/concat-all/wrap-section)
|
||||
- [x] deep tree editing (`tree-edit.sx`: doc-deep-update/replace/delete/insert-into)
|
||||
- [x] id remapping / clone (`clone.sx`: content/remap-ids + prefix-ids, collision-free compose)
|
||||
- [x] block query + TOC (`query.sx`: content/select/select-type/count-type/headings)
|
||||
- [x] block transforms (`transform.sx`: content/map-blocks/map-type/set-field-on)
|
||||
- [x] TOC rendering (`toc.sx`: content/toc-markdown + toc-html from headings)
|
||||
- [x] anchored-heading render (`anchor.sx`: content/html-anchored, functional TOC links)
|
||||
- [x] document outline (`outline.sx`: content/outline, nested heading tree)
|
||||
- [x] document flatten (`flatten.sx`: content/flatten, un-nest sections; inverse of wrap-section)
|
||||
- [x] relative reorder (`move.sx`: content/move-before/after/to-front/to-back by id)
|
||||
- [x] document normalization (`normalize.sx`: content/normalize, drop empty blocks/sections)
|
||||
- [x] global find/replace (`find-replace.sx`: content/find-replace across text-bearing blocks)
|
||||
- [x] portable data serialization (`data.sx`: content/to-data + from-data, round-trips tree)
|
||||
- [x] wire serialization (`wire.sx`: content/to-wire + from-wire, SX-text on the wire)
|
||||
|
||||
## Progress log
|
||||
(loop fills this in)
|
||||
|
||||
- 2026-06-07 — Hardening: audit confirmed the persist op-log (store.sx) carries
|
||||
every block type through commit → replay (op-insert carries the block
|
||||
instance; updates apply by id). Locked with +4 store tests (callout/media
|
||||
insert + update via the durable log). No bug; coverage gap closed. Suite
|
||||
746/746.
|
||||
- 2026-06-07 — Hardening: tree-CRDT orphan reparenting. Concurrent
|
||||
delete-section + insert-child previously orphaned the child (its parent no
|
||||
longer a live section) → silently dropped from materialise. Fixed
|
||||
`crdt-tree-materialize`/`crdt-tree-order` to root any element whose parent is
|
||||
"" OR not a live section, so content is never lost on concurrent edits. +4
|
||||
tests (orphan survives, commutes, content preserved, renders at root). Suite
|
||||
742/742.
|
||||
- 2026-06-07 — Hardening: regression suite `crdt-blocks` (7 tests) locking that
|
||||
non-core block types (callout/table/media/section) survive both the flat and
|
||||
nested-tree CvRDT materialise paths (insert → merge → materialise → render),
|
||||
the integration the ct-class-for-type fix repaired. Verified flat + tree,
|
||||
including concurrent mixed-type inserts into a section converging. Suite
|
||||
738/738.
|
||||
- 2026-06-07 — Hardening: fixed `ct-class-for-type` (block.sx) to map all block
|
||||
tags (added section/table/callout/media). Latent bug: `content/from-data` and
|
||||
CRDT materialise of callout/media blocks failed with "unknown block type" (they
|
||||
fell through to `mk-block`, which only knew the original 8 types). Now all block
|
||||
types build uniformly via mk-block; data/wire/CRDT round-trips of callout/media
|
||||
work. +4 data regression tests; full no-regression gate over the foundational
|
||||
block.sx change: suite 731/731.
|
||||
- 2026-06-07 — Extension: nested-tree CvRDT (`crdt-tree.sx`). Extends the flat
|
||||
CvRDT to a TREE: each element carries a `parent` (containing section id, "" =
|
||||
root) beside its Logoot pos; merge reuses crdt.sx's pos/register/field joins +
|
||||
parent (immutable). Materialisation rebuilds the ordered tree (root + per-section
|
||||
children sorted by pos, recursive). Sections now merge collaboratively; proven
|
||||
commutative/associative/idempotent — same- and different-parent concurrent
|
||||
inserts converge, nested sections, LWW, two-replica convergence. Reuses crdt.sx
|
||||
+ section.sx; flat crdt untouched (34/34). 17 tests; suite 727/727. This was
|
||||
the flagged "research-grade" gap — done as a clean self-contained layer.
|
||||
- 2026-06-07 — Extension: multi-document index (`index.sx`). `content/index`
|
||||
projects a doc list into summary cards (blog index); `content/index-by-tag`
|
||||
filters by tag (category pages); `content/all-tags` is a deduped tag cloud;
|
||||
`content/has-tag?`. Composes content/summary + doc metadata. 13 tests; suite
|
||||
710/710.
|
||||
- 2026-06-07 — Extension: list-card summary (`summary.sx`). `content/summary`
|
||||
returns `{:id :title :excerpt :words :reading-minutes :cover}` for index/listing
|
||||
cards, composing metadata + text + stats + query (`content/cover` = first
|
||||
image's src). Title falls back to id. 14 tests; suite 697/697.
|
||||
- 2026-06-07 — Extension: video/audio media block (`media.sx`). `CtMedia` holds
|
||||
kind (video/audio) + src; answers asHTML (`<video/audio src controls>`,
|
||||
escaped), asSx, asText (""), asMarkdown: (`[kind](src)`). `mk-media`/`mk-video`/
|
||||
`mk-audio`/`media?`/`media-kind`. validate.sx gained a `media` case (kind ∈
|
||||
{video,audio}). Fits the platform's media focus. 15 tests; suite 683/683.
|
||||
- 2026-06-07 — Extension: relative block reorder (`move.sx`).
|
||||
`content/move-before` / `content/move-after` move a top-level block to just
|
||||
before/after another by id; `content/move-to-front` / `move-to-back` too. More
|
||||
ergonomic than index-based doc-move; no-op on missing ids; immutable. 11 tests;
|
||||
suite 668/668.
|
||||
- 2026-06-07 — Extension: callout/admonition block (`callout.sx`). `CtCallout`
|
||||
holds kind (note/warning/tip) + text; answers asHTML (`<aside class="callout
|
||||
callout-KIND">`, escaped), asSx, asText, asMarkdown: (`> **kind:** text`).
|
||||
Self-contained; `mk-callout`/`callout?`/`callout-kind`. validate.sx gained a
|
||||
`callout` field case. 12 tests; suite 657/657.
|
||||
- 2026-06-07 — Extension: document flatten (`flatten.sx`). `content/flatten`
|
||||
un-nests a sectioned doc into a flat block sequence (each section replaced
|
||||
inline by its recursively-flattened children, wrapper dropped) — the inverse of
|
||||
content/wrap-section, for flat export targets. Inline tree handling; immutable.
|
||||
10 tests; suite 645/645.
|
||||
- 2026-06-07 — Extension: nested document outline (`outline.sx`).
|
||||
`content/outline` builds a hierarchical heading tree from content/headings —
|
||||
each node `{:id :text :level :children}`, headings nesting under the nearest
|
||||
lower-level heading (recursive forest build). The structured companion to the
|
||||
flat TOC for nested nav. 14 tests; suite 635/635.
|
||||
- 2026-06-07 — Extension: anchored-heading render (`anchor.sx`).
|
||||
`content/html-anchored` renders like asHTML but headings carry `id="<block-id>"`
|
||||
(tree-wide, sections recurse, text escaped), so the TOC's `#id` links resolve —
|
||||
completing the TOC feature end-to-end. A separate render; plain asHTML
|
||||
unchanged. 6 tests; suite 621/621.
|
||||
- 2026-06-07 — Extension: global find/replace (`find-replace.sx`).
|
||||
`content/find-replace` replaces every occurrence of a substring in the text
|
||||
field of text/heading/code/quote blocks tree-wide (via the transform layer) —
|
||||
rename a term throughout a doc. Leaves non-text fields (image alt/src) alone;
|
||||
immutable, case-sensitive. 10 tests; suite 615/615.
|
||||
- 2026-06-07 — Extension: document normalization (`normalize.sx`).
|
||||
`content/normalize` drops empty text blocks and empty sections tree-wide;
|
||||
sections are normalised first so one emptied by the pass is itself removed.
|
||||
For tidying imported/edited docs; non-text empties (dividers, blank-alt images)
|
||||
preserved. Inline tree handling; immutable. 11 tests; suite 605/605.
|
||||
- 2026-06-07 — Extension: table-of-contents rendering (`toc.sx`).
|
||||
`content/toc-markdown` produces a Markdown bullet list indented by heading
|
||||
level with `[text](#id)` links; `content/toc-html` produces a `<ul>` of escaped
|
||||
anchor links (`#id`). Built from `content/headings`; the blog page's TOC
|
||||
artifact. 8 tests; suite 594/594.
|
||||
- 2026-06-07 — Extension: tree-wide block transforms (`transform.sx`). The write
|
||||
counterpart to query: `content/map-blocks` (predicate) / `content/map-type` /
|
||||
`content/set-field-on` apply a function to every matching block across the tree
|
||||
(sections rebuilt), for bulk edits (cdn src rewrites, heading-level bumps, text
|
||||
sanitisation). Inline tree rebuild (no section.sx dep); immutable. 12 tests;
|
||||
suite 586/586.
|
||||
- 2026-06-07 — Extension: block query + TOC (`query.sx`). `content/select`
|
||||
(predicate) / `content/select-type` / `content/count-type` / `content/select-ids`
|
||||
collect blocks across the whole tree (sections recurse); `content/headings`
|
||||
derives a table of contents (`{:id :level :text}` per heading, document order).
|
||||
Inline tree detection (no section.sx dep). 13 tests; suite 574/574.
|
||||
- 2026-06-07 — Extension: id remapping / clone (`clone.sx`).
|
||||
`content/remap-ids` deep-rewrites every block id across the tree (sections
|
||||
recurse) via a function; `content/prefix-ids` prefixes them. Enables
|
||||
collision-free composition (prefix each doc before concat → validates clean,
|
||||
where the unprefixed concat has duplicate ids). Content unchanged, only ids;
|
||||
immutable. 10 tests; suite 561/561.
|
||||
- 2026-06-07 — Extension: deep tree editing (`tree-edit.sx`). `doc-deep-update`
|
||||
/ `doc-deep-replace` / `doc-deep-delete` / `doc-deep-insert-into` mutate blocks
|
||||
anywhere in the nested tree (descending into CtSection children), completing
|
||||
tree mutation to match the deep-find read path; all immutable. 17 tests; suite
|
||||
551/551.
|
||||
- 2026-06-07 — Extension: on-the-wire serialization (`wire.sx`).
|
||||
`content/to-wire` serialises a document to a transmittable SX-text string (data
|
||||
form + SX serializer); `content/from-wire` parses it back into a live document.
|
||||
The whole-document format for persistence / HTTP / federation (distinct from
|
||||
the per-op persist log); round-trips nested trees + tables; reads externally
|
||||
authored wire strings. 11 tests; suite 534/534.
|
||||
- 2026-06-07 — Extension: portable data serialization (`data.sx`).
|
||||
`content/to-data` converts a document to plain SX data
|
||||
(`{:id :title :slug :tags :blocks [{:id :type :fields}]}`, sections recursing);
|
||||
`content/from-data` reconstructs real block objects (section/table handled
|
||||
specially, others generically via mk-block). Round-trips the full tree +
|
||||
metadata (render-equal), decoupling storage/transport from the Smalltalk
|
||||
instance shape. 21 tests; suite 523/523.
|
||||
- 2026-06-07 — Extension: document composition (`compose.sx`). `content/concat`
|
||||
/ `content/prepend` / `content/concat-all` combine documents (keeping the
|
||||
first's id + metadata, concatenating blocks, immutable); `content/wrap-section`
|
||||
collapses a doc's blocks into a single nested section. For assembling pages
|
||||
from header/body/footer parts and templates. 17 tests; suite 502/502.
|
||||
- 2026-06-07 — Extension: SEO-complete page (`page-full.sx`). `content/page-full`
|
||||
extends content/page with `<html lang="en">` and a `<meta name="description">`
|
||||
drawn from the document excerpt (plain text, escaped, 160 chars), composing the
|
||||
page/metadata/text layers into the SEO-ready artifact. 4 tests; suite 485/485.
|
||||
- 2026-06-07 — Extension: Markdown document export (`md-doc.sx`).
|
||||
`content/markdown-doc` emits a `---` frontmatter block from metadata
|
||||
(title/slug/tags, only present fields) ahead of the Markdown body, or plain
|
||||
asMarkdown when there's no metadata. Completes the metadata round-trip:
|
||||
`md/import ∘ content/markdown-doc` preserves title/slug/tags + blocks. 12
|
||||
tests; suite 481/481.
|
||||
- 2026-06-07 — Extension: Markdown frontmatter. `md/import` parses a leading
|
||||
`---` / `key: value` / `---` block into document metadata (title, slug,
|
||||
comma-separated tags via `doc-with-meta`) before parsing the body; a `---`
|
||||
elsewhere stays a divider. Ties the Markdown importer to the metadata layer the
|
||||
way real blog posts work. +9 tests; suite 469/469.
|
||||
- 2026-06-07 — Extension: Markdown table import. `md-import.sx` now recognizes a
|
||||
`| … |` header row followed by a `| --- |` separator and parses a `CtTable`
|
||||
(cells trimmed, mixed with other blocks via blank-line separation), completing
|
||||
the Markdown table round-trip (import∘export == identity). +5 tests; suite
|
||||
460/460.
|
||||
- 2026-06-07 — Extension: HTML page wrapper (`page.sx`). `content/page` composes
|
||||
metadata + render into a minimal valid HTML5 document — escaped `<title>` from
|
||||
doc metadata (falling back to id) and the rendered blocks as the body.
|
||||
`content/page-title`. The shippable artifact the blog serves. 7 tests; suite
|
||||
455/455.
|
||||
- 2026-06-07 — Extension: table block (`table.sx`). `CtTable` holds headers +
|
||||
rows (string lists); answers asHTML (escaped `<table>`), asSx, asText, and
|
||||
asMarkdown: (pipe table with dashed separator row) by folding rows×cells via
|
||||
nested `inject:into:`. Self-contained (no edits to block.sx/render.sx);
|
||||
`mk-table`, `table?`, `table-headers/rows`. validate.sx gained a `table` field
|
||||
case (headers/rows must be lists). 15 tests; suite 448/448.
|
||||
- 2026-06-07 — Extension: document statistics (`stats.sx`). `content/stats`
|
||||
returns `{:words :chars :blocks :reading-minutes}`; word/char counts derive
|
||||
from the tree-accurate `asText` projection, block count from an inline tree
|
||||
walk (no section.sx dep), reading time at 200 wpm rounded up. Counts descend
|
||||
into nested sections. 17 tests; suite 433/433.
|
||||
- 2026-06-07 — Refinement: tree-aware validation. `validate.sx` now flattens the
|
||||
whole block tree (descending into `CtSection` children, guarding malformed
|
||||
non-list children) so field checks and duplicate-id detection cover nested
|
||||
blocks and span section boundaries; added a `section` field-type case. Inline
|
||||
tree detection (class + st-iv-get) keeps it free of a section.sx dependency.
|
||||
+6 tests; suite 416/416.
|
||||
- 2026-06-07 — Extension: nested block trees (`section.sx`). `CtSection` is a
|
||||
block whose `children` ivar is a list of blocks (incl. nested sections →
|
||||
arbitrary depth), turning the flat document into the ordered TREE from the
|
||||
architecture sketch. Self-contained: it answers asHTML/asSx/asText/asMarkdown:
|
||||
by folding children's renderings (pure polymorphic recursion — no changes to
|
||||
block.sx/render.sx). `mk-section`, `section-children`, `section-append` (cow),
|
||||
and tree traversal `doc-deep-find` / `doc-tree-ids` / `doc-tree-count` that
|
||||
descend into sections. 25 tests; suite 410/410.
|
||||
- 2026-06-07 — Extension: plain-text render + excerpts (`text.sx`). Fourth
|
||||
boundary format via polymorphic `asText` (heading/text/code/quote→text,
|
||||
image→alt, embed/divider→"", list→", "-joined); the document joins non-empty
|
||||
child texts with a space. `content/render doc "text"`, `content/text`,
|
||||
`content/excerpt doc n` (first n chars + "…" if truncated). For previews,
|
||||
meta-descriptions, search indexing. 20 tests; suite 385/385.
|
||||
- 2026-06-07 — Extension: document metadata (`meta.sx`). CtDoc gained optional
|
||||
title/slug/tags ivars (declared in doc.sx, default nil/empty, no effect on
|
||||
block ops). Reads via message dispatch; copy-on-write setters
|
||||
(`doc-with-title/slug/tags`, `doc-add-tag`, `doc-with-meta`, `doc-new-meta`)
|
||||
and `content/*` aliases; `doc-meta` returns the metadata dict. Ghost adapter
|
||||
now carries `:title` through import/export/round-trip. 27 tests; suite 365/365.
|
||||
- 2026-06-07 — Extension: snapshot cache over op-log replay (`snapshot.sx`).
|
||||
Snapshots are a cache, never primary state — the log stays the source of truth.
|
||||
`content/snapshot!` stores a materialised head at a seq in the persist KV;
|
||||
`content/head-cached` / `content/at-cached` start from the nearest snapshot and
|
||||
replay only the tail, returning a document IDENTICAL to a full replay (tests
|
||||
assert transparency before/after snapshot, across versions, and after
|
||||
drop-snapshot fallback). `content/has-snapshot?` / `snapshot-seq` /
|
||||
`drop-snapshot!`. 20 tests; suite 338/338.
|
||||
- 2026-06-07 — Extension: Markdown import adapter (`md-import.sx`), inverse of
|
||||
asMarkdown. Line-based parser: ATX headings, fenced code (```lang), blockquotes,
|
||||
unordered/ordered lists (grouping consecutive items), thematic breaks,
|
||||
paragraphs (consecutive plain lines joined with a space). Sequential ids
|
||||
b0,b1…. `md/import` / `content/from-markdown` / `markdown-adapter` (import +
|
||||
asMarkdown export). Round-trips canonical Markdown (import∘export == identity);
|
||||
imported docs pass validation. 24 tests; suite 318/318.
|
||||
- 2026-06-07 — Extension: document validation (`validate.sx`). `content/validate`
|
||||
returns issue dicts `{:id :kind :detail}` (empty = valid); `content/valid?`
|
||||
and `content/issue-kinds` convenience. Checks block id (non-empty string),
|
||||
per-type required fields/types (heading level number, image src/alt strings,
|
||||
list ordered boolean + items list, etc.), unknown block types, and
|
||||
document-level duplicate ids. Guards imports/edits/federated input. 17 tests;
|
||||
suite 294/294.
|
||||
- 2026-06-07 — Extension: durable CRDT replication (`crdt-store.sx`), uniting
|
||||
Phase 2 (persist) + Phase 3 (CvRDT). Each replica appends its CRDT ops to its
|
||||
own stream (`crdt:<doc>:<replica>`); `crdt/replay` folds one log into a state,
|
||||
`crdt/converge` merges every replica's replayed state, `crdt/document` /
|
||||
`crdt/order` materialise. Converged result is identical regardless of replica
|
||||
order or duplicate delivery (join + idempotent apply) → offline-capable,
|
||||
eventually-consistent editing. 14 tests; suite 277/277.
|
||||
- 2026-06-07 — Extension: Markdown render mode (`markdown.sx`). Third boundary
|
||||
format alongside asHTML/asSx via the same polymorphic dispatch; blocks answer
|
||||
`asMarkdown: nl` (boundary supplies the newline — this Smalltalk dialect has
|
||||
no Character newline ctor). `content/render doc "md"`/`"markdown"`/`:md`,
|
||||
`content/markdown`, `asMarkdown`. headings (`#`×level), fenced code, `> ` quote,
|
||||
``, `- `/`1. ` lists, `---`; doc joins blocks with a blank line. No
|
||||
MD escaping yet. 20 tests; suite 263/263.
|
||||
- 2026-06-07 — Extension: asSx wire string-escaping. Added `String>>sxEscaped`
|
||||
(escapes `\`→`\\` then `"`→`\"`) and routed every `asSx` text/attr/list-item
|
||||
through it, so the SX wire format stays valid when content contains quotes or
|
||||
backslashes. +5 render tests (expected strings built from `q`/`bs` helpers to
|
||||
avoid escaping miscounts). Suite 243/243.
|
||||
- 2026-06-07 — Extension: HTML escaping at the render boundary. Added
|
||||
`String>>htmlEscaped` (recursive char walk escaping & < > ", order-safe so &
|
||||
isn't double-escaped) and routed every `asHTML` text/attr through it — heading,
|
||||
text, code body + language, quote, image src/alt, embed url, list items.
|
||||
Render stays fully polymorphic in Smalltalk; escaping lives at the boundary.
|
||||
+8 render tests (incl. `<script>` payloads, attr breakout, ampersand-once).
|
||||
asSx wire-escaping deferred to next. Suite 238/238.
|
||||
|
||||
- 2026-06-07 — Phase 4 `fed.sx` (**Phase 4 COMPLETE — roadmap done**):
|
||||
trust-gated federation. Peer ops carry provenance (`:author`, `:sig` stub);
|
||||
none are auto-accepted. The trust gate is a pluggable predicate (acl-on-sx
|
||||
hook) with a trusted-actor-list convenience stub. `content/merge-peer[-with]`
|
||||
applies only accepted ops through the CvRDT and quarantines the rest
|
||||
(`{:state :accepted :rejected}`). Concurrent local/external edits reconcile
|
||||
deterministically: same-field LWW by (ts,actor), commutative, idempotent;
|
||||
untrusted ops never touch state. 20 tests; suite 230/230.
|
||||
- 2026-06-07 — Phase 4 `sync.sx` (cb1): external CMS sync via an injected
|
||||
adapter. Core defines the shape — `{:import :export}` — and delegates;
|
||||
`content/import` / `content/export` / `content/round-trip` know nothing about
|
||||
Ghost. A Ghost-flavoured adapter confines all format translation (post
|
||||
`:sections` ↔ content blocks, all 8 kinds). Swapping in a stub `raw-adapter`
|
||||
works identically. Round-trip (export∘import and import∘export) preserves ids,
|
||||
types, fields, order. 14 tests; suite 210/210. Next: trust-gated federation +
|
||||
concurrent-external-edit conflict (via CRDT).
|
||||
- 2026-06-07 — Phase 3 `crdt.sx` (**Phase 3 complete**): collaborative merge as
|
||||
a state-based CvRDT. Merge is a join (lub) on a semilattice → commutative,
|
||||
associative, idempotent by construction. Ordering = unique dense Logoot
|
||||
position keys (cell = (digit actor), lexicographic); presence = OR-tombstones
|
||||
(remove-wins); each field = an LWW-Register keyed by logical (ts, actor). Every
|
||||
op contributes a PARTIAL element and per-id state is their join, so
|
||||
update-/delete-before-insert are not lost. `crdt-materialize` bridges back to a
|
||||
Phase-1 `CtDoc` (sort live elements by pos → blocks). Tests prove: ops in any
|
||||
order converge, double-apply is a no-op, merge commutes/associates/is
|
||||
idempotent, concurrent inserts order deterministically, same-field LWW by
|
||||
(ts,actor), disjoint fields both survive, two divergent replicas converge both
|
||||
ways. 34 tests; suite 196/196.
|
||||
- 2026-06-07 — Phase 2 `store.sx` (**Phase 2 complete**): op log + versioning
|
||||
over the persist event stream. `content/commit!` appends an edit op as a
|
||||
persist event to the doc's stream (`content:<id>`); the log is the source of
|
||||
truth. `content/head` / `content/at b id seq` replay the op stream to the
|
||||
latest / any version (materialised doc is a cache, never primary state).
|
||||
`content/history` returns per-version metadata; `content/diff` /
|
||||
`content/diff-versions` report added/removed/changed block ids. Backend is
|
||||
injected via `(persist/open)` — content knows nothing about which backend.
|
||||
Minimal persist load (event/backend/log/kv/api). 29 tests; suite 162/162.
|
||||
- 2026-06-07 — Phase 1 `api.sx` (**Phase 1 complete**): `content/*` facade over
|
||||
block + doc + render. `content/bootstrap!` registers the hierarchy;
|
||||
`content/edit` applies one op or an op stream; `content/render` picks the
|
||||
boundary format ("html"/"sx" or keyword). Re-exports `content/new`,
|
||||
`content/append`, `content/insert|update|move|delete`, `content/find`, etc.
|
||||
`content/op?` distinguishes a single op from a list/block. 26 tests; suite
|
||||
133/133. content/history deferred to Phase 2 (needs the persist op log).
|
||||
- 2026-06-07 — Phase 1 `render.sx`: render boundary as polymorphic message
|
||||
dispatch. Every block and `CtDoc` answers `asHTML` / `asSx`; the document
|
||||
folds children via Smalltalk `inject:into:` (works on raw SX lists), so
|
||||
`(asHTML doc)` / `(asSx doc)` are pure sends with zero type-switching in SX.
|
||||
Lists/headings render in Smalltalk source. No HTML escaping yet (noted in
|
||||
render.sx — boundary concern before untrusted content). 29 tests; suite
|
||||
107/107.
|
||||
- 2026-06-06 — Phase 1 `doc.sx`: ordered block document (`CtDoc`) as a
|
||||
Smalltalk object holding an ordered block sequence. Edit ops are data dicts
|
||||
(`insert`/`update`/`move`/`delete`) with `op-*` constructors; `doc-apply` /
|
||||
`doc-apply-all` interpret an op stream, each returning a NEW document (input
|
||||
never mutated → replay-safe). Structural moves, insert-after/at, find/index,
|
||||
immutability all tested. 40 tests; suite 78/78.
|
||||
- 2026-06-06 — Phase 1 `block.sx`: typed block objects as Smalltalk instances
|
||||
(`CtBlock` hierarchy: text/heading/code/quote/image/embed/divider/list).
|
||||
Type tag + accessors are message sends (polymorphic dispatch); fields are
|
||||
immutable copy-on-write via functional `st-iv-set!` (history-safe). Added
|
||||
`mk-*` constructors, `block?` predicate, `lib/content/conformance.sh` +
|
||||
scoreboard. 38/38.
|
||||
|
||||
## Blockers
|
||||
(loop fills this in)
|
||||
|
||||
- Smalltalk-only load chain (tokenizer/parser/runtime/eval) does **not** load
|
||||
`lib/r7rs.sx`/`spec/stdlib.sx`, so r7rs aliases (`car`/`cdr`/`null?`) are
|
||||
absent. Use base SX primitives (`first`/`rest`/`(= (len x) 0)`) in
|
||||
`lib/content/**`. Not a substrate bug — just the load surface.
|
||||
|
||||
Reference in New Issue
Block a user