Compare commits

...

1 Commits

Author SHA1 Message Date
claude[bot] cacf8310d5 fix: prevent HTML color codes from being parsed as hashtags and truncate observation permalinks
Fixes #446

Two related fixes to prevent indexing failures:

1. HTML color codes no longer interpreted as hashtags
   - Changed from simple '#' in content check to regex pattern
   - Uses negative lookbehind to exclude hex color codes like #4285F4
   - Pattern: (?<![0-9a-fA-F=])#\w+ ensures proper hashtag detection

2. Observation permalinks now truncated to prevent PostgreSQL btree overflow
   - Truncates content to 150 chars before generating permalink
   - Prevents exceeding PostgreSQL's 2704 byte btree index limit
   - Full content still stored, only permalink generation affected

Added comprehensive tests:
- test_observation_html_color_codes() validates HTML/CSS color codes are excluded
- test_observation_permalink_truncation() validates long content handling

Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2025-11-30 04:53:24 +00:00
4 changed files with 97 additions and 2 deletions
+3 -1
View File
@@ -30,7 +30,9 @@ def is_observation(token: Token) -> bool:
# Check for proper observation format: [category] content
match = re.match(r"^\[([^\[\]()]+)\]\s+(.+)", content)
has_tags = "#" in content
# Match proper hashtags (word characters after #), not HTML color codes
# Negative lookbehind ensures # is not preceded by hex digit or equals sign
has_tags = bool(re.search(r"(?<![0-9a-fA-F=])#\w+", content))
return bool(match) or has_tags
+6 -1
View File
@@ -162,9 +162,14 @@ class Observation(Base):
We can construct these because observations are always defined in
and owned by a single entity.
Note: Content is truncated to 150 chars to prevent exceeding PostgreSQL's
btree index limit (2704 bytes) for the permalink column.
"""
# Truncate content to prevent permalink overflow in database indexes
content_for_permalink = self.content[:150] if len(self.content) > 150 else self.content
return generate_permalink(
f"{self.entity.permalink}/observations/{self.category}/{self.content}"
f"{self.entity.permalink}/observations/{self.category}/{content_for_permalink}"
)
def __repr__(self) -> str: # pragma: no cover
+29
View File
@@ -121,6 +121,35 @@ def test_observation_excludes_markdown_and_wiki_links():
assert not is_observation(token), "No space after category should not be valid observation"
def test_observation_html_color_codes():
"""Test that HTML color codes are NOT parsed as hashtags (issue #446).
This validates the fix where content like:
- **<font color="#4285F4">Jane:</font>** Welcome...
should NOT be treated as an observation due to the HTML color code.
"""
# Test HTML color codes should NOT be detected as hashtags
token = Token("inline", '**<font color="#4285F4">Jane:</font>** Welcome to the deep dive', 0)
assert not is_observation(token), "HTML color codes should not be parsed as hashtags"
token = Token("inline", '<font color="#FF0000">Red text</font>', 0)
assert not is_observation(token), "HTML color codes should not be parsed as hashtags"
token = Token("inline", 'Style: background-color=#ABCDEF', 0)
assert not is_observation(token), "CSS color codes should not be parsed as hashtags"
# Test valid hashtags still work
token = Token("inline", "This is a note #hashtag", 0)
assert is_observation(token), "Valid hashtags should still be detected"
token = Token("inline", "[note] Content with #tag1 and #tag2", 0)
assert is_observation(token), "Valid hashtags in observations should still work"
# Test edge case: hashtag next to hex but separated by space
token = Token("inline", "Color is ABCDEF #hashtag", 0)
assert is_observation(token), "Hashtags after hex should work if properly separated"
def test_relation_plugin():
"""Test relation plugin."""
md = MarkdownIt().use(relation_plugin)
@@ -356,3 +356,62 @@ async def test_find_by_category_case_sensitivity(
upper_case = await repo.find_by_category("TECH")
assert len(upper_case) == 0 # Currently case-sensitive
@pytest.mark.asyncio
async def test_observation_permalink_truncation(
session_maker: async_sessionmaker, repo, test_project: Project
):
"""Test that observation permalinks are truncated to prevent database index overflow (issue #446).
PostgreSQL btree indexes have a maximum size of 2704 bytes. Long observation content
was causing permalinks to exceed this limit. The fix truncates content to 150 chars
when generating permalinks.
"""
async with db.scoped_session(session_maker) as session:
entity = Entity(
project_id=test_project.id,
title="test_entity",
entity_type="test",
permalink="test/test-entity",
file_path="test/test_entity.md",
content_type="text/markdown",
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
)
session.add(entity)
await session.flush()
# Create observation with very long content (simulating transcript dialogue)
long_content = "This is a very long observation content that would normally cause the permalink to exceed the PostgreSQL btree index limit of 2704 bytes. " * 50
obs = Observation(
entity_id=entity.id,
content=long_content,
category="transcript",
)
session.add(obs)
await session.commit()
# Refresh to get the relationship
await session.refresh(obs)
await session.refresh(obs, ["entity"])
# Verify the full content is stored
assert len(obs.content) > 150
assert obs.content == long_content
# Verify the permalink is truncated
permalink = obs.permalink
assert permalink is not None
assert len(permalink) < 500 # Should be much shorter than full content
# Verify permalink contains truncated content, not full content
# The permalink format is: {entity.permalink}/observations/{category}/{truncated_content}
assert "test-entity/observations/transcript/" in permalink
# Verify the content portion is limited
# Extract the content portion after the last slash
content_portion = permalink.split("/")[-1]
# The content portion should correspond to truncated content (150 chars max)
# After generate_permalink processing (lowercase, spaces to hyphens, etc.)
assert len(content_portion) <= 200 # Allow some buffer for URL encoding