Skip to content

feat(store): canonicalize tags on the write path (#653 choke-point) - #684

Open
norrietaylor wants to merge 1 commit into
feat/653-tag-controlled-vocabfrom
feat/653-write-path-chokepoint
Open

feat(store): canonicalize tags on the write path (#653 choke-point)#684
norrietaylor wants to merge 1 commit into
feat/653-tag-controlled-vocabfrom
feat/653-write-path-chokepoint

Conversation

@norrietaylor

Copy link
Copy Markdown
Owner

What

Follow-up to #680 (Phase 1). Phase 1 backfilled existing tags once; this is the complementary guarantee that new writes stay canonical — without it, the graph silently re-fragments as the feed poller ingests new items (it currently never applies the alias map).

Stacked on #680. Base is feat/653-tag-controlled-vocab because this needs canonicalize_tags + config.tags.aliases. Retarget to main once #680 merges.

Changes

  • Single write-path choke-point in DuckDBStore: store / store_batch / update run each entry's tags through _canonicalize_entry_tags (alias substitution + optional namespace normalization + order-preserving dedupe). update() re-fetches to build its return, so the returned Entry reflects the canonical tags.
  • Three new constructor params (tag_aliases, tag_reserved_prefixes, tag_normalize_namespaces), wired from config.tags.* at all eight config-bearing construction sites (server ×2, webhooks, cli ×5). Empty alias map + normalization off is a true no-op → an un-configured store writes byte-for-byte as before.

Why just the store (not the merge sites too)

The original design floated also canonicalizing at the poller / classify / gh-sync merge sites. That turns out to be redundant for correctness: every ingest path persists through the store, and the choke-point canonicalizes and dedupes on the canonical form at persist time. So one choke-point gives the full guarantee with far less surface than touching three call sites. (Entry stays validate-only — no config in the model layer.)

Deferred

Namespace-membership enforcement (known_namespaces + off/warn/strict gating of unknown namespaces) is split to a follow-up. This PR is pure canonicalization.

Testing

  • store / store_batch / update canonicalize + dedupe; update return reflects canonical tags; namespace-normalization path; inert-by-default (tags verbatim with no config).
  • Full suite: 3235 passed, 79 skipped; mypy --strict src/ clean; ruff check src/ tests/ clean.

🤖 Generated with Claude Code

Phase 1 backfilled existing tags once; this is the complementary guarantee that
*new* writes stay canonical, so the graph cannot silently re-fragment as the feed
poller ingests new items.

- DuckDBStore gains a single write-path choke-point: store / store_batch / update
  run each entry's tags through _canonicalize_entry_tags (alias substitution +
  optional namespace normalization + order-preserving dedupe). update() re-fetches
  to build its return value, so the returned Entry reflects the canonical tags.
- Configured via three new constructor params (tag_aliases,
  tag_reserved_prefixes, tag_normalize_namespaces), wired from config.tags.* at
  all eight config-bearing construction sites (server, webhooks, cli). Empty
  alias map + normalization disabled is a true no-op, so an un-configured store
  writes byte-for-byte as before.

Because every ingest path (feed poller, gh-sync, classify merge, direct
distillery_store) persists through the store, this one choke-point also dedupes
their assembled tags on the canonical form — no per-call-site change needed.

Namespace-membership enforcement (known_namespaces + off/warn/strict) is split to
a follow-up; this PR is pure canonicalization, inert by default.

Stacked on the Phase 1 branch (needs canonicalize_tags + config.tags.aliases).
Tests: store/store_batch/update canonicalize + dedupe, update return reflects
canonical tags, namespace-normalization path, inert-by-default. Full suite 3235
passed; mypy --strict and ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 118f685d-0907-4fdd-81f5-24c74db141e2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

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