You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Reviewed 2026-08-19 in a backlog staleness pass. Still blocked — but the blocker is not quite what this issue says, and #221 (its other dependency) has closed. Two corrections and a design direction added below; everything else stands and remains accurate.
Correction 1 — Replicator does not solve this as assumed
The header says the content store is "forthcoming in the TBD Replicator service". Replicator now exists and permanent replication shipped (replicator#29, closed — GCS per RepSpec, via AsyncGcsDriver.create_if_absent). That does not deliver what this issue needs:
It stores raw fetched bytes, not the extracted text the fingerprint is computed over. Diffing raw HTML is not the same product as diffing extracted content, and this issue's "Diff target" question below is precisely about that choice.
It is per-RepSpec, so coverage is partial. This issue's own acceptance criterion — "Diff availability does not depend on whether the item is Archiver-linked" — fails against a per-RepSpec store the same way it fails against the Archiver-fetch option, and for the same reason.
So "wait for Replicator" is not by itself a plan. Something has to be specified.
Correction 2 — Watcher already holds the text it needs to store
The extracted text exists in memory at apply time — it is what the fingerprint is computed over — and is then discarded. ChangeRevision persists only content_fingerprint, content_size_bytes, schema_version, captured_at.
This means deferred design question 1, option B (denormalize previous extracted text onto ChangeRevision, size-capped and compressed) requires nothing from Replicator and could be built today. It is recorded here as available, deliberately not chosen — see the direction below.
Direction — a Replicator-side sidecar, not a Watcher-side column
The preferred shape is for Replicator to store the full extracted text as a sidecar artifact alongside the blob it already holds, addressed by URI, rather than Watcher freighting text through its own database or the bus.
This follows the pattern already in force. Nothing puts content on Redis today — SourceRevisionObservedEvent on content.revisions carries blob_uri, extracted_fingerprint, spec_fingerprint, sizes and media types, but never the bytes. The bus is claim-check by design; the data plane is the blob store. A sidecar keeps that property, where a denormalized ChangeRevision column moves content into Watcher's Postgres and a bus-carried payload would move it into Redis. Both are the wrong direction.
The wrinkle to resolve, stated plainly: Replicator does not extract. It fetches raw bytes; extraction is Watcher's, because Watcher holds source_specs. So a sidecar requires either
Watcher writes the sidecar into Replicator's store after extraction — needs a write path into a store Watcher currently only reads from, and blob_uri is a host-local file:// by contract, or
Replicator gains extraction — a substantial boundary move that would need source_specs on the fetch command, and contradicts the current split.
The first is much the smaller change and fits replicator#7 (Object-store blob backend (claim-check) — remove the shared-filesystem data-plane coupling), which is already about giving the blob store a real addressable backend rather than a VM-local path. That issue is the natural place for this to land, and this issue should not be unblocked until it does.
Retention is the other half: blobs today are deliberately transient (TTL sweeper, BlobUsage ceiling). A diff sidecar must outlive its blob by at least one check interval, which is a different retention class and needs stating in whatever contract change carries it.
Status of the original dependency
#221 is closed (2026-07-10) — the dead surface this issue restores has been stripped as planned. The 89d64eb restore anchor below remains valid.
Blocked by: a persistent content store — forthcoming in the TBD Replicator service. Depends on:#221, which removes the dead surface this issue restores.
Context
Phase 5 (#156) dropped the Snapshot/Change tables and the diff engine. ChangeRevision
stores a content fingerprint and nothing else (change_revision.py),
so at dispatch time watcher has no previous content to diff against.
The notification UI kept offering Diff snippet, Full diff, and Significance score
anyway. notify.py:188 hard-codes unified_diff = "",
and real change_detected metadata never carries significance / added / modified / removed, so all three toggles have been inert in production since #156. Only the preview
appeared to work, because it was fed canned fixtures.
#221 strips that surface: the toggles, the ContentOptions fields, and the now-unreachable
plumbing all come out. This issue restores the capability once there is somewhere to read
previous content from.
Watcher is, for now, a fingerprint-shift notifier: it reports that a page changed,
not what changed.
Restore anchor
All removed code is preserved in git at 89d64eb (the commit immediately preceding the #221 strip). Restore from there rather than rewriting — in particular _truncate_unified_diff_lines is hunk-boundary-aware and carries real test coverage that
would be tedious to reconstruct.
git show 89d64eb:src/core/notifications/content.py
git show 89d64eb:src/api/schemas/content_config.py
git show 89d64eb:src/core/notifications/preview_fixtures.py
the corresponding branches in _parse_content_config_from_form and the preview route
What restoring actually requires
Restoring is not re-adding the checkboxes. Three separate problems, in ascending cost:
Previous content must be readable at dispatch time. Today it exists only as an
Archiver SourceRevision, plus a TTL-bounded scratch file (WATCHER_CACHE_DIR,
default 600 s) that is written only when archiver_info_source_id is set —
dashboard-created WatchedItems have none. This is what Replicator is expected to solve.
Chunk semantics are a rebuild, not a restore.added / modified / removed / significance / chunks_changed were produced by the diff engine Phase 5 deleted.
Nothing in the current codebase computes them. Treat as a separate scope.
Deferred design questions
Storage source. Read previous text from Replicator/Archiver at change time (a network
hop on the change path), or denormalize previous extracted text onto ChangeRevision
(size-capped, compressed)? The Archiver-fetch option silently yields no diff for
WatchedItems without archiver_info_source_id — a worse failure mode than no diff at all,
so whatever Replicator exposes must cover every item.
Diff target. Extracted text (what the fingerprint is computed over) or raw bytes?
Does significance come back at all, or is a unified diff sufficient signal?
Correction 1 — Replicator does not solve this as assumed
The header says the content store is "forthcoming in the TBD Replicator service". Replicator now exists and permanent replication shipped (replicator#29, closed — GCS per RepSpec, via
AsyncGcsDriver.create_if_absent). That does not deliver what this issue needs:So "wait for Replicator" is not by itself a plan. Something has to be specified.
Correction 2 — Watcher already holds the text it needs to store
src/workers/pipeline.py:The extracted text exists in memory at apply time — it is what the fingerprint is computed over — and is then discarded.
ChangeRevisionpersists onlycontent_fingerprint,content_size_bytes,schema_version,captured_at.This means deferred design question 1, option B (denormalize previous extracted text onto
ChangeRevision, size-capped and compressed) requires nothing from Replicator and could be built today. It is recorded here as available, deliberately not chosen — see the direction below.Direction — a Replicator-side sidecar, not a Watcher-side column
The preferred shape is for Replicator to store the full extracted text as a sidecar artifact alongside the blob it already holds, addressed by URI, rather than Watcher freighting text through its own database or the bus.
This follows the pattern already in force. Nothing puts content on Redis today —
SourceRevisionObservedEventoncontent.revisionscarriesblob_uri,extracted_fingerprint,spec_fingerprint, sizes and media types, but never the bytes. The bus is claim-check by design; the data plane is the blob store. A sidecar keeps that property, where a denormalizedChangeRevisioncolumn moves content into Watcher's Postgres and a bus-carried payload would move it into Redis. Both are the wrong direction.The wrinkle to resolve, stated plainly: Replicator does not extract. It fetches raw bytes; extraction is Watcher's, because Watcher holds
source_specs. So a sidecar requires eitherblob_uriis a host-localfile://by contract, orsource_specson the fetch command, and contradicts the current split.The first is much the smaller change and fits replicator#7 (Object-store blob backend (claim-check) — remove the shared-filesystem data-plane coupling), which is already about giving the blob store a real addressable backend rather than a VM-local path. That issue is the natural place for this to land, and this issue should not be unblocked until it does.
Retention is the other half: blobs today are deliberately transient (TTL sweeper,
BlobUsageceiling). A diff sidecar must outlive its blob by at least one check interval, which is a different retention class and needs stating in whatever contract change carries it.Status of the original dependency
#221 is closed (2026-07-10) — the dead surface this issue restores has been stripped as planned. The
89d64ebrestore anchor below remains valid.Blocked by: a persistent content store — forthcoming in the TBD Replicator service.
Depends on: #221, which removes the dead surface this issue restores.
Context
Phase 5 (#156) dropped the Snapshot/Change tables and the diff engine.
ChangeRevisionstores a content fingerprint and nothing else (change_revision.py),
so at dispatch time watcher has no previous content to diff against.
The notification UI kept offering Diff snippet, Full diff, and Significance score
anyway. notify.py:188 hard-codes
unified_diff = "",and real
change_detectedmetadata never carriessignificance/added/modified/removed, so all three toggles have been inert in production since #156. Only the previewappeared to work, because it was fed canned fixtures.
#221 strips that surface: the toggles, the
ContentOptionsfields, and the now-unreachableplumbing all come out. This issue restores the capability once there is somewhere to read
previous content from.
Watcher is, for now, a fingerprint-shift notifier: it reports that a page changed,
not what changed.
Restore anchor
All removed code is preserved in git at
89d64eb(the commit immediately preceding the#221 strip). Restore from there rather than rewriting — in particular
_truncate_unified_diff_linesis hunk-boundary-aware and carries real test coverage thatwould be tedious to reconstruct.
Inventory of what #221 removes
include_diff_snippet,diff_snippet_lines,include_diff_full,include_significance_build_diff_text,_render_unified_diff_block,_normalize_unified_diff_lines,_truncate_unified_diff_lines,_format_chunks_changed,_compute_change_summary,_DEFAULT_DIFF_SNIPPET_CAP, thediff_snippet/diff_full/chunks_changed/change_summarycontext keys, and theunified_diff/diff_snippet_capparams onbuild_body+build_template_contextunified_diff = ""stub and its pass-throughcompute_preview_unified_diff,_PREVIEW_PREVIOUS_TEXT,_PREVIEW_CURRENT_TEXT, and theadded/modified/removed/significancefixture keysTemplateVariableentries forchange_summary,added,modified,removed,diff_snippet,diff_full,chunks_changed,significance; andCHANGE_DETECTED_BODY_BLOCK_LINES_parse_content_config_from_formand the preview routeWhat restoring actually requires
Restoring is not re-adding the checkboxes. Three separate problems, in ascending cost:
Archiver
SourceRevision, plus a TTL-bounded scratch file (WATCHER_CACHE_DIR,default 600 s) that is written only when
archiver_info_source_idis set —dashboard-created WatchedItems have none. This is what Replicator is expected to solve.
compute_unified_diffis gone fromsrc/entirely; only the rendering helper survived (and Notification Template UI/UX polish: event labels, Context layout, and strip the dead diff/significance surface #221 removes that too).Cheap to rebuild on
difflibonce (1) lands.added/modified/removed/significance/chunks_changedwere produced by the diff engine Phase 5 deleted.Nothing in the current codebase computes them. Treat as a separate scope.
Deferred design questions
hop on the change path), or denormalize previous extracted text onto
ChangeRevision(size-capped, compressed)? The Archiver-fetch option silently yields no diff for
WatchedItems without
archiver_info_source_id— a worse failure mode than no diff at all,so whatever Replicator exposes must cover every item.
significancecome back at all, or is a unified diff sufficient signal?change_detecteddefault body to the header block.Restoring a diff means re-deciding where it sits and whether it's on by default
(verbose channels vs. Slack).
Acceptance
change_detectedemail with Full diff enabled contains the diff.in Notification Template UI/UX polish: event labels, Context layout, and strip the dead diff/significance surface #221 still passes, now with diff fields on both sides.