Skip to content

add: Long_Form_Strategy projector + fosse_long_form_strategy option#29

Draft
kraftbj wants to merge 10 commits intotrunkfrom
add/long-form-strategy-projector
Draft

add: Long_Form_Strategy projector + fosse_long_form_strategy option#29
kraftbj wants to merge 10 commits intotrunkfrom
add/long-form-strategy-projector

Conversation

@kraftbj
Copy link
Copy Markdown
Contributor

@kraftbj kraftbj commented Apr 24, 2026

Stacked on #24 — this is Task 3 of the long-form Bluesky strategy SDD.

Fixes DOTCOM-16887
See DOTCOM-16810 (parent epic)

What this adds

  • `Automattic\Fosse\Long_Form_Strategy` at `src/class-long-form-strategy.php`. Option-backed projector, same shape as the existing `Object_Type` projector but opinionated: unset / empty / unknown values coerce to `'teaser-thread'` (FOSSE's default) rather than passing through. Upstream Atmosphere's own default stays `'link-card'`.
  • Wired into `fosse.php` on `init` with the same `class_exists` guard pattern as `Object_Type`.
  • 8 unit tests covering every enum value, the unset/unknown/empty coercion invariants, and the upstream-override contract. `composer run-script test-php` → 21/21 green.
  • `composer run-script lint-php` → clean.

Forward-compatible, no-op until upstream lands

The filter the projector hooks (`atmosphere_long_form_composition`) is added by Automattic/wordpress-atmosphere#34 — still draft. Until that merges and Task 2 refreshes `bundled/atmosphere/`, nothing in the current bundled copy ever fires the filter, so this projector registers a callback no one calls. Harmless. When upstream lands, this just works — no FOSSE change needed beyond the `tools/sync-bundled.sh` refresh.

Why stack on #24

  • SDD: long form Bluesky strategy (exploration spec) #24 is the SDD — spec, plan, and now the implementation notes at `sdd/long-form-bluesky-strategy/implementation.md`. The plan at Task 3 specifies exactly this file shape; reviewers can cross-reference without context-switching.
  • Keeping it out of trunk until upstream lands avoids shipping a dangling `fosse_long_form_strategy` option users could set but that wouldn't do anything.

Staying as a draft

Per session convention — no reviewers pinged on this or the parent PRs.

kraftbj added 9 commits April 22, 2026 10:36
Per the 2026-04-23 call with Jim Ray (Bluesky devrel), standard.site
native rendering is S-tier on Bluesky's roadmap but months out and
multi-iteration. That changes the trade-off from "short bridge with
Option 2 until Option 3 lands" to "long bridge," making Option 5's
upstream cost worth paying for v1.

spec.md:
- Flip Recommended v1 from Option 2 to Option 5.
- Promote Option 5 analysis (SELECTED), downgrade Option 2 to opt-in.
- Rewrite Technical Details around sequential-writes-with-rollback,
  ordered-array post meta, and filterable 2-post default composition.
- Refresh Open Questions (composition details, 3-post variant, rollback
  observability) and add the Jim Ray call + "clearly better" criteria
  to Open Questions Resolved.

plan.md (new):
- Five tasks: one upstream Atmosphere PR (composition + Publisher
  redesign), bundle refresh, FOSSE projector, e2e thread capture, docs.
- Each task has file-level precision and TDD-style commit granularity
  so a fresh agent can execute from the plan.

Paired with DOTCOM-16810 (now Todo). Part of the DOTCOM-16795 epic.
Two review passes on the long-form Option 5 SDD (plan-executability and
AT-Protocol-correctness) surfaced a handful of real issues. This commit
folds in the confident fixes; remaining decisions are flagged for
confirmation on PR #24.

spec.md:
- Reframe atomicity: the constraint isn't "client-side CID computation
  required" — it's "reply records need the parent's CID which only
  arrives via server response." Two-call batching isn't possible for a
  chained thread because each post's parent CID is unknown until commit.
- Architecture/diagram: rename Publisher's composition entry point to
  build_long_form_records() and make the short/long branch explicit.
- Thread write semantics: partial-meta writes after each successful
  create (crash-recovery guard); createdAt stamped at write time per
  post (not pre-computed); langs inherited consistently across the
  thread; facet byte-offsets computed against each post's own text.
- 3-post thread refs clarified: reply.parent chains (post 3 parents
  post 2, not root).
- Post meta shape: single array of {uri, cid, tid} triples under
  _atmosphere_bsky_thread_records, not parallel arrays. Single-value
  legacy keys preserved as root mirrors.
- Publisher::update() limitations documented: orphans replies from
  other users; resets in-feed timestamp. Both go in the readme.txt
  changelog.
- File Changes: one upstream PR (not two); drop class-post-meta.php;
  capture helper switches from transition_post_status to
  pre_http_request; add AGENTS.md work.
- Open Questions pared to the two actually-open items (3-post default
  composition; atomic-write upgrade path); everything else moved to
  Resolved with "applied (confirm)" markers so Kraft can redirect from
  PR #24 without rewriting the plan.

plan.md (full rewrite):
- Task 1 opens as a draft PR first so upstream review runs in parallel.
- Commit-level TDD structure preserved.
- New method names (build_long_form_records / build_teaser_thread /
  build_truncate_link_text / truncate_at_word_boundary) with exact
  signatures.
- Publisher tasks explicitly address store_results() and
  update_document_bsky_ref() helpers.
- Task 3 (projector) coerces unset/unknown to 'teaser-thread' — opinionated
  default, documented divergence from Object_Type's pass-through.
- Task 4 (e2e) rewrites the capture mu-plugin as a pre_http_request
  interceptor; existing specs get shape updates.
- File-placement guidance fixed (META_THREAD_RECORDS lives on Post);
  fosse.php wiring uses the existing anonymous-function + class_exists
  pattern; CHANGELOG.md reference dropped (FOSSE uses readme.txt).
Per Kraft's 2026-04-23 review of the pickup comment:

- The final prose cut before the CTA (v1's post 1 hook) clamps at a
  sentence boundary — last `.`/`!`/`?` (with optional trailing close-
  quote/bracket) ≤ 280 graphemes. Word boundary is the fallback only
  when no sentence break exists in the window; mid-word is a last
  resort for single very long words.
- In a future 3+-post variant, intermediate body-to-body cuts can be
  word-boundary (mid-word permitted); the sentence-boundary rule only
  applies to the last body post before the CTA.
- Upstream `atmosphere_long_form_composition` default remains
  'link-card'. The teaser-thread opinion lives entirely in FOSSE's
  projector; if the Atmosphere plugin team wants to change the upstream
  default later, that's their call to make separately.

Spec:
- Composition (2-post default) and Option 5's composition narrative now
  specify sentence-first with word fallback.
- Key Components table renames the helper to `truncate_to_budget(text,
  max, prefer_sentence = true)` — sentence → word → hard-cap.
- Open Questions Resolved gets the 2026-04-23 calls explicitly.

plan.md:
- Task 1 Commit 1: `truncate_to_budget()` signature + implementation
  sketch with the `$prefer_sentence` parameter and fallback chain.
- `build_truncate_link_text()` uses `$prefer_sentence = false` (word
  boundary is enough for a single-post strategy where the permalink
  follows immediately).
- `build_teaser_thread()` uses `$prefer_sentence = true` for the hook
  and notes the 3-post variant's boundary rule as a filter-author
  responsibility.
- Task 1 Commit 2: tests split to cover sentence-first, trailing-close-
  punctuation, no-sentence fallback, word-only mode, hard-cap.
- Task 4 e2e: assert the hook ends at sentence-closing punctuation.
Kraft's 2026-04-23 follow-up on the PR #24 pickup comment:

- Excerpts (`$post->post_excerpt`) win as the hook source when set.
  User-curated copy beats a machine body-prefix truncation. Body path
  stays as the fallback, with sentence-boundary clamping.
- Empty / whitespace-only body AND no excerpt → degrade to link-card
  composition for that post only (site option unchanged); emit a
  notice/log so ops can tell the fallback from an intentional
  link-card configuration. Threshold: < 10 graphemes of rendered plain
  text.
- `atmosphere_transform_bsky_post` fires per thread record, not once
  per WP post. Consistent treatment; behavior change documented in
  changelog. No auto-elision.

spec.md:
- Composition (2-post default) captures excerpt precedence + empty-
  body fallback + per-record filter semantics.
- Option 5 composition block tightens the hook precedence order.
- Open Questions Resolved gets the three new decisions with their
  resolver attribution.

plan.md:
- Task 1 Commit 1 step 4 (`build_teaser_thread()`) now reads the
  excerpt first; step 5 (`build_long_form_records()`) has the empty-
  body degradation guard and documents the per-record filter change.
- Task 1 Commit 2 tests add excerpt-as-hook and empty-body-degrades-
  to-link-card cases.
Base automatically changed from sdd-long-form-bluesky-strategy-DOTCOM-16810 to trunk April 24, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant