crucible wiki deploy decided a page had drifted by hashing NodeBB's stored
copy and comparing it to the hash of the text Crucible rendered. Those match
only if NodeBB gives our HTML back byte for byte. It does not, so every sow-topdata tag deploy failed:
local pages: 1200, updated: 1, skipped: 1176, drifted: 23
remote managed wiki content drifted; rerun with --force to overwrite
Nobody had edited those pages. The operator's only way out was to leave --force on, which removes the protection the guard exists for.
What changed
New manifest field remote_hash: the managed-region hash of the post NodeBB
hands back right after we write it. Drift compares against that, so it means
"the live page changed after we last wrote it".
A post with no sourceContent is not drift. It predates sourceContent
sync, reads back as rendered HTML, and belongs to the existing SourceContentSynced repair — which the drift refusal used to block.
The error names the drifted pages, capped at ten plus a count.
Deleted the dead wikiDeployPlan.RemoteHash field.
Old manifests keep the previous comparison until each page is next written, so
no re-seed is needed. Cost: one extra post read per page written. Pages that
skip are still never fetched.
Tests
internal/topdata/wiki_deploy_test.go, same seam as the rest of the file
(DeployWikiWithOptions against the fake NodeBB): normalized remote copy is not
drift, a hand-edited page still is and the error names it, --force overwrites
it and re-records the hash, a post without sourceContent is repaired instead
of refused, and an old-format manifest deploys and gains a remote_hash.
Fixes #103.
## What was wrong
`crucible wiki deploy` decided a page had drifted by hashing NodeBB's stored
copy and comparing it to the hash of the text Crucible rendered. Those match
only if NodeBB gives our HTML back byte for byte. It does not, so every
`sow-topdata` tag deploy failed:
```
local pages: 1200, updated: 1, skipped: 1176, drifted: 23
remote managed wiki content drifted; rerun with --force to overwrite
```
Nobody had edited those pages. The operator's only way out was to leave
`--force` on, which removes the protection the guard exists for.
## What changed
- New manifest field `remote_hash`: the managed-region hash of the post NodeBB
hands back right after we write it. Drift compares against that, so it means
"the live page changed after we last wrote it".
- A post with no `sourceContent` is not drift. It predates `sourceContent`
sync, reads back as rendered HTML, and belongs to the existing
`SourceContentSynced` repair — which the drift refusal used to block.
- The error names the drifted pages, capped at ten plus a count.
- Deleted the dead `wikiDeployPlan.RemoteHash` field.
Old manifests keep the previous comparison until each page is next written, so
no re-seed is needed. Cost: one extra post read per page written. Pages that
skip are still never fetched.
## Tests
`internal/topdata/wiki_deploy_test.go`, same seam as the rest of the file
(`DeployWikiWithOptions` against the fake NodeBB): normalized remote copy is not
drift, a hand-edited page still is and the error names it, `--force` overwrites
it and re-records the hash, a post without `sourceContent` is repaired instead
of refused, and an old-format manifest deploys and gains a `remote_hash`.
`make check` passes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The drift guard hashed NodeBB's stored copy of a page and compared it to the
hash of the text Crucible rendered. Those only match if NodeBB round-trips our
HTML byte for byte, so every sow-topdata deploy failed with "remote managed
wiki content drifted; rerun with --force" on pages nobody had edited.
Record the managed-region hash of the post NodeBB hands back right after each
write (manifest field remote_hash) and compare the next run against that. A
post with no sourceContent is no longer drift either: it predates sourceContent
sync, so it reads back as rendered HTML and belongs to the existing repair
path. The error now names the drifted pages, capped at ten.
Old manifests without remote_hash keep the previous comparison, so no re-seed
is needed. Costs one extra post read per page written; skipped pages are still
never fetched.
Closes#103
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
archvillainette
scheduled this pull request to auto merge when all checks succeed 2026-08-07 21:00:25 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes #103.
What was wrong
crucible wiki deploydecided a page had drifted by hashing NodeBB's storedcopy and comparing it to the hash of the text Crucible rendered. Those match
only if NodeBB gives our HTML back byte for byte. It does not, so every
sow-topdatatag deploy failed:Nobody had edited those pages. The operator's only way out was to leave
--forceon, which removes the protection the guard exists for.What changed
remote_hash: the managed-region hash of the post NodeBBhands back right after we write it. Drift compares against that, so it means
"the live page changed after we last wrote it".
sourceContentis not drift. It predatessourceContentsync, reads back as rendered HTML, and belongs to the existing
SourceContentSyncedrepair — which the drift refusal used to block.wikiDeployPlan.RemoteHashfield.Old manifests keep the previous comparison until each page is next written, so
no re-seed is needed. Cost: one extra post read per page written. Pages that
skip are still never fetched.
Tests
internal/topdata/wiki_deploy_test.go, same seam as the rest of the file(
DeployWikiWithOptionsagainst the fake NodeBB): normalized remote copy is notdrift, a hand-edited page still is and the error names it,
--forceoverwritesit and re-records the hash, a post without
sourceContentis repaired insteadof refused, and an old-format manifest deploys and gains a
remote_hash.make checkpasses.🤖 Generated with Claude Code