From 4fb733c0a40d46fd4c192d6e2ac2bdb95458e4c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 23:39:31 +0000 Subject: [PATCH 1/2] Commons: add post@1, the broadcast utterance (#204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the fourth cell of the artifact/speech × bounded/unbounded 2×2 that text-types.md already names: `post` as the speech/unbounded contract, alongside note (kept), message (sent), and article (published). - docs/commons/post.md: new type file — schema (`text` required, `format`, `url`), the no-date-field rationale (uttering is creating, same reasoning as message@1), conventions (parentId threading, embed/location/syndicated-to cross-type labels, entityId authorship), and the scheduled-posts open question. - docs/commons/README.md: post moves from "deliberately absent" into the namespace list and type table as Draft. - docs/commons/text-types.md: the two "not yet in the commons" worked-example rows and the fourth-contract section now point at post.md. - docs/commons/message.md: cross-references post.md from its "Deliberately excluded" section. - packages/commons: POST export, added to defineCommonsTypes' test coverage and read-compat core checks. No core changes needed — the ATProto RFCs (#15, #16) the issue discusses as a would-be blocker are already closed, and the type itself doesn't depend on #203 (settable createdAt), which only affects importing an existing archive into this type. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011bz5a8shrGQsz5tdFB3z4i --- .changeset/post-type.md | 10 +++ docs/commons/README.md | 22 ++--- docs/commons/message.md | 4 +- docs/commons/post.md | 117 +++++++++++++++++++++++++++ docs/commons/text-types.md | 56 ++++++------- packages/commons/README.md | 4 +- packages/commons/src/index.ts | 10 +++ packages/commons/tests/index.test.ts | 4 +- 8 files changed, 186 insertions(+), 41 deletions(-) create mode 100644 .changeset/post-type.md create mode 100644 docs/commons/post.md diff --git a/.changeset/post-type.md b/.changeset/post-type.md new file mode 100644 index 0000000..0faf392 --- /dev/null +++ b/.changeset/post-type.md @@ -0,0 +1,10 @@ +--- +'@haverstack/commons': minor +--- + +Add `org.haverstack/post@1` — the broadcast utterance, completing the fourth cell +(speech / unbounded audience) of the commons' text-type contract 2×2. + +`POST` exports `{ text: required, format, url }`, no date field by design — uttering +is creating, and a mirrored `publishedAt` would equal `createdAt` on every record. See +`docs/commons/post.md` for the full rationale and conventions. diff --git a/docs/commons/README.md b/docs/commons/README.md index 7089da3..9a3cbf6 100644 --- a/docs/commons/README.md +++ b/docs/commons/README.md @@ -33,6 +33,7 @@ org.haverstack/article@1 org.haverstack/place@1 org.haverstack/page@1 org.haverstack/photo@1 +org.haverstack/post@1 org.haverstack/message@1 (proposed — see below) org.haverstack/event@1 (proposed) org.haverstack/poll@1 (proposed) @@ -216,7 +217,8 @@ rewritten by and for them. Three clusters. The **personal-data cluster** covers the shapes nearly every personal app re-invents first. The **publishing cluster** covers the personal-web shapes (its note/article boundary is the IndieWeb's post-type-discovery rule: a note is an entry -without a name, an article is an entry with one). The **group cluster** covers the +without a name, an article is an entry with one) plus `post`, the broadcast-speech +counterpart to `article`'s published-artifact contract. The **group cluster** covers the small-group workspace — the Basecamp shape: message board, shared calendar, decisions, shared drive — built on collaborative group stacks (`_group` with `stackUrl`), where `entityId`-as-author and per-type grants do the heavy lifting. Pairs across the set @@ -224,10 +226,10 @@ make the interop story demonstrable: notes ↔ flashcards, bookmarks ↔ read-la articles and pages ↔ any site generator, polls ↔ calendar (a scheduling poll's winning slot becomes an event). -Three types carry free-form text and are distinguished by **social contract, not -shape** — notes are kept, messages are sent, articles are published. -[Choosing a text type](./text-types.md) is the decision guide, with worked examples -(comments on a blog post are messages; private marginalia are notes). +Four types carry free-form text and are distinguished by **social contract, not +shape** — notes are kept, messages are sent, articles are published, posts are +broadcast. [Choosing a text type](./text-types.md) is the decision guide, with worked +examples (comments on a blog post are messages; private marginalia are notes). | Type | File | Status | Read-compat core | | --------------------------- | ------------------------------ | -------- | ------------------------- | @@ -239,6 +241,7 @@ shape** — notes are kept, messages are sent, articles are published. | `org.haverstack/place@1` | [`place.md`](./place.md) | Draft | `{ latitude, longitude }` | | `org.haverstack/page@1` | [`page.md`](./page.md) | Draft | `{ slug, text }` | | `org.haverstack/photo@1` | [`photo.md`](./photo.md) | Draft | `{ image }` | +| `org.haverstack/post@1` | [`post.md`](./post.md) | Draft | `{ text }` | | `org.haverstack/message@1` | [`message.md`](./message.md) | Proposed | `{ text }` | | `org.haverstack/event@1` | [`event.md`](./event.md) | Proposed | `{ title, startsAt }` | | `org.haverstack/poll@1` | [`poll.md`](./poll.md) | Proposed | `{ question, options }` | @@ -254,11 +257,10 @@ concrete intended writer exists — the group cluster graduates when a group-too or demo is real, building on the grant/group primitives (`_group`, type-level grants) documented in the identity and access-control specs. -Deliberately absent from the initial set: `post` (the broadcast contract — `message` is -the group-scoped shape, not the social one), recurrence rules (see `event`: occurrences are materialized in @1), -`file`/`document` (a first-class `file` type is expected to follow `photo`'s pattern; -until a real writer needs it, a record plus attachment covers it), and `checkin` -(subsumed by the `location` cross-type convention plus any record). +Deliberately absent from the initial set: recurrence rules (see `event`: occurrences are +materialized in @1), `file`/`document` (a first-class `file` type is expected to follow +`photo`'s pattern; until a real writer needs it, a record plus attachment covers it), and +`checkin` (subsumed by the `location` cross-type convention plus any record). --- diff --git a/docs/commons/message.md b/docs/commons/message.md index 86bc49f..8149859 100644 --- a/docs/commons/message.md +++ b/docs/commons/message.md @@ -83,7 +83,7 @@ discussion. ## Deliberately excluded -- Social-post semantics — the broadcast contract's territory (see above). +- Social-post semantics — the broadcast contract's territory; see [`post`](./post.md). - Reactions — a future micro-proposal (likely tag associations by non-authors — which needs finer-grained association permissions than today's `update-own`/`update-any` grant actions provide). @@ -96,3 +96,5 @@ discussion. - **Proposed** — initial definition: `text` (required), `subject`, `format`. - **Proposed, amended** — threads may be anchored by any record (comments on articles, photos, polls); text-type contract guide cross-referenced. +- **Proposed, amended** — [`post@1`](./post.md) landed, discharging the broadcast-shape + cross-reference this file previously carried as a forward pointer only. diff --git a/docs/commons/post.md b/docs/commons/post.md new file mode 100644 index 0000000..765a67d --- /dev/null +++ b/docs/commons/post.md @@ -0,0 +1,117 @@ +# `org.haverstack/post@1` — Post + +> **Status:** Draft. + +A broadcast utterance: a short, untitled piece of speech published under a public +identity, addressed to whoever listens rather than to a bounded audience. Completes the +fourth cell of the artifact/speech × bounded/unbounded 2×2 in +[Choosing a text type](./text-types.md#the-fourth-contract-posts-are-broadcast) — +artifact/unbounded is `article`/`page`, speech/bounded is `message`, speech/unbounded is +`post`. + +**Not the IndieWeb's "note."** Their note is our `post`; see +[On the names](./text-types.md#on-the-names) for why the commons keeps its own names +despite the collision with IndieWeb post-type discovery and ActivityStreams 2.0's `Note`. + +Posts are **broadcast** — speech constituted by the act of publishing, under a +self-certifying identity, to an audience the stack has no boundary around. For the +boundary with `note` (kept), `message` (sent to a bounded audience), and `article` +(published as a named work), see +[Choosing a text type](./text-types.md). + +## Schema + +```ts +await stack.defineType('org.haverstack/post@1', 'Post', { + text: { kind: 'text', required: true }, + format: { kind: 'string' }, + url: { kind: 'string' }, +}); +``` + +## Field semantics + +| Field | Kind | Required | Meaning | +| -------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `text` | `text` | yes | The utterance. What the post _is_. | +| `format` | `string` | no | Same vocabulary and defaults as `note`: `"markdown"` (default when absent), `"plain"`; unknown values render as plain. | +| `url` | `string` | no | Canonical location. POSSE's premise is that the canonical copy lives on your own site; the publishing app stamps it at first publish, same convention as `article.url`. Syndicated copies never overwrite it. | + +The author is `entityId` — set natively by `Stack.create()`/`ScopedStack.create()`. No +`author` string: unlike `article`, a post is never a captured work carrying someone +else's byline. + +### No date field, deliberately + +The commons applies a consistent rule: a content date earns its place by naming an +event `createdAt` doesn't (`photo.takenAt` is capture vs. import, `article.publishedAt` +is publication vs. drafting). For a broadcast utterance, **uttering is creating** — an +unsaid post is working material, not a post, which is the same reasoning that leaves +`message@1` with no date field. A `publishedAt` here would equal `createdAt` on +essentially every record ever written, which design rule 5 forbids by name +(_"timestamps are `createdAt`/`updatedAt` — never mirrored into content"_). + +This makes native `createdAt` the only ordering key a post will ever have. Importing an +existing archive of dated posts therefore depends on +[#203](https://github.com/haverstack/core/issues/203) (settable `createdAt` for +owner-side imports on unscoped `Stack`) — a dependency on _importing into_ the type, not +on the type's definition, which is fully expressible today. + +**Scheduled posts**, the strongest objection to no date field: the position taken here +is that scheduling is the scheduler's state, not the utterance's — a queued post is +working material inside a tool until it goes out, and its moment is when it went out. +If that proves wrong in practice, an optional date field lands in place with no version +bump; starting with a mirror field and later removing it would be the expensive +direction. + +## Conventions + +- **Threading**: `parentId` for in-stack anchoring, as `message` does. Cross-stack + replies — a reply living in the replier's stack, referencing a record in someone + else's — are a relationship's `target` union + (`{ scope: 'record', recordId, stackUrl }` / `{ scope: 'external', ns, id }`), not + `parentId`, and need no change here; see + [Choosing a text type § The fourth contract](./text-types.md#the-fourth-contract-posts-are-broadcast). +- **Embedded media**: attachment associations labeled `embed`, per the cross-type + convention. +- **Geotagging**: the cross-type `location` relationship to a [`place`](./place.md) + record — the Foursquare-style public check-in is `post` + `location`. +- **Tags**: tag associations, never a content field. +- **Syndication**: a bridge publishes a copy elsewhere and stamps a `syndicated-to` + relationship with an external target on the canonical record; see + [Cross-type conventions](./README.md#cross-type-conventions). The canonical copy is + this record; syndicated copies never own `url`. +- **Authorship**: `entityId`. Deletion inside the stack is recoverable as usual; once a + post has been syndicated, deletion of the broadcast copy is a request to the network, + not a guarantee — a caveat of syndication, not of this type. + +## Prior art + +IndieWeb post-type discovery (their "note"), ActivityStreams 2.0 `Note`, +`app.bsky.feed.post`, Mastodon status, Twitter. + +## Read-compat core + +```ts +{ text: { kind: 'text', required: true } } +``` + +Shared with `note`, `message`, and `article` by design — generic text consumers reach +all four via `isCompatible()`; the `typeId` is the contract signal for consumers that +honor it. + +## Deliberately excluded + +- `createdAt`/`updatedAt`/a `publishedAt` mirror — see "No date field, deliberately" + above. +- `tags: string[]` — tag associations exist. +- `author` string — a post is never a captured work with someone else's byline; + `entityId` is the author. +- `to`/`cc` addressing, reply-count, like-count and similar network-derived fields — + bridge/adapter territory, not properties of the utterance itself. +- `lexiconId` mapping, content addressing, tombstone semantics — bridge machinery + (`adapter-atproto` and friends), not a content schema concern. + +## Changelog + +- **Draft** — initial definition: `text` (required), `format`, `url`. diff --git a/docs/commons/text-types.md b/docs/commons/text-types.md index ccffb19..5ccd229 100644 --- a/docs/commons/text-types.md +++ b/docs/commons/text-types.md @@ -56,26 +56,26 @@ that being read as a finished work is its purpose. ## Worked examples -| Case | Type | Why | -| -------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------- | -| Comment on a blog post | `message` | Sent into a shared space, anchored to the post (`parentId` → the article), time-indexed. | -| Private marginalia while reading | `note` (+ `about` rel.) | Kept, not sent — your annotation, invisible to the conversation. | -| Draft blog post | `article` (no `publishedAt`) | Already a work-in-becoming; drafthood is a lifecycle stage, not a different type. | -| Scribbled idea for a post | `note` | Working material; promote to `article` later (new record, `derived-from` relationship). | -| Journal / diary entry | `note` | Time-indexed but addressed to no one — address, not temporality, makes speech. | -| Memoir chapter it becomes | `article` | Now a named work for readers. | -| Newsletter issue | `article` | Distributed, not conversational; archive-space, not conversation-space. | -| Announcement ("practice canceled tomorrow") | `message` | Addressed and moment-indexed even though one-way; its meaning decays with its moment. | -| Meeting minutes | `note` | The group's own working record; optionally _announced_ with a `message` linking to it. | -| Group how-to ("opening the clubhouse") | `note` | Living document, current state matters, collectively maintained. | -| Same how-to on the club's public website | `page` | Same text, different act: now a node in a published site. | -| Recipe in your kitchen file | `note` | Kept. | -| Same recipe on your blog | `article` | Published. The type records the act, not the text. | -| Sharing a link into the group ("read this!") | `message` (+ rel.) | The commentary is speech; the shared bookmark/article stays an artifact, linked. | -| Check-in, private location diary | `note` (+ `location`) | A journal entry with coordinates — addressed to no one. | -| "I'm at the café — come join me" (group) | `message` (+ `location`) | Speech; the `location` association is the invariant across every check-in contract. | -| Foursquare-style public check-in | _not yet in the commons_ | Broadcast speech — `post` + `location` once the fourth contract lands. | -| Social media post | _not yet in the commons_ | A fourth contract — public broadcast — see below. | +| Case | Type | Why | +| -------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------- | +| Comment on a blog post | `message` | Sent into a shared space, anchored to the post (`parentId` → the article), time-indexed. | +| Private marginalia while reading | `note` (+ `about` rel.) | Kept, not sent — your annotation, invisible to the conversation. | +| Draft blog post | `article` (no `publishedAt`) | Already a work-in-becoming; drafthood is a lifecycle stage, not a different type. | +| Scribbled idea for a post | `note` | Working material; promote to `article` later (new record, `derived-from` relationship). | +| Journal / diary entry | `note` | Time-indexed but addressed to no one — address, not temporality, makes speech. | +| Memoir chapter it becomes | `article` | Now a named work for readers. | +| Newsletter issue | `article` | Distributed, not conversational; archive-space, not conversation-space. | +| Announcement ("practice canceled tomorrow") | `message` | Addressed and moment-indexed even though one-way; its meaning decays with its moment. | +| Meeting minutes | `note` | The group's own working record; optionally _announced_ with a `message` linking to it. | +| Group how-to ("opening the clubhouse") | `note` | Living document, current state matters, collectively maintained. | +| Same how-to on the club's public website | `page` | Same text, different act: now a node in a published site. | +| Recipe in your kitchen file | `note` | Kept. | +| Same recipe on your blog | `article` | Published. The type records the act, not the text. | +| Sharing a link into the group ("read this!") | `message` (+ rel.) | The commentary is speech; the shared bookmark/article stays an artifact, linked. | +| Check-in, private location diary | `note` (+ `location`) | A journal entry with coordinates — addressed to no one. | +| "I'm at the café — come join me" (group) | `message` (+ `location`) | Speech; the `location` association is the invariant across every check-in contract. | +| Foursquare-style public check-in | [`post`](./post.md) (+ `location`) | Broadcast speech, geotagged — the fourth contract, below. | +| Social media post | [`post`](./post.md) | A fourth contract — public broadcast — see below. | ## Comments are messages; marginalia are notes @@ -95,12 +95,12 @@ contracts encode two things, and speech-ness is only one. The other is **audienc shape**: a `message`'s audience is defined by a boundary (presence in the stack _is_ the addressing — the reason `message` has no `to`/`cc`), while a post's audience is whoever listens: unbounded and unenumerable. That completes a 2×2, and the fourth cell -is deliberately outside this guide: +is [`post`](./post.md): -| | Bounded audience | Unbounded audience | -| ------------ | ----------------------------- | ---------------------------------- | -| **Artifact** | `note` (kept — self or group) | `article` / `page` (published) | -| **Speech** | `message` (sent) | **`post` (broadcast)** — see below | +| | Bounded audience | Unbounded audience | +| ------------ | ----------------------------- | ------------------------------- | +| **Artifact** | `note` (kept — self or group) | `article` / `page` (published) | +| **Speech** | `message` (sent) | [`post`](./post.md) (broadcast) | (Naming trap from prior art: the IndieWeb's "note" post-type is a public short utterance — _their_ note is our `post`, not our `note`.) @@ -136,8 +136,10 @@ rests on: relationship targets that name a record in someone else's stack or an identifier in another protocol, and a `relatedTo` filter that queries them. A _bridge_ carries the rest — its typeId → `$type` translation table, and the content addressing and tombstone machinery that describes a copy rather than the record it was -made from. Those are separate tracks from the content type: a `post` with no replies -and no bridge is fully expressible with what core provides. +made from. Those are separate tracks from the content type: [`post`](./post.md) is +defined and fully expressible with what core provides today, replies and bridge +machinery included; only the bridge itself (`adapter-atproto`, the `lexiconId` mapping, +`externalIds`) remains future work. The shape this enables is the IndieWeb's POSSE pattern — publish on your own site, syndicate elsewhere — with real primitives underneath: the canonical copy lives in your diff --git a/packages/commons/README.md b/packages/commons/README.md index 6e22f4f..eddd54d 100644 --- a/packages/commons/README.md +++ b/packages/commons/README.md @@ -34,8 +34,8 @@ await defineCommonsTypes(stack, [NOTE, TASK]); const note = await stack.create(NOTE.id, { text: 'Hello, Haverstack!' }); ``` -Each export (`NOTE`, `BOOKMARK`, `TASK`, `CONTACT`, `ARTICLE`, `PLACE`, `PAGE`, `PHOTO`) -is a `{ id, name, schema }` triple mirroring its type's `stack.defineType(...)` block in +Each export (`NOTE`, `BOOKMARK`, `TASK`, `CONTACT`, `ARTICLE`, `PLACE`, `PAGE`, `PHOTO`, +`POST`) is a `{ id, name, schema }` triple mirroring its type's `stack.defineType(...)` block in [`docs/commons/`](https://github.com/haverstack/core/tree/main/docs/commons) exactly. `defineCommonsTypes()` is a thin loop over `stack.defineType()` — calling it again with types already registered is the ordinary idempotent no-op path (see `defineType`'s own diff --git a/packages/commons/src/index.ts b/packages/commons/src/index.ts index 49d019b..949fe35 100644 --- a/packages/commons/src/index.ts +++ b/packages/commons/src/index.ts @@ -132,6 +132,16 @@ export const PHOTO: CommonsType = { }, }; +export const POST: CommonsType = { + id: 'org.haverstack/post@1', + name: 'Post', + schema: { + text: { kind: 'text', required: true }, + format: { kind: 'string' }, + url: { kind: 'string' }, + }, +}; + /** * Registers each given commons type on `stack` via `defineType()`, exactly * as written here. Sequential, matching `Stack`'s own system-type seeding — diff --git a/packages/commons/tests/index.test.ts b/packages/commons/tests/index.test.ts index f11e324..e1054a6 100644 --- a/packages/commons/tests/index.test.ts +++ b/packages/commons/tests/index.test.ts @@ -11,10 +11,11 @@ import { PLACE, PAGE, PHOTO, + POST, defineCommonsTypes, } from '../src/index.js'; -const ALL = [NOTE, BOOKMARK, TASK, CONTACT, ARTICLE, PLACE, PAGE, PHOTO]; +const ALL = [NOTE, BOOKMARK, TASK, CONTACT, ARTICLE, PLACE, PAGE, PHOTO, POST]; let adapter: MemoryAdapter; let stack: Stack; @@ -75,6 +76,7 @@ describe('read-compat cores', () => { text: { kind: 'text', required: true }, }, [PHOTO.id]: { image: { kind: 'file-ref', required: true } }, + [POST.id]: { text: { kind: 'text', required: true } }, }; test('every schema satisfies its documented read-compat core', () => { From 024e9242493db03336a22fb5df348d7591ace60d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 23:51:26 +0000 Subject: [PATCH 2/2] Fix Type column in text-types.md worked examples to match convention The two new post rows used markdown links ([`post`](./post.md)) instead of the plain backtick code spans every other row in the table uses. The doc already links to post.md from the fourth-contract section above the table, so the link wasn't needed here either. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011bz5a8shrGQsz5tdFB3z4i --- docs/commons/text-types.md | 40 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/commons/text-types.md b/docs/commons/text-types.md index 5ccd229..5972ca5 100644 --- a/docs/commons/text-types.md +++ b/docs/commons/text-types.md @@ -56,26 +56,26 @@ that being read as a finished work is its purpose. ## Worked examples -| Case | Type | Why | -| -------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------- | -| Comment on a blog post | `message` | Sent into a shared space, anchored to the post (`parentId` → the article), time-indexed. | -| Private marginalia while reading | `note` (+ `about` rel.) | Kept, not sent — your annotation, invisible to the conversation. | -| Draft blog post | `article` (no `publishedAt`) | Already a work-in-becoming; drafthood is a lifecycle stage, not a different type. | -| Scribbled idea for a post | `note` | Working material; promote to `article` later (new record, `derived-from` relationship). | -| Journal / diary entry | `note` | Time-indexed but addressed to no one — address, not temporality, makes speech. | -| Memoir chapter it becomes | `article` | Now a named work for readers. | -| Newsletter issue | `article` | Distributed, not conversational; archive-space, not conversation-space. | -| Announcement ("practice canceled tomorrow") | `message` | Addressed and moment-indexed even though one-way; its meaning decays with its moment. | -| Meeting minutes | `note` | The group's own working record; optionally _announced_ with a `message` linking to it. | -| Group how-to ("opening the clubhouse") | `note` | Living document, current state matters, collectively maintained. | -| Same how-to on the club's public website | `page` | Same text, different act: now a node in a published site. | -| Recipe in your kitchen file | `note` | Kept. | -| Same recipe on your blog | `article` | Published. The type records the act, not the text. | -| Sharing a link into the group ("read this!") | `message` (+ rel.) | The commentary is speech; the shared bookmark/article stays an artifact, linked. | -| Check-in, private location diary | `note` (+ `location`) | A journal entry with coordinates — addressed to no one. | -| "I'm at the café — come join me" (group) | `message` (+ `location`) | Speech; the `location` association is the invariant across every check-in contract. | -| Foursquare-style public check-in | [`post`](./post.md) (+ `location`) | Broadcast speech, geotagged — the fourth contract, below. | -| Social media post | [`post`](./post.md) | A fourth contract — public broadcast — see below. | +| Case | Type | Why | +| -------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------- | +| Comment on a blog post | `message` | Sent into a shared space, anchored to the post (`parentId` → the article), time-indexed. | +| Private marginalia while reading | `note` (+ `about` rel.) | Kept, not sent — your annotation, invisible to the conversation. | +| Draft blog post | `article` (no `publishedAt`) | Already a work-in-becoming; drafthood is a lifecycle stage, not a different type. | +| Scribbled idea for a post | `note` | Working material; promote to `article` later (new record, `derived-from` relationship). | +| Journal / diary entry | `note` | Time-indexed but addressed to no one — address, not temporality, makes speech. | +| Memoir chapter it becomes | `article` | Now a named work for readers. | +| Newsletter issue | `article` | Distributed, not conversational; archive-space, not conversation-space. | +| Announcement ("practice canceled tomorrow") | `message` | Addressed and moment-indexed even though one-way; its meaning decays with its moment. | +| Meeting minutes | `note` | The group's own working record; optionally _announced_ with a `message` linking to it. | +| Group how-to ("opening the clubhouse") | `note` | Living document, current state matters, collectively maintained. | +| Same how-to on the club's public website | `page` | Same text, different act: now a node in a published site. | +| Recipe in your kitchen file | `note` | Kept. | +| Same recipe on your blog | `article` | Published. The type records the act, not the text. | +| Sharing a link into the group ("read this!") | `message` (+ rel.) | The commentary is speech; the shared bookmark/article stays an artifact, linked. | +| Check-in, private location diary | `note` (+ `location`) | A journal entry with coordinates — addressed to no one. | +| "I'm at the café — come join me" (group) | `message` (+ `location`) | Speech; the `location` association is the invariant across every check-in contract. | +| Foursquare-style public check-in | `post` (+ `location`) | Broadcast speech, geotagged — the fourth contract, below. | +| Social media post | `post` | A fourth contract — public broadcast — see below. | ## Comments are messages; marginalia are notes