v1.10.1 — Per-environment ids block in page.meta.json + path-first pushPage#3
Draft
PeterKnightDigital wants to merge 3 commits into
Draft
v1.10.1 — Per-environment ids block in page.meta.json + path-first pushPage#3PeterKnightDigital wants to merge 3 commits into
ids block in page.meta.json + path-first pushPage#3PeterKnightDigital wants to merge 3 commits into
Conversation
Adds new helpers in SyncManager — buildIdsBlock(env, pageId, existing?)
and readEnvId(meta, env) — and threads them through every PHP write
site that touches page.meta.json.
The ids block:
"ids": {
"local": { "id": 1234, "lastSeenAt": "..." },
"remote": { "id": 5678, "lastSeenAt": "..." }
}
Each write populates only its own slot, never overwrites the other.
Top-level meta.pageId is kept as a back-compat mirror for tooling we
don't own that reads it directly. The mirror always equals the slot
just written by the current operation — same behaviour the legacy
single-slot had.
Write sites updated:
- pullPage: writes ids.local, preserves any pre-existing
ids.remote. Reads any existing meta first to
do the merge.
- exportPageYaml: emits ids.remote in the export payload. From the
receiver's perspective this id IS the remote id —
labelling it correctly at source means the
receiver doesn't need to know which environment
it pulled from.
- initPageMeta: writes ids.local in the 'linked' (existing page)
branch. The 'new' scaffold branch keeps pageId:
null (no real id yet) and gets the slot
populated by the publishPage writeback after
the page is actually created.
- publishPage: writes ids.local after a successful create,
preserving any pre-existing ids.remote.
- pushPage: on successful save, refreshes ids.local
(lastSeenAt advances; new id recorded if it
differs from the meta's last-seen).
Path-first refactor of pushPage:
= $wire->pages->get($meta['canonicalPath']);
$expected = $meta['ids']['local']['id'] ?? null;
if ($expected && $page->id !== $expected && !$force) {
// refuse with structured error: expectedId / currentId /
// canonicalPath / hint pointing at force:true
}
Same path-first / id-as-sanity-check pattern the page-assets work in
v1.10.0 already uses. Catches: page deleted-and-recreated at the same
path, slug rebound to an unrelated page, meta from a different site.
Falls back to id when the path lookup fails (covers legacy metas
without canonicalPath, plus the rare moved-without-reconcile case).
getPageSyncStatus and sync:reconcile updated with the same rule so
pw_sync_status and pw_sync_reconcile give identical results regardless
of which side last wrote the meta. pw_sync_status results now also
include the ids block when present.
Validated with 16 reflection-driven PHP tests covering buildIdsBlock
(fresh writes, preserve other side, defensive bad env names) and
readEnvId (new format, legacy fallback for local, null for remote
when only legacy data exists, malformed inputs).
Co-authored-by: PKDigital <PeterKnightDigital@users.noreply.github.com>
Two TS-side changes that close the v1.10.1 loop:
pullPageFromRemote (mcp-server/src/pages/puller.ts):
Previously wrote the entire remote-emitted meta over any existing
page.meta.json, which would have wiped the new ids.local slot from
a prior local pull. Now reads the existing meta first, merges it
with the payload by:
- preserving existing ids.local
- stripping any stray ids.local from the remote payload
(defensive — page:export-yaml on v1.10.1+ only emits
ids.remote, but an older or misconfigured endpoint could
include local; we don't trust the remote to know about our
local slot)
- promoting legacy top-level pageId to ids.local for pre-v1.10.1
metas so the value isn't lost on this remote pull
- falling through to writing the payload as-is when the existing
meta is missing or malformed (rather than failing the pull)
pushPage / publishPage (mcp-server/src/pages/pusher.ts):
After a successful live remote push, reads the pageId from the API
response (page:update / page:create both return it) and writes
ids.remote into the local meta. Best-effort, dry-run-aware, never
fails the push. The local meta now learns the remote id from a
remote push without needing a follow-up pull — closing the symmetric
case of pullPageFromRemote learning the local id from disk.
Validated with 9 Node tests covering the merge logic: no-existing-meta
passes payload through; existing v1.10.1 ids.local is preserved; legacy
top-level pageId is promoted to ids.local; stray payload ids.local is
stripped; the no-local-available case leaves ids.local absent rather
than guessing.
Co-authored-by: PKDigital <PeterKnightDigital@users.noreply.github.com>
Module + MCP server bumped 1.10.0 → 1.10.1 so the version Cursor reports matches the new ids-block + path-first pushPage feature set. ROADMAP: removes the deferred 'follow-up planned for v1.10.1' section from inside the v1.10.0 entry, replaces it with a full v1.10.1 release entry covering the format change, the pushPage refactor, the legacy migration semantics, and the validation results. CHANGELOG: new v1.10.1 entry above v1.10.0 covering the same ground in release-note form. Co-authored-by: PKDigital <PeterKnightDigital@users.noreply.github.com>
cursor Bot
pushed a commit
that referenced
this pull request
Apr 30, 2026
…st pushPage Closes the meta format gap deliberately deferred from v1.10.0. Replaces the single top-level meta.pageId slot with an additive ids block (local / remote, each with id + lastSeenAt). Each meta-write site populates only its own slot; the other side is preserved verbatim. Top-level pageId is kept as a back-compat mirror. Refactors SyncManager::pushPage to resolve pages by canonical path with the last-seen ids.local.id used as a sanity check (refuses path/id mismatches with a structured error pointing at force:true). Same path-first cross-environment safety the page-assets work in v1.10.0 already gave us, applied uniformly to page content. getPageSyncStatus and sync:reconcile updated to the same rule. MCP-side pullPageFromRemote now MERGES the remote payload with any existing local meta — preserving ids.local, stripping stray payload ids.local, promoting legacy top-level pageId for pre-v1.10.1 metas. pushPage and publishPage record ids.remote in the local meta after a successful live remote operation, closing the loop. Migration is automatic and additive: older metas keep working; readEnvId falls back to legacy pageId for local; the first write on v1.10.1+ adds the ids block. Validated with 25 tests (16 PHP, 9 Node), all pass. See PR #3 for the full release notes. Co-authored-by: PKDigital <PeterKnightDigital@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on top of #2 (v1.10.0). Base branch is
cursor/page-assets-sync-2d5b. Merging this PR after #2 lands.Summary
Closes the meta format gap that v1.10.0 explicitly deferred. Prior to v1.10.1,
page.meta.jsonhad a single top-levelpageIdslot:meta.pageId = 1234(local id).meta.pageIdsilently switches to5678(remote id).pw_page_pushcalls$wire->pages->get($meta['pageId'])and addresses the wrong page on local. Or fails with "Page not found". Or — worst case — addresses an unrelated page that happened to have id5678locally.The meta — your persistent record of the page — also had nowhere to record the local id and the remote id side by side, so an at-a-glance "which physical id corresponds to this page on each environment?" required re-resolving on both sides every time.
The format change
page.meta.jsongains anidsblock alongside the existing top-levelpageId(kept as a back-compat mirror):{ "pageId": 1234, "canonicalPath": "/about/", "ids": { "local": { "id": 1234, "lastSeenAt": "2026-04-30T15:00:00+00:00" }, "remote": { "id": 5678, "lastSeenAt": "2026-04-30T15:30:00+00:00" } } }Each meta-write site populates only its own slot. The other side's slot is preserved verbatim. Five write paths cover every code route in / out of the meta:
SyncManager::pullPage(local pull): writesids.local, preserves any pre-existingids.remote.SyncManager::exportPageYaml(remote-pull payload): emitsids.remote(from the receiver's perspective, this id IS the remote one — labelling it correctly at source means the receiver doesn't need to know which environment it pulled from).SyncManager::initPageMeta: writesids.localfor "linked" (existing page) scaffolds.SyncManager::publishPage: writesids.localafter the page is actually created on this site by apw_page_publishwriteback. Preserves any pre-existingids.remote.SyncManager::pushPagewriteback: refreshesids.localafter a successful save (lastSeenAtadvances; new id recorded if it differs from the meta's last-seen).Two more write paths handle the closing-the-loop case for remote pushes from the MCP side:
pushPage(mcp-server/src/pages/pusher.ts): after a successful live remote push, reads thepageIdfrom the API response and writesids.remoteinto the local meta. Best-effort, dry-run-aware, never fails the push.publishPageremote branch: same recording forpage:createresponses.And the critical merge fix on the puller side:
pullPageFromRemote(mcp-server/src/pages/puller.ts): merges the remote-emitted payload with any existing local meta — preservingids.local, stripping any strayids.localan older payload might include, and promoting legacy top-levelpageIdtoids.localfor pre-v1.10.1 metas. Without this merge apw_page_pull source: "remote"would still wipe the local id slot, defeating the whole point.The
pushPagerefactorThis is the critical write-path change v1.10.0 deliberately deferred:
ids.local.id, the push is refused with a structured error pointing the operator atforce:true. Catches: page deleted-and-recreated at the same path, slug rebound to an unrelated page, meta from a different site entirely.pageIdis the fallback when a meta has nocanonicalPath(covers older pulls and the rare moved-without-reconcile case).getPageSyncStatus(poweringpw_sync_status) andsync:reconcileare updated with the same path-first / id-fallback rule so all three read sites give identical results regardless of which side last wrote the meta.pw_sync_statusresults now also include the per-environmentidsblock when present.Migration
Zero manual steps. Older
page.meta.jsonfiles without theidsblock keep working:readEnvId(meta, 'local')falls back to top-levelpageId(the dominant historical case — almost every existing meta on disk was last written by a local pull).readEnvId(meta, 'remote')returnsnullfor legacy metas — callers that need a remote id fall back to a path lookup, which is what the new code path does anyway.idsblock.pageIdtoids.localbefore merging in the remote payload.Any caller that was passing a now-misaddressed
meta.pageIdand getting "Page not found" will instead get a successful path-based resolution OR a clear id-mismatch diagnostic — both improvements over the silent breakage they were getting before.Validation
buildIdsBlock: fresh writes, preserve other side, defensive bad env names, key-stable output for byte-identical writes.readEnvId: new format, legacy fallback for local, null for remote when only legacy data exists, malformed / empty / zero inputs.pullPageFromRemote:ids.localis preserved across the merge.pageIdis promoted toids.local.ids.localis stripped (defensive).ids.localabsent rather than guessing.npm run build(TypeScript strict) passes.php -lclean on every modified PHP file.All 25 tests pass.
Files changed
src/Sync/SyncManager.php— newbuildIdsBlock/readEnvIdhelpers;pullPage,exportPageYaml,initPageMeta,publishPageandpushPagewrites wired through them;pushPagelookup refactored to path-first with id verification;getPageSyncStatusandsync:reconcileupdated to the same rule.mcp-server/src/pages/puller.ts—pullPageFromRemotenow merges existing meta with the remote payload (preservesids.local, strips stray local from payload, promotes legacypageId).mcp-server/src/pages/pusher.ts—pushPageandpublishPagenow recordids.remotein the local meta after a successful live remote operation, via a newrecordRemoteIdInMeta()helper.src/Cli/CommandRouter.php,mcp-server/src/index.ts,PromptWire.module.php,mcp-server/package.json— version bump 1.10.0 → 1.10.1.CHANGELOG.md,ROADMAP.md— release entry; the deferred section that lived inside the v1.10.0 entry has been removed (replaced by this release).