Reframe the interface around composed posts, not republished entries
The channel publishes a post written from a hall entry -- condensed, personal, reach-optimized -- not the entry itself. That changes what this interface is for and where its boundary sits. Composition is deliberately outside the interface. Composing is a probabilistic editorial task, and Blueprint 48.1 forbids an LLM in the mandatory request path, so an interface that composed on demand would be non-deterministic exactly where determinism is the whole claim. It is also editorial: a post speaks about a named person's work in HelixForge's voice, and a review step inside the request path is a review step nobody performs. The contract now takes a composed post with a required reviewer, a cited source entry and a consent basis. A post carrying none of those is refused. Absence of objection is not consent, and in an audit trail an unfaithful post looks exactly like a faithful one -- the defence is that a human read it, the source is named, and both are recorded. The first experiment is rebuilt accordingly. The old competition asked how to fit a whole entry into Telegram; that question no longer exists. The new one asks whether the portrait earns the 3072 characters of caption limit it costs, which is a delivery question this interface actually owns. Competition over editorial voice belongs to the campaign and runs on the per-variant engagement this interface reports. Adds T05b (composition and review), T05c (consent basis for the 94 existing participants, blocking public publication) and T11 (extracting the campaign into pr-hall-of-helix). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu Assistant: claude-code Assistant-Model: opus Assistant-Process: 1116572@bnt-lap001 Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400
This commit is contained in:
parent
aa95a09ab6
commit
3aa8cb7eaf
14 changed files with 793 additions and 521 deletions
|
|
@ -3,96 +3,147 @@ info:
|
|||
title: HelixForge Telegram Publishing
|
||||
version: "R-1"
|
||||
description: |
|
||||
Publishes hall-of-helix entries to a Telegram channel.
|
||||
Publishes composed channel posts to a Telegram channel.
|
||||
|
||||
The interface takes an entry as it was written and is responsible for
|
||||
everything about how it becomes a Telegram post. A caller should never need
|
||||
to know Telegram's constraints.
|
||||
The unit of publication is a POST, not a hall entry. A post is a short,
|
||||
personal, reach-optimized piece written *from* a hall-of-helix entry — it
|
||||
is not the entry, and it is not a truncation of the entry.
|
||||
|
||||
Publication is idempotent on entry_id: publishing the same entry twice
|
||||
yields one publication. This matters because the caller is usually
|
||||
automation that may be retried.
|
||||
Composition happens upstream, before this interface is called. That is a
|
||||
deliberate architectural boundary and not an accident of layering:
|
||||
composing a condensed, entertaining post is a probabilistic task, and
|
||||
ArchitectureBlueprint.md section 48.1 names an LLM in the mandatory request
|
||||
path as an anti-pattern. This interface stays deterministic by taking the
|
||||
composed text as input.
|
||||
|
||||
Publication is idempotent on post_id, because the caller is automation that
|
||||
may be retried and a duplicate post cannot be quietly undone.
|
||||
|
||||
paths:
|
||||
/v1/hall-entries:
|
||||
/v1/channel-posts:
|
||||
post:
|
||||
operationId: publishEntry
|
||||
summary: Publish a hall-of-helix entry
|
||||
operationId: publishPost
|
||||
summary: Publish a composed channel post
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [entry_id, title, body]
|
||||
required: [post_id, body, source_entry_id, source_entry_url, reviewed_by]
|
||||
additionalProperties: false
|
||||
properties:
|
||||
entry_id:
|
||||
post_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 128
|
||||
description: |
|
||||
The hall entry's own id, from its frontmatter. Publication
|
||||
is idempotent on this value.
|
||||
title:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 256
|
||||
Stable id for this post. Publication is idempotent on it.
|
||||
A recomposition of the same entry is a NEW post_id, because
|
||||
it is a different piece of writing about the same work.
|
||||
|
||||
body:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 4096
|
||||
description: |
|
||||
The entry body as Markdown, frontmatter removed.
|
||||
The composed post, already within Telegram's single-message
|
||||
limit. Bounded here on purpose: a composition that does not
|
||||
fit was not condensed, and this interface must not decide
|
||||
on the composer's behalf what to cut.
|
||||
|
||||
Deliberately unbounded. Telegram's 4096-character message
|
||||
limit is this interface's problem, not the caller's, and a
|
||||
maxLength here would push it back onto them.
|
||||
display_name:
|
||||
source_entry_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: |
|
||||
The hall entry this post was written from. Required: a post
|
||||
with no traceable source cannot be checked against what the
|
||||
person actually said about their own work.
|
||||
|
||||
source_entry_url:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Canonical location of the source entry.
|
||||
|
||||
reviewed_by:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: |
|
||||
Who approved this composition for publication. Required at
|
||||
FLUID-2 and above: the post speaks about a named person's
|
||||
work in HelixForge's voice, and no generated text reaches
|
||||
subscribers without someone having read it.
|
||||
|
||||
subject_display_name:
|
||||
type: string
|
||||
maxLength: 128
|
||||
worker_kind:
|
||||
description: The person or agent whose work the post is about.
|
||||
|
||||
subject_consent:
|
||||
type: string
|
||||
enum: [human, agent-session]
|
||||
recorded_at:
|
||||
enum: [explicit, standing, not-required]
|
||||
description: |
|
||||
On what basis this post may speak about the subject's work.
|
||||
"standing" covers contributors who agreed when they took a
|
||||
seat in the hall; "explicit" is per-post approval.
|
||||
|
||||
variant:
|
||||
type: string
|
||||
format: date
|
||||
source_url:
|
||||
maxLength: 64
|
||||
description: |
|
||||
Which composition strategy produced this post. Opaque to
|
||||
this interface: it records the label and reports engagement
|
||||
against it, so that the campaign can compare strategies it
|
||||
owns without this interface needing to understand them.
|
||||
|
||||
visual_url:
|
||||
type: string
|
||||
description: Canonical location of the entry in hall-of-helix.
|
||||
description: Portrait or illustration to attach.
|
||||
|
||||
tags:
|
||||
type: array
|
||||
maxItems: 8
|
||||
items: {type: string, maxLength: 32}
|
||||
responses:
|
||||
"201":
|
||||
description: Published
|
||||
"200":
|
||||
description: Already published; the existing publication is returned
|
||||
"400":
|
||||
description: The entry cannot be published faithfully
|
||||
description: The post cannot be published as given
|
||||
"403":
|
||||
description: Publication refused; no review or no basis for consent
|
||||
"502":
|
||||
description: Telegram was unreachable or refused the message
|
||||
|
||||
get:
|
||||
operationId: listPublications
|
||||
operationId: listPosts
|
||||
summary: List publications, newest first
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
schema: {type: integer, minimum: 1, maximum: 100}
|
||||
- name: state
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [pending, published, failed, superseded]
|
||||
- name: variant
|
||||
in: query
|
||||
schema: {type: string, maxLength: 64}
|
||||
- name: source_entry_id
|
||||
in: query
|
||||
schema: {type: string}
|
||||
responses:
|
||||
"200":
|
||||
description: Publications
|
||||
|
||||
/v1/hall-entries/{entry_id}:
|
||||
/v1/channel-posts/{post_id}:
|
||||
get:
|
||||
operationId: getPublication
|
||||
summary: Publication state for one entry
|
||||
operationId: getPost
|
||||
summary: Publication state for one post
|
||||
parameters:
|
||||
- name: entry_id
|
||||
- name: post_id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string, minLength: 1}
|
||||
|
|
@ -100,4 +151,4 @@ paths:
|
|||
"200":
|
||||
description: Publication state
|
||||
"404":
|
||||
description: This entry has not been published
|
||||
description: This post has not been published
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue