Skip to content

Mark dedupe-cap trap pages post-hoc; default-enable --dedupe-cap - #264

Merged
YusukeHirao merged 12 commits into
devfrom
feat/dedupe-cap-post-hoc-marking
Aug 9, 2026
Merged

Mark dedupe-cap trap pages post-hoc; default-enable --dedupe-cap#264
YusukeHirao merged 12 commits into
devfrom
feat/dedupe-cap-post-hoc-marking

Conversation

@YusukeHirao

Copy link
Copy Markdown
Member

Summary

  • --dedupe-cap's crawl-time judgment is bounded by actual fetches (the shape's metaSig can only be
    known after fetching a few matching pages), so a same-cluster trap can admit a burst of pages before
    the shape becomes sticky. Add content_items.dedupe_cap_event_id, a self-healing nullable FK to
    dedupe_cap_events, and recompute it against every internal page's URL shape on every
    viewer-build/crawl-completion run — regardless of when a page was crawled relative to the cap
    firing — so already-admitted burst pages get marked after the fact.
  • Expose the mark end-to-end: ListPagesOptions.isDedupeCapped / ListViewerPagesOptions.isDedupeCapped
    / PageDetail.isDedupeCapped+dedupeCapShapeKey (query), viewer_pages.is_dedupe_capped (read model
    schema v24→v25), query pages --isDedupeCapped (CLI), list_pages/get_page_detail (MCP), and a
    Pages-view checklist filter + page-detail display (viewer).
  • Default-enable --dedupe-cap (was opt-in with no default) to 10 on crawl/pipeline, since an
    unbounded trap can make a crawl never finish. False positives on legitimate large sections are
    structurally prevented by the underlying majority-vote tracker (each such page differs in
    title/og tags, so its counter never accumulates). Use --no-dedupe-cap (or --dedupeCap 0) to
    disable — map-flags-to-crawl-config.ts explicitly converts the 0 yargs-parser produces for
    --no-dedupe-cap to null, since the tracker floors any positive threshold at 1 and would
    otherwise cap on the very first observation instead of disabling.
  • Fix scripts/migrate-to-0.13.mjs: the new FK broke pre-0.13 archive migration (entity-table data
    population ran before the referenced adjunct table existed under foreign_keys = ON). Move
    adjunct-table creation immediately after entity-table creation, before any data write.

Test plan

  • yarn lint — 0 errors (pre-existing warnings only)
  • NX_WORKSPACE_ROOT_PATH=<worktree> yarn build — all 13 projects
  • yarn test — 608 files / 4428 tests passing
  • New unit/integration coverage: migrate-content-items-dedupe-cap-event-id,
    backfill-dedupe-cap-event-id, has-dedupe-cap-event-id-column,
    dedupe-cap-shape-key-select-column, list-pages/get-page-detail/apply-viewer-pages-filters
    isDedupeCapped cases, register-pages-route fast/live path, MCP list_pages/get_page_detail,
    CLI flag-parsing (crawl-flag-parsing.spec.ts: default 10, --no-dedupe-cap → 0, explicit
    override), pipeline/crawl default sync, viewer e2e checklist filter
  • New E2E: dedupe-cap.e2e.ts verifies the full crawl → buildViewerReadModellistPages/
    getPageDetail pipeline against a real self-generating trap fixture

…p marking

--dedupe-cap's crawl-time judgment is bounded by actual fetches, so a
same-cluster trap can admit a burst of pages before the shape becomes
sticky. Add a self-healing nullable FK column so those already-admitted
pages can be marked after the fact once the shape is confirmed, instead of
only stopping future enqueues.

Fix scripts/migrate-to-0.13.mjs: the new FK made pre-0.13 archive migration
fail (entity data population ran before the referenced adjunct table
existed under foreign_keys=ON). Move adjunct-table creation earlier in the
migration sequence, and update every isolated entity-table test helper that
enables FK enforcement to provision adjunct tables alongside it.
Recompute content_items.dedupe_cap_event_id against dedupe_cap_events on
every viewer-build/crawl-completion run, matching every internal page's
URL shape regardless of when it was crawled relative to the cap firing.
Skip entirely when dedupe_cap_events is empty — the deferred FK on the
column guarantees no row can be marked in that case, not just a
performance assumption.

Expose the mark through isDedupeCapped (ListPagesOptions,
ListViewerPagesOptions) and PageDetail.isDedupeCapped/dedupeCapShapeKey.
An archive predating this feature (no dedupe_cap_event_id column) degrades
to "nothing is marked" instead of throwing, since --dedupe-cap is opt-in
and a missing column is semantically identical to zero marks.
Add a checklist filter (viewer_pages.is_dedupe_capped, schema v24->v25) to
the Pages view and a shape-key display on page-detail, mirroring the
isSkipped/skipReason presentation. No list column: matches the existing
asymmetry where PageListItem omits fields PageDetail-only exposes.
… query pages

An unbounded --dedupe-cap trap can make a crawl never finish (observed:
6,400+ self-generating /search/ URLs from one double-encoding pagination
bug on a real site). Default the crawl/pipeline --dedupe-cap flag to 10 so
a fresh crawl stops itself against this class of trap without requiring
users to know the flag exists. False positives on legitimate large
sections are structurally prevented by the underlying majority-vote
tracker (each such page differs in title/og tags, so its counter never
accumulates) — see DedupeCapTracker.

Use --no-dedupe-cap (or --dedupeCap 0) to disable. yargs-parser's
boolean-negation coercion turns --no-dedupe-cap into dedupeCap: 0, not
undefined, so map-flags-to-crawl-config.ts converts 0 to null explicitly
before it reaches DedupeCapTracker (which floors any positive threshold at
1 — passing 0 through unchanged would cap on the very first observation,
the opposite of disabling).

pipeline.ts hand-writes its own flag definitions rather than reusing
crawl.ts's (pre-existing, tracked separately) — synced its default there
too so pipeline doesn't silently stay opt-in while crawl becomes on by
default.

Add query pages --isDedupeCapped / MCP list_pages isDedupeCapped, already
wired through in the query/mcp-server commits, to the CLI flag surface.
…hange

Add content_items.dedupe_cap_event_id to the schema index alongside
dedupe_cap_events, generalize the throw-vs-degrade decision rule for
future self-healing FK columns, and record --dedupe-cap's new default-on
CLI behavior (crawl.ts default: 10, pipeline.ts kept in sync, the
migrate-to-0.13.mjs adjunct-table ordering fix) in the dedupe-cap Reading
path section.
…ems.spec.ts

Rebasing onto dev's newly-merged inventory-exclusion feature surfaced the
same gap the earlier crawler commit fixed elsewhere: this test's isolated
DB setup calls createEntityTables without createAdjunctTables, so its
foreign_keys=ON inserts into content_items fail against the new
dedupe_cap_event_id column's unresolvable REFERENCES target.
The viewer's Pages-view checklist filter requires a real table column to
attach its button to (paged-table.tsx renders filter controls strictly
per-column, no column-less filter exists in this framework), but the
isDedupeCapped filter was added list-option-only with no matching
PageListItem field, so the filter button never rendered.

Add isDedupeCappedSelectColumn (degrades to a 0 literal on archives
predating the column, mirroring templateKeySelectColumn) and wire it into
listPages, listPagesByTag, listPagesByJsonLdType, and
joinViewerPageIdsToListItems so every page-list path exposes the flag
consistently.
…nders

paged-table.tsx renders every checklist-filter button strictly attached to
a matching TanStack column (header.column.id) — there is no column-less
filter in this table implementation. The dedupe-cap trap filter had no
corresponding column, so its button never appeared, silently failing the
new e2e test on CI. Add the isDedupeCapped column (boolCell, same pattern
as hasCSP/hasHSTS) now that PageListItem carries the field.

Update ARCHITECTURE.md's post-hoc-marking entries to reflect PageListItem
now exposing isDedupeCapped and to record why list filters need a column.
inventory-snapshot was already the slowest shard (~14.5 min on a 15-minute
job timeout per recent scheduled runs); adding dedupe-cap.e2e.ts (a full
crawl -> viewer-read-model-build -> query pipeline test) there pushed the
job past the timeout three runs in a row on this PR, even though every
individual test passed. Move it to append-pipeline, the lightest shard.
@YusukeHirao
YusukeHirao merged commit 8eaa301 into dev Aug 9, 2026
10 checks passed
@YusukeHirao
YusukeHirao deleted the feat/dedupe-cap-post-hoc-marking branch August 9, 2026 02:55
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