Skip to content

Phase 05 M2: block-range locator and opt-in citation write routes - #104

Merged
dknauss merged 1 commit into
mainfrom
claude/keen-tesla-49orwb
Sep 26, 2026
Merged

dknauss merged 1 commit into
mainfrom
claude/keen-tesla-49orwb

Conversation

@dknauss

@dknauss dknauss commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Summary

What changed?

Phase 05, M2: the first REST routes that write bibliographies, as planned in the design memo (Tier 2).

1. Block-range locator (includes/block-locator.php)

  • Finds each bibliography block's exact byte range in post_content. It uses WP_Block_Parser's delimiter grammar and the same document-order indexing as the read routes' {index}, so nested blocks are handled.
  • Refuses malformed structure with 409: a stray closer, a mismatched closer, or a block left unclosed. A write never guesses where a block ends.
  • Splices one block in place, then re-locates every block to prove that no other byte moved.
  • Tested against a vendored copy of core's real WP_Block_Parser (tests/phpunit/wp-block-parser/, GPL, test-only), not the bootstrap's canned parse_blocks() mock. So it's checked on every push, not only in the runtime matrix.

2. Citation write routes (includes/write-routes.php)

The routes are off by default. A site opts in with add_filter( 'bibliography_builder_enable_write_routes', '__return_true' );, typically from an mu-plugin; this filter is the design memo's "companion-plugin flag". They live under /wp-json/bibliography/v1/posts/<id>/bibliographies/<ref>/citations:

Method Path Does
POST …/citations Add 1–50 CSL-JSON items. Duplicates, by the editor's rules, are skipped and reported.
PATCH …/citations/<citation_id> Change fields. null removes a field; id and type are locked. Works like the editor's field editor.
DELETE …/citations/<citation_id> Remove one citation. The response returns the whole entry, so it can be restored.
PUT …/citations/order Reorder. Numeric styles only; others return 409.

Every route:

  • needs edit_post on the post;
  • is a dry run unless dry_run=false;
  • needs If-Match with the post's ETag to write: 428 without it, 412 when it's stale. The ETag is a hash of the content, not post_modified_gmt, so two saves in the same second can't collide.

A write rebuilds the block's markup with the PHP port of save(), splices only that block, and saves through wp_update_post() as an ordinary revision. While the routes are on, the read routes send the ETag.

Docs:

  • docs/rest-write-routes.md: enabling, routes, responses, and caveats;
  • the design memo (M2 marked done, with the decisions made);
  • CLAUDE.md, the README, the CHANGELOG, and the metrics.

Why? The design memo's M2. Its prerequisites are now all on main: the PHP save() port (#97), locale-independent markup (#98, #99), and full citation styles (#103).

Known gap: BibTeX and BibLaTeX export strings need citation-js, so only the editor computes them. With per-entry Cite / Export on, entries written over REST show RIS and CSL-JSON links until the post is next saved in the editor, which then adds the missing strings. This is documented.

Validation

  • composer test:php passes (310). It includes 9 locator tests against the real parser and 14 write-route tests. The route tests assert that every written block's markup is byte-identical to what save() renders for its attributes. The suite also passes with --order-by=random.

  • composer lint:php and composer analyze:php pass.

  • npm run lint:js and npm run test (872, also with --randomize) pass. No JS changed.

  • verify-metrics.sh passes.

  • Tested end to end in a real WordPress: Playground CLI 3.1.40, WordPress trunk, this build mounted, and the routes enabled by an mu-plugin. The steps ran through core's REST server with rest_do_request():

    • read the ETag;
    • dry-run an add (the post was unchanged);
    • 428 without If-Match, 412 with a stale one;
    • a real add, which skipped a duplicate within the same request;
    • PATCH, DELETE, and PUT on three different blocks;
    • PUT on an APA list returned 409;
    • 5 revisions were recorded.

    Afterwards the block editor loaded all 44 blocks as valid, with no unsaved changes and no page errors.

Checklist

  • Linked issue or explained why none was needed: Phase 05 M2 in .planning/phases/05-writable-bibliography-rest/05-DESIGN-MEMO.md.
  • No sensitive details disclosed publicly

🤖 Generated with Claude Code

https://claude.ai/code/session_01KbVuMPN7YTcV27vwsG3LZ2


Generated by Claude Code

includes/block-locator.php finds each bibliography block's exact byte
range in post_content with WP_Block_Parser's delimiter grammar (same
document-order indexing as the read routes), refuses malformed
structure, and splices one block in place, re-locating afterwards to
prove no other byte moved. Tested against a vendored copy of core's
real parser (tests/phpunit/wp-block-parser/).

includes/write-routes.php adds, when a site opts in with the
bibliography_builder_enable_write_routes filter:
  POST   …/bibliographies/<ref>/citations            add (skips duplicates)
  PATCH  …/bibliographies/<ref>/citations/<id>       change fields
  DELETE …/bibliographies/<ref>/citations/<id>       remove, returns entry
  PUT    …/bibliographies/<ref>/citations/order      reorder (numeric)
All need edit_post, are dry runs unless dry_run=false, and need
If-Match with the post's content-hash ETag (428 missing, 412 stale).
A write rebuilds the markup with the save() port, splices only that
block, and saves through wp_update_post() as a normal revision. The
read routes send the ETag while writes are enabled.

Docs: docs/rest-write-routes.md; design memo, CLAUDE.md, README,
CHANGELOG, metrics updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbVuMPN7YTcV27vwsG3LZ2
@dknauss
dknauss marked this pull request as ready for review September 26, 2026 17:21
@codecov

codecov Bot commented Sep 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.87305% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.20%. Comparing base (7fb1372) to head (674b964).

Files with missing lines Patch % Lines
includes/write-routes.php 91.32% 30 Missing ⚠️
includes/block-locator.php 97.75% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #104      +/-   ##
==========================================
+ Coverage   86.75%   87.20%   +0.45%     
==========================================
  Files          52       54       +2     
  Lines        5406     5847     +441     
  Branches      597      597              
==========================================
+ Hits         4690     5099     +409     
- Misses        235      267      +32     
  Partials      481      481              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dknauss
dknauss merged commit 685a083 into main Sep 26, 2026
16 checks passed
@dknauss
dknauss deleted the claude/keen-tesla-49orwb branch September 26, 2026 17:30
dknauss pushed a commit that referenced this pull request Sep 26, 2026
Full GPL CSL styles and locales (#103), Phase 05 M0-M3 (stable IDs,
review routes and abilities, opt-in citation and block write routes:
#104, #106), the save-markup language fix, reference-manager .bib
fixes, the Playground demo page (#102), and the runtime matrix on the
release package with formatter-parity and write-route checks (#105).

Version bumped in the plugin header, block.json, readme.txt Stable tag,
the POT header, and the package manifests. CHANGELOG [Unreleased] is
dated as 1.7.0; readme.txt gains a user-facing changelog entry, an
Upgrade Notice, and an FAQ note on the opt-in write routes. README
highlights and STATE/ROADMAP move to the 1.7.0 baseline.

Tested up to stays 7.1: WordPress latest is 7.1.2, which CI's runtime
matrix exercises, including formatter parity across all nine styles.

package-release.sh now also prunes .git directories from vendor/. When
Composer falls back from dist to a git clone (as it did in this
sandbox), the ZIP otherwise carried 4 MB of git metadata.

Release checklist gates run locally: lint:js, lint:css, lint:i18n,
npm audit (0), composer audit (none), Jest 873 passed / 2 skipped,
PHPUnit 318 OK (the 4 vendor deprecations already on main), build,
package:release (552,718-byte ZIP). citeproc-php v2.7.1 is current.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbVuMPN7YTcV27vwsG3LZ2
@dknauss dknauss mentioned this pull request Sep 26, 2026
9 tasks done
dknauss added a commit that referenced this pull request Sep 26, 2026
* chore(release): 1.7.0

Full GPL CSL styles and locales (#103), Phase 05 M0-M3 (stable IDs,
review routes and abilities, opt-in citation and block write routes:
#104, #106), the save-markup language fix, reference-manager .bib
fixes, the Playground demo page (#102), and the runtime matrix on the
release package with formatter-parity and write-route checks (#105).

Version bumped in the plugin header, block.json, readme.txt Stable tag,
the POT header, and the package manifests. CHANGELOG [Unreleased] is
dated as 1.7.0; readme.txt gains a user-facing changelog entry, an
Upgrade Notice, and an FAQ note on the opt-in write routes. README
highlights and STATE/ROADMAP move to the 1.7.0 baseline.

Tested up to stays 7.1: WordPress latest is 7.1.2, which CI's runtime
matrix exercises, including formatter parity across all nine styles.

package-release.sh now also prunes .git directories from vendor/. When
Composer falls back from dist to a git clone (as it did in this
sandbox), the ZIP otherwise carried 4 MB of git metadata.

Release checklist gates run locally: lint:js, lint:css, lint:i18n,
npm audit (0), composer audit (none), Jest 873 passed / 2 skipped,
PHPUnit 318 OK (the 4 vendor deprecations already on main), build,
package:release (552,718-byte ZIP). citeproc-php v2.7.1 is current.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbVuMPN7YTcV27vwsG3LZ2

* Parse each CSL style once per format request

CiteProc::render() calls init() every time, which builds a new Context,
reads and parses the locale file, and rebuilds the style's object tree:
about 3 ms, against well under 1 ms to render an entry. With one render
per entry (the fix that stopped an "et al." entry from dropping the
next entry's "and"), 50 entries took about 90 ms, against about 8 ms in
1.6.0 with its short stand-in styles.

Bibliography_Builder_Reusable_CiteProc parses once, records every
property of every object in the parsed style tree, and before each later
render restores them, resets the Context's per-render lists, and zeroes
Layout's static cited-item counter. Each entry renders against the state
a fresh parse would give it: 50 entries now take about 25 ms.

The nine style goldens still match byte for byte. A new test formats the
corpus in reverse order, putting the "et al." and APA 21+ entries ahead
of the short author lists, and requires every entry to match its golden
line; with the restore disabled, both the goldens and the new test fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbVuMPN7YTcV27vwsG3LZ2

* Drop the #[\Override] attribute from the reusable formatter

PHP 7.4 reads an attribute on its own line as a comment (php -l on
7.4 passes, and so does the PHP 7.4 runtime smoke), but Copilot flagged
it as a 7.4 parse error, and compatibility scanners can do the same. It
does nothing at runtime, so drop it, and turn off Psalm's
MissingOverrideAttribute, which cannot apply to a plugin that supports
PHP 7.4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbVuMPN7YTcV27vwsG3LZ2

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants