Skip to content

fix(sync/notion): render database-row properties into the synced document - #146

Open
YellowSnnowmann wants to merge 4 commits into
tinyhumansai:mainfrom
YellowSnnowmann:fix/notion-property-rendering-5500
Open

fix(sync/notion): render database-row properties into the synced document#146
YellowSnnowmann wants to merge 4 commits into
tinyhumansai:mainfrom
YellowSnnowmann:fix/notion-property-rendering-5500

Conversation

@YellowSnnowmann

@YellowSnnowmann YellowSnnowmann commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

NOTION_GET_PAGE_MARKDOWN returns page block content only, so a Notion
database row's structured property values (select / status /
multi_select / date / people / relation / scalars) never appeared in
the synced document. The agent therefore received a tracker page with no
dropdown text and invented the selections.

Add render_properties() in
src/memory/sync/composio/providers/notion.rs, which walks the
already-fetched row (item.raw — the same object notion_title reads, so no
extra Composio call), renders each non-empty property as a Name: value line
under a Properties: header, and prepends it to the markdown body. The
title property is skipped (already the document title); empty/null values
are skipped; lines are sorted for deterministic output.

Addresses tinyhumansai/openhuman#5500 (the memory-sync read path). The
agent-tool read path depends on the backend markdownFormatted renderer
(outside these repos) and is tracked separately. Shipping to OpenHuman also
needs the tinycortex submodule pointer bumped after merge.

API Or Behavior Changes

Behavior: synced Notion documents for database rows now include a
Properties: block with the row's field values ahead of the page markdown.
Documents for rows with no properties object are unchanged (empty render →
body only). No public API change.

Tests

Ran locally:

  • cargo fmt --all --check — clean.

  • cargo clippy --features sync — no warnings from the changed file
    (notion.rs). One pre-existing clippy::question_mark warning remains in
    providers/normalize/slack_post_process.rs, unrelated to this change, so
    the --all-targets -- -D warnings box below is left unchecked rather than
    claimed falsely.

  • cargo test --features sync --test composio_sync_mock notion — 2 passed
    (notion_renders_database_row_properties_into_document new;
    notion_fetches_markdown_and_counts_both_requests unchanged).

  • Regression proof: with the source change stashed (test kept), the new test
    fails on the pre-fix code — it asserted only the markdown body before.

  • cargo fmt --check

  • cargo clippy --all-targets -- -D warnings — changed file clean; one pre-existing unrelated warning in slack_post_process.rs

  • cargo build --all-targets (sync test target compiled)

  • cargo test — ran the targeted notion sync tests; full suite deferred to CI

Documentation

Item-level docs on render_properties explain the contract and the root
cause. No external docs needed.

Summary by CodeRabbit

  • New Features

    • Notion-synced documents now include database properties in a clearly labeled Properties section.
    • Supports statuses, selections, dates, people, relations, checkboxes, numbers, links, formulas, rollups, and other property types.
    • Property output is consistently ordered with normalized whitespace.
  • Bug Fixes

    • Empty values are omitted and titles are not duplicated.
    • Markdown and page titles are preserved.
    • Pages without markdown use a reliable raw-data fallback.
    • Complex property values are rendered safely without misleading lines.

…ment

NOTION_GET_PAGE_MARKDOWN returns page block content only, so a database
row's structured property values (select / status / multi_select / date /
people / relation / scalars) never appeared in the synced document. The
agent therefore received a tracker page with no dropdown text and invented
the selections (#5500).

Add render_properties(), which walks the already-fetched row (item.raw — the
same object notion_title reads, so no extra Composio call) and emits readable
'Name: value' lines under a 'Properties:' header, prepended to the markdown
body. The title property is skipped (it is the document title); empty/null
values are skipped; lines are sorted for deterministic output.

Integration test drives the real fetch->markdown->document path with a row
carrying status/select/multi_select/date properties and asserts each
selection reaches the document content; it fails on the pre-fix code (which
emitted only the markdown body).
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1d6402b0-ed17-4962-bad2-ad29e7fbf3b4

📥 Commits

Reviewing files that changed from the base of the PR and between 56c88d9 and 35bb7a4.

📒 Files selected for processing (2)
  • src/memory/sync/composio/providers/notion.rs
  • tests/composio_sync_mock.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/composio_sync_mock.rs
  • src/memory/sync/composio/providers/notion.rs

Included review availability: Your plan includes up to 2 reviews per rolling hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

Notion document generation now separates markdown from database properties. It renders readable non-title properties, omits empty values, normalizes whitespace, sorts output, and preserves raw JSON when markdown is unavailable. Integration tests cover standard and fallback property types.

Changes

Notion property sync

Layer / File(s) Summary
Property rendering and document composition
src/memory/sync/composio/providers/notion.rs
The sync renders supported and unknown Notion property types, omits empty and title values, normalizes whitespace, sorts property lines, prepends Properties:, and preserves raw JSON fallback.
Integration validation
tests/composio_sync_mock.rs
Integration tests verify structured properties, deterministic ordering, title deduplication, empty-value omission, markdown preservation, fallback values, newline neutralization, and formula and rollup values.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: 🟡 Moderate · up to 35bb7

Database-row properties are now rendered, but formula and rollup values with date or array shapes can still be silently omitted, leaving some synced fields missing. The PR is not fully merge-ready until this bounded correctness gap is fixed or explicitly accepted by the owner.

Sequence Diagram(s)

sequenceDiagram
  participant NotionAPI
  participant NotionSync
  participant Document
  NotionAPI->>NotionSync: Return markdown and database properties
  NotionSync->>NotionSync: Render, normalize, and sort properties
  NotionSync->>Document: Prepend Properties section to markdown
Loading

Poem

A rabbit sorts each field in line,
While markdown keeps its shape and shine.
Empty values hop away,
Newlines stay within their stay.
Notion notes now read just fine.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: rendering Notion database-row properties in synced documents.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@YellowSnnowmann
YellowSnnowmann marked this pull request as ready for review August 13, 2026 11:15

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

             $0.0209 · 21,496 in / 6,697 out · 18,180 cached (85%) · z-ai/glm-5.2
critique:    $0.0074 · 5,965 in  / 2,540 out · 5,082 cached (85%)  · z-ai/glm-5.2
security:    $0.0037 · 5,923 in  / 1,004 out · 5,048 cached (85%)  · z-ai/glm-5.2
tests:       $0.0058 · 4,490 in  / 2,017 out · 3,764 cached (84%)  · z-ai/glm-5.2
description: $0.0039 · 5,118 in  / 1,136 out · 4,286 cached (84%)  · z-ai/glm-5.2

@tinysweeper

tinysweeper Bot commented Aug 13, 2026

Copy link
Copy Markdown

How this change flows

2 changed behaviours across 6 relationships. 6 surrounding behaviours are shown (60 graph nodes walked). 43 further behaviours left out to keep the diagram readable.

flowchart LR
  n0["notion_title<br/>changed"]:::changed
  n1["...fetches_markdown_and_counts_both_requests<br/>changed"]:::changed
  n2["test_context"]:::impacted
  n3["direct_config"]:::impacted
  n4["test_config"]:::impacted
  n5["iter"]:::impacted
  n6["MemoryConfig"]:::impacted
  n7["render_property_value"]:::impacted
  n0 -->|calls| n5
  n1 -->|calls| n2
  n1 -->|calls| n3
  n1 -->|calls| n4
  n4 -->|uses| n6
  n7 -->|calls| n5
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed behaviour. Grey: surrounding behaviour. Arrows name the call, use, implementation, or test relationship. Orange: has findings. Red: has a finding that blocks the merge.

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Aug 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/composio_sync_mock.rs (1)

568-599: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the complete composed document.

These fragment checks pass if sorting stops, the Properties: header is removed, or properties are appended after the body. Assert the exact expected content to protect deterministic ordering and composition.

Proposed test change
-    // Every structured selection reaches the document text …
-    assert!(
-        content.contains("Status: In progress"),
-        "status missing: {content}"
-    );
-    assert!(
-        content.contains("Priority: High"),
-        "select missing: {content}"
-    );
-    assert!(
-        content.contains("Tags: infra, urgent"),
-        "multi_select missing: {content}"
-    );
-    assert!(
-        content.contains("Due: 2026-06-01"),
-        "date missing: {content}"
-    );
-    // … the markdown body is preserved …
-    assert!(
-        content.contains("# Roadmap\n\nBody"),
-        "body missing: {content}"
-    );
-    // … the title property is not duplicated as a property line …
-    assert!(
-        !content.contains("Name: Roadmap"),
-        "title duplicated: {content}"
-    );
-    // … and an empty property is skipped rather than rendered blank.
-    assert!(
-        !content.contains("Owner:"),
-        "empty select rendered: {content}"
-    );
+    assert_eq!(
+        content.as_str(),
+        "Properties:\n\
+         Due: 2026-06-01\n\
+         Priority: High\n\
+         Status: In progress\n\
+         Tags: infra, urgent\n\n\
+         # Roadmap\n\n\
+         Body"
+    );
🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

In `@tests/composio_sync_mock.rs` around lines 568 - 599, Update the assertions in
the composed-document test to compare content against the complete expected
document string, including deterministic property ordering, any required
Properties header, and the markdown body placement. Retain coverage for omitted
title and empty properties through the exact expected output rather than
separate fragment checks.
🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Nitpick comments:
In `@tests/composio_sync_mock.rs`:
- Around line 568-599: Update the assertions in the composed-document test to
compare content against the complete expected document string, including
deterministic property ordering, any required Properties header, and the
markdown body placement. Retain coverage for omitted title and empty properties
through the exact expected output rather than separate fragment checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 916191fe-91dd-4224-86ea-76e4a64c7927

📥 Commits

Reviewing files that changed from the base of the PR and between 4f6517c and 444187c.

📒 Files selected for processing (2)
  • src/memory/sync/composio/providers/notion.rs
  • tests/composio_sync_mock.rs

@oxoxDev oxoxDev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core of this is right: ordering is deterministic, there's no panic or UTF-8 hazard, no extra Composio call, and the test genuinely fails pre-fix (I reverted notion.rs to origin/main while keeping the new test — it fails with status missing: # Roadmap\n\nBody). Requesting changes for two data-fidelity holes, both of which I reproduced locally, in a change whose whole purpose is to stop the agent inventing property values.

Major

  • notion.rs:282_ => String::new() silently drops every unhandled variant: formula, rollup, created_time/created_by, last_edited_time/last_edited_by, files, unique_id, verification, button. I added "Days left": {type: formula, formula:{type:number,number:3}} and "Ticket": {type: unique_id, unique_id:{prefix:"TASK",number:7}} to the fixture and neither appears in the rendered document. Formula, rollup and unique_id are precisely what a #5500-style tracker page uses, so the hallucination class this PR exists to close survives for exactly those fields — and there is no tracing:: call anywhere in the new code, so nothing signals it. Either add a generic fallback (inner scalar, or a compact JSON dump of property[kind]) or at minimum a tracing::debug!(kind, "unrendered notion property").

  • notion.rs:233 and plain_text() at notion.rs:299 — no newline or whitespace collapsing, so text can forge a property line. A rich_text value of "real\nStatus: FAKE-INJECTED" renders as:

    Notes: real
    Status: FAKE-INJECTED
    

    inside the Properties: block, and because lines are sorted it lands above the genuine Status: In progress. Any Notion text field — a shared database, an imported row, a form response — can therefore spoof another property's value in the agent's context. It also breaks the one-line Name: value contract the block otherwise holds. value.split_whitespace().collect::<Vec<_>>().join(" ") before emitting covers it.

Non-blocking

  • Ingestion is dedup-gated on id@last_edited_time (orchestrator.rs:354-357, state.is_synced), so already-synced rows are never re-rendered — properties will appear only on rows edited after this ships. #5500's acceptance criteria stay false for untouched rows until a state reset or a dedup-namespace bump. Probably a note on the issue rather than a change here.
  • There are no unit tests on render_properties itself; the only coverage is one mock with an author-authored envelope. If real Composio NOTION_FETCH_DATA nests properties anywhere other than properties or data.properties, the function returns "" and the whole feature is inert — again with no log. The repo already has examples/composio_harness and the composio_sync_live target, so one live-key run against a real database row would settle it.
  • tests/composio_sync_mock.rs:568-599 — the fragment contains() assertions pin none of: the Properties: header, the sort, or prepend-before-body. The PR body claims deterministic ordering that no test actually enforces. CodeRabbit's suggested full-document assert_eq! is the right call (agreed); worth adding a case where insertion order differs from sorted order.
  • notion.rs:151-160 — when markdown extraction fails, body falls back to pretty-printed item.raw, which already contains the raw properties, so properties then appear twice: once rendered, once as raw JSON.

Nits

  • notion.rs:242 — relation renders bare UUIDs: embedding noise with no agent value.
  • notion.rs:241 — people renders empty (so the property is dropped) whenever Composio returns user ids without name, which is common when the integration lacks user-read capability.
  • plain_text() duplicates title-run joining that already exists in notion_title (notion.rs:~175) and normalize/notion.rs:86. Three copies will drift.

Things I checked that are fine: determinism is solid — Cargo.toml:117 is serde_json = "1" with no preserve_order, so Map is a BTreeMap, and lines.sort() at notion.rs:294 makes it explicit regardless; no HashMap churn, so no diff or embedding thrash. No unwrap/expect/index/byte-slice in the new code either, so the &s[..n] truncation hazard doesn't apply here.

Merge order: this depends on #144 — without fetch_type every NOTION_FETCH_DATA request is rejected, so render_properties never runs in production and this lands inert and unvalidated against the real API. The tests don't cross-guard that (the fetch mock here has no body matcher, so it passes either way). No textual conflict between the two, though: I merged both branches onto current origin/main and both files auto-merged, with cargo test --features sync --test composio_sync_mock notion passing 2/2. Suggest #144 first, then this once the two majors are addressed.

…injection

The property renderer closed the #5500 hallucination gap for select/status/
multi_select/date/scalars but left two holes a reviewer reproduced:

- The `_ => String::new()` catch-all silently dropped every kind without an
  explicit arm — formula, rollup, unique_id, created/last_edited time and
  user, files — which are exactly the fields a tracker page leans on, so the
  agent still invented those values, with no signal that anything was skipped.
- Property text was emitted without collapsing whitespace, so a rich_text
  value like "real\nStatus: FAKE" forged a second `Name: value` line that,
  once the block is sorted, outranked the genuine `Status` — any Notion text
  field could spoof another property in the agent's context.

Add `render_unknown` to render the common unhandled kinds (timestamps,
`unique_id` as `PREFIX-n`, `formula`/`rollup` inner value, files, and any bare
scalar via `scalar_value`); a kind it still can't read degrades to a
`tracing::debug` + skip rather than vanishing. Collapse whitespace in every
property name and value at the single emit point, which neutralises the
injection for all kinds at once and preserves the one-line contract.

Also stop the raw-JSON fallback (used when page markdown is absent) from
double-rendering properties: it already contains the `properties` object, so
the rendered block is no longer prepended on that path.

Tests: the existing case now asserts the exact composed document (header +
deterministic sort + body placement), and a new case proves formula /
unique_id / timestamp fallbacks render and that an injected newline is
collapsed instead of forging a property line.
@YellowSnnowmann

Copy link
Copy Markdown
Contributor Author

Thanks — both Majors reproduced and both are fixed in 7cf3239, plus the double-render non-blocker and CodeRabbit's exact-document assertion.

Major 1 — dropped variants. The _ => String::new() catch-all is replaced with render_unknown(kind, property), which renders the shapes a tracker page actually uses: created_time/last_edited_time (the timestamp), created_by/last_edited_by (the user name), unique_id as PREFIX-n, formula/rollup (their inner typed value), and files (names). Anything else falls through scalar_value (string/number/bool/{name}), and a shape still unreadable degrades to tracing::debug!(kind, \"[memory_sync:notion] unrendered property\") + skip — so a new Notion property type is visible in telemetry, never silent. Your exact fixture is now covered: formula:{type:number,number:3}Days left: 3, unique_id:{prefix:\"TASK\",number:7}Ticket: TASK-7.

Major 2 — text injection. Both the property name and value are now passed through collapse_ws (split_whitespace().join(\" \")) at the single emit point, so a rich_text value like \"real\\nStatus: FAKE-INJECTED\" collapses to one line and can no longer forge a second, higher-sorting Status: property line. Doing it at the emit point covers every kind at once (rich_text, select names, relation, fallback), not just rich_text. New test asserts the forged line never appears (!content.contains(\"\\nStatus: FAKE-INJECTED\")).

Non-blocking:

  • Double-render on the markdown-fallback path: fixed. When NOTION_GET_PAGE_MARKDOWN yields nothing we fall back to the raw row JSON, which already contains properties, so the rendered block is no longer prepended on that path (it's only prepended when real page markdown exists).
  • render_properties coverage: added the exact-document assert_eq! CodeRabbit suggested (agreed — pins the Properties: header, the sort, and prepend-before-body) plus the new fallback/injection case. Full live-key validation against a real database row is still worth one run, but the shapes are now unit-covered.
  • Dedup-gating (id@last_edited_time): correct that already-synced rows won't re-render until edited — that's the sync dedup namespace, not something to change in this renderer. I'll note it on #5500 so the acceptance criterion accounts for a state reset / namespace bump for untouched rows.
  • relation-bare-UUIDs and people-without-name: acknowledged as low-value/limited (the latter is a Composio user-read capability gap); left as-is rather than widening scope here.

Merge order: agreed — this depends on #144 (fetch_type), so land #144 first, then this. No textual conflict; both auto-merge onto origin/main.

Gates: cargo fmt --all --check clean; cargo clippy --all-targets -- -D warnings clean on the default gate and no findings in notion.rs under --features sync (the one remaining clippy::question_mark is the pre-existing slack_post_process.rs warning, unrelated); cargo test --all-features and --no-default-features --features sync both green (the 3 notion sync tests included).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/memory/sync/composio/providers/notion.rs (1)

291-297: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the property rendering contract.

Add item documentation for render_properties. Specify title omission, empty-value omission, normalization, sorting, and raw-JSON fallback behavior. This behavior is non-obvious and affects generated document content.

As per coding guidelines, “Document public APIs, module contracts, and non-obvious behavior thoroughly.”

🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

In `@src/memory/sync/composio/providers/notion.rs` around lines 291 - 297,
Document the public render_properties function with item-level documentation
covering title omission, empty-value omission, whitespace normalization,
property sorting, and raw-JSON fallback behavior. Keep the documentation focused
on the generated document contract and the existing behavior implemented by
render_properties and its helpers.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Inline comments:
In `@src/memory/sync/composio/providers/notion.rs`:
- Around line 371-375: Update the formula and rollup handling in scalar_value so
structured date results and rollup arrays are rendered instead of defaulting to
empty output; preserve existing scalar behavior, and add fixtures covering
formula dates, rollup dates, and rollup arrays.

---

Nitpick comments:
In `@src/memory/sync/composio/providers/notion.rs`:
- Around line 291-297: Document the public render_properties function with
item-level documentation covering title omission, empty-value omission,
whitespace normalization, property sorting, and raw-JSON fallback behavior. Keep
the documentation focused on the generated document contract and the existing
behavior implemented by render_properties and its helpers.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c0a05d3a-d560-490f-8be2-d09bc6393396

📥 Commits

Reviewing files that changed from the base of the PR and between 444187c and 7cf3239.

📒 Files selected for processing (2)
  • src/memory/sync/composio/providers/notion.rs
  • tests/composio_sync_mock.rs

Included review availability: Your plan includes up to 2 reviews per rolling hour; 0 remain after this review.

Comment thread src/memory/sync/composio/providers/notion.rs Outdated

@oxoxDev oxoxDev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 7cf32395. Major 2 is fully and correctly fixed. Major 1 is most of the way there, but it still drops the two commonest formula/rollup result shapes — the exact fields I named as the #5500 hallucination surface — and the new arm is strictly worse than the generic fallback on a flattened envelope. Narrow ask, roughly six lines.

Major 2 (forgery) — fixed. collapse_ws is applied at the single emit point (notion.rs:296) to both name and value, after the match, so every arm including the new render_unknown path is covered by construction. I grepped every property-line construction site: format!("{}: {value}", collapse_ws(name)) is the only one, no bypass. Verified on four vectors — value newline collapses to Notes: real Status: FAKE-INJECTED; injection through the property name gives NAME Status: FORGED-VIA-NAME: x on one line; U+2028 is caught too (Rust is_whitespace covers U+2028/2029); and injection routed through an unknown variant via render_unknown/scalar_value still collapses, since it's post-match. No content truncation — "line one\n\nline two with\tspaces"line one line two with spaces, all tokens preserved. Doing it at the emit point rather than per-kind was the right call.

Major 1 (dropped variants) — partially fixed. My fixture does render now; I confirmed by reverting notion.rs to 444187c7 while keeping the new test, which fails with exactly my two original findings, and Days left: 3 / Ticket: TASK-7 appear at head.

But a 22-property probe row still drops these entirely — no line at all: FormulaDate (formula.type=date), RollupDate, RollupArray (show_original), RollupNested, RollupUnsupported, Verify, Btn, and CreatedBy when the user has no name.

The cause is that render_unknown unwraps formula/rollup to {type: T, T: value} and hands the inner to scalar_value, which only knows String/Number/Bool/{name}. A date inner is {start, end}""; an array inner is a Vec"". The file already has correct date rendering in the explicit "date" arm about 60 lines up — it just isn't reused. Rollup show_original and date-arithmetic formulas are staple tracker fields, so the hallucination class survives precisely there.

The sharper half: the formula/rollup specialization has no fallback to the generic path. Second probe:

"FlatFormula":   {"type":"formula","formula":3}
"FlatRollup":    {"type":"rollup","rollup":12}
"UnknownScalar": {"type":"newkind","newkind":"hello"}
→ Properties:
  UnknownScalar: hello

A completely unknown kind carrying a bare scalar renders; formula carrying the identical scalar does not, because .and_then(Value::as_object) fails and .unwrap_or_default() swallows it. Specializing a kind made it strictly worse than not specializing it. Since there's no live-Composio fixture proving the envelope is always wrapped, that's a real inertness risk rather than a hypothetical.

The ask, and it's mechanical: in render_unknown's formula/rollup arm, (a) route date results through the existing date renderer, (b) comma-join array results, (c) .unwrap_or_else(|| scalar_value(inner)) when the wrapper shape isn't recognised. Plus fixtures for formula-date, rollup-date, rollup-array.

On CodeRabbit's new review: its Major at notion.rs:371-375 is valid and blocking — I reproduced it independently before reading it, and its web-check of the Notion API is accurate (rollup result types are number/date/array/unsupported/incomplete). It's the same defect as the residue above. Its "document the property rendering contract" nitpick I'd skip: a 12-line doc comment already exists at notion.rs:213-224 covering title omission, empty-value omission, sort rationale and the empty return, whitespace normalization is documented in the very inline comment it's pointing at, and the raw-JSON fallback lives in document(), not render_properties. At most, fold the two comments together.

Verified clean: empty lines can't leak — (!value.is_empty()).then(...) runs after collapse_ws, so dropped properties produce no line rather than a bare Name: . Determinism intact (lines.sort() at 306, serde_json without preserve_order). No unwrap/expect/panic/byte-slice added. No extra Composio call. Double-render is fixedbody is now Option and the None arm returns pretty raw JSON without prepending the rendered block. Union-merged against current main (post-#153): clean, 22/22 tests, fmt clean, clippy --features sync --all-targets --no-deps clean.

Tests: both fail against origin/main, and the new one fails against 444187c7 — genuine coverage. CodeRabbit's exact-document assert_eq! is present essentially verbatim and pins the header, sorted order, prepend-before-body, title non-duplication and the empty-Owner skip. One caveat worth knowing: serde_json::Map is a BTreeMap, so keys arrive already sorted and the test can't actually distinguish lines.sort() working from map ordering — insertion order never differs from sorted order in this build. The sort is still correct belt-and-braces if preserve_order is ever unified in.

Smaller items

  • No tracing at all when the properties object is absent entirely — that's the "feature is inert against real Composio" case, and it matters more than the per-kind log that was added.
  • The new tracing::debug! carries kind but not the property name, and fires routinely for button/verification where empty is correct, which dilutes the signal. Name-in-log plus skipping known-valueless kinds would sharpen it; warn-once is worth considering for "we dropped user data".
  • created_by/last_edited_by and people entries lacking name drop silently (probe: one user vanished from a two-person field, CreatedBy produced no line). Pre-existing.
  • Relation still renders bare UUIDs. Unaddressed, still fine to defer.
  • The plain_text() triplication note is now partly moot — #153 merged and deleted providers/normalize/ wholesale, so it's down to two copies inside notion.rs (notion_title vs plain_text).
  • Still no unit tests on render_properties itself, only envelopes written by hand — and the flattened-formula finding above raises the stakes on that.

Merge order: #144 is still open, and without its fetch_type the fetch is rejected upstream, so this is dead code in production until that lands — the acceptance criteria can't be observed otherwise. No conflict with #153 (this doesn't touch providers/normalize/); I confirmed the union merge is clean and green, so no rebase is needed. The current BLOCKED state is the review decision, not CI — all checks pass.

render_unknown handled scalar formula/rollup results but a `date` or `array`
result fell through scalar_value to empty output — dropping exactly the
rolled-up dates and related-title lists a tracker page carries, the #5500
hallucination class this PR closes.

Add render_typed_value for the Notion `{ "type": T, T: <inner> }` wrapper used
by formula, rollup, and each rollup-array element: it renders date and nested
array results and dispatches the common leaf kinds, falling through to
scalar_value otherwise. Extract the date formatting into a shared format_date
used by both the `date` property arm and these results. Also expand the
render_properties doc to state the title/empty omission, whitespace
normalization, best-effort unknown handling, sort determinism, and raw-JSON
fallback contract (per the repo's documentation guideline).

Test: formula date, rollup date, and rollup array all render into the exact
composed document.
@YellowSnnowmann

Copy link
Copy Markdown
Contributor Author

@coderabbitai Both addressed in 56c88d9.

Render structured formula and rollup values — valid, fixed. A formula/rollup result whose inner type is date or array fell through scalar_value to empty output, dropping rolled-up due dates and related-title lists (exactly the tracker fields #5500 is about). Added render_typed_value for the Notion { "type": T, T: <inner> } wrapper (used by formula, rollup, and each rollup-array element): it renders date (via a new shared format_date) and nested array results, dispatches the common leaf kinds, and falls through to scalar_value otherwise. New test asserts formula-date, rollup-date, and rollup-array all render into the exact composed document (Due: 2026-08-01 → 2026-08-03, Items: A, 2, Next: 2026-09-01, Score: 42).

Document the render_properties contract — done. Expanded the item doc to state title/empty omission, whitespace normalization, best-effort unknown handling, sort determinism, and the raw-JSON fallback behavior.

Gates green: fmt, default clippy -D warnings, doc gate, --all-features (1606) and --features sync (1487); the 4 notion sync tests included.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --stat --oneline 56c88d9
git show --format=fuller --find-renames --find-copies -- \
  src/memory/sync/composio/providers/notion.rs \
  tests/composio_sync_mock.rs 56c88d9

ast-grep outline src/memory/sync/composio/providers/notion.rs \
  --items all --type function --match 'render_properties|render_typed_value|format_date|render_unknown'

rg -n -C 4 \
  'render_typed_value|format_date|render_properties|formula-date|rollup-date|rollup-array|Due:|Items:|Next:|Score:' \
  src/memory/sync/composio/providers/notion.rs tests/composio_sync_mock.rs

Length of output: 20617


@YellowSnnowmann Both findings are verified in 56c88d9.

render_typed_value now renders typed formula and rollup date and recursive array values. The integration test asserts the complete rendered document for the reported date, array, and scalar cases.

The render_properties documentation now defines the rendering contract, including omission rules, whitespace normalization, best-effort handling, sorting, and the raw-JSON fallback behavior.


✏️ Learnings added
Learnt from: YellowSnnowmann
URL: https://github.com/tinyhumansai/tinycortex/pull/146

Timestamp: 2026-08-18T12:42:36.752Z
Learning: In `src/memory/sync/composio/providers/notion.rs`, Notion `formula` and `rollup` property payloads use typed wrappers of the form `{ "type": T, T: value }`. The `render_typed_value` helper must render typed `date` values, recursive typed `array` values, common leaf kinds, and scalar fallbacks so tracker fields such as rolled-up due dates and related-title lists are retained.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@YellowSnnowmann

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

The prior actionable comment is addressed at the current head (commit 56c88d9): render_typed_value now renders formula/rollup date and array results and falls through to the generic scalar path for flat/unknown shapes, with fixtures for each. Please re-review the latest head.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

@YellowSnnowmann I will perform a full review of the latest head, including commit 56c88d9.

⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 11 minutes.

@YellowSnnowmann

Copy link
Copy Markdown
Contributor Author

@oxoxDev Thanks — the residual Major 1 is fixed at head (56c88d9); you reviewed 7cf3239, before that push.

Formula/rollup structured results + the strictly-worse-than-generic regression → fixed. Added render_typed_value for the Notion {type: T, T: <inner>} wrapper used by formula, rollup, and each rollup-array element: it (a) routes date through the existing date renderer (extracted into a shared format_date), (b) comma-joins array results, and (c) falls through to scalar_value when the wrapper shape isn't the {type, T} envelope — so your FlatFormula {"formula":3} probe now renders 3 instead of vanishing, closing the specialization-made-it-worse gap. Fixtures added for formula-date, rollup-date, and rollup-array (Due: 2026-08-01 → 2026-08-03, Items: A, 2, Next: 2026-09-01, Score: 42).

Doc nitpick — I did expand the render_properties doc (title/empty omission, whitespace normalization, best-effort unknown handling, sort determinism, raw-JSON fallback). Happy to trim if you'd rather keep it lean per your "at most fold the two comments" note.

Smaller items — acknowledged, taking the reasonable ones and deferring the pre-existing:

  • No tracing when the properties object is absent entirely (the "inert against real Composio" case) — fair that this matters more than the per-kind log; I can add a one-line debug there. Say the word and I'll fold it in.
  • tracing::debug! carrying kind but not the property name, and firing for button/verification where empty is correct — will add the name and skip known-valueless kinds if you want it sharpened.
  • created_by/people without name, relation bare UUIDs, plain_text duplication (now 2 copies post-Drop the host-side Composio normalisers (tinymemory#18 §B3) #153) — pre-existing, deferring rather than widening this PR.
  • Unit tests directly on render_properties (vs hand-written envelopes) — reasonable; the flattened-formula case raises the stakes, agreed.

Merge order — confirmed: #144 first (its fetch_type gates the fetch), then this. Union-merge against current main (post-#153) is clean, no providers/normalize/ touch. All checks pass; the BLOCKED state is the review decision, not CI.

@YellowSnnowmann

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@YellowSnnowmann

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 57 minutes.

@oxoxDev oxoxDev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything I asked for in round 2 is genuinely delivered, and I verified all of it empirically at 56c88d98 — formula/rollup date routes through the shared format_date, array comma-joins, and the flattened {"formula":3} envelope now falls back to scalar_value. One Major left, and it's roughly three lines from an approve.

Verified with a 53-property probe row through the real pipeline + wiremock:

Probe Round 2 Now
formula.type=date (start+end) absent 2026-08-01 → 2026-08-03
formula.type=date (start only) absent 2026-08-01
rollup.type=date absent 2026-09-01
rollup.type=array (show_original, 10 mixed) absent A, 2, rt, Sel, m1, m2, 2026-01-01, Ann, Yes, https://x.dev
nested array-of-array absent deep1, 9
{"type":"formula","formula":3} absent 3
{"type":"rollup","rollup":12} absent 12
flat "flatstr" / true / {name:…} absent flatstr / Yes / NamedNoType
rollup.type=unsupported/incomplete/empty array absent absent — correct, no value exists
rollup.type=number = 0 0 renders, no falsy-drop

Major — render_typed_value (notion.rs:389) has a narrower dispatch than its own siblings.

It hand-rolls a subset of the dispatch that render_properties and render_unknown already implement, and its _ arm is scalar_value(wrapper.get(kind)), which returns "" for any object/array without a name. So this is the same "specialization is strictly worse than the generic path" defect from round 2, moved one level down. Isolated probe, each as a single show_original rollup-array element:

Element kind Renders?
formula whole line dropped
rollup dropped
relation dropped
files dropped
unique_id dropped
created_time, last_edited_time, created_by, email, url, phone_number, status, unknown-scalar render

Rollup-of-a-formula and rollup-of-a-relation are mainstream tracker configurations, and the failure mode is exactly #5500's — property absent, model invents it. It does get logged via render_unknown's empty-check, so it isn't invisible in telemetry, but it's still a data drop.

The fix I'd suggest is structural rather than another arm: extract render_properties' match body into a shared render_property_value(kind, property) and have render_typed_value delegate to it for unhandled kinds, keeping only the array recursion local. That collapses three partially-overlapping dispatch tables into one and makes this class of bug impossible by construction — the same posture as the single-emit-point collapse_ws fix, which is exactly why Major 2 has stayed fixed through two rounds of new code paths.

Major 2 (injection) — still fixed, no bypass in the new path. Single construction site confirmed at notion.rs:298-299, collapse_ws on both name and value after the match; the only other format! sites are {prefix}-{n} and {start} → {end}, both upstream of the collapse. All 13 injection probes render on one line: value newline, property-name newline, CRLF, and U+2028 (Rust's split_whitespace uses Unicode White_Space, so that's covered), plus injection specifically routed through the new code — render_typed_value string, date-start, typed-select, a rollup-array element, a nested array element, and the flat-formula fallback. No bypass.

Regression test is valid. Reverting notion.rs to round-2 7cf32395 with the new test in place fails for the right reason:

left:  "Properties:\nScore: 42\n\n# Metrics\n\nBody"
right: "Properties:\nDue: 2026-08-01 → 2026-08-03\nItems: A, 2\nNext: 2026-09-01\nScore: 42\n\n# Metrics\n\nBody"

Full suite green — 23 passed; 0 failed on cargo test --features sync --test composio_sync_mock.

Smaller items

  • Still no tracing when the properties object is absent entirely (notion.rs:240 early-returns silently) — the feature-inert-against-real-Composio case I flagged as mattering more than the per-kind log. You offered to add it; yes please, and it's fine to fold into the same round as the Major above.
  • No fixture for the flattened {"formula":3} / {"rollup":12} envelope — the sharper regression from round 2 that you explicitly closed. It works (probe: 3, 12), but nothing pins it, so it can silently come back.
  • verification ({state, verified_by, date}) still renders empty and arguably has a renderable state; button empty is correct. Pre-existing, fine to leave deferred.
  • The expanded doc contract at :213-238 is good — keep it.

Merge path is clear now: #144 merged at 13:26, so the dead-code-in-production concern is gone and this is live-effective on merge. #153 merged at 11:50 and removed providers/normalize/, but this touches only providers/notion.rs and the mock test — git merge-tree origin/main exits 0, zero conflicts.

CI is fully green, and CodeRabbit cleared its CHANGES_REQUESTED and approved at this head, with both of its round-2 points genuinely addressed. The remaining BLOCKED state is my stale round-2 review, which this one supersedes.

…ver drop

render_typed_value hand-rolled a narrower subset of the dispatch that
render_properties and render_unknown already implemented, and its fallback was
a bare scalar — so a rollup `array` element that is itself a formula, rollup,
relation, files, or unique_id rendered empty and was dropped. Rollup-of-formula
and rollup-of-relation are mainstream tracker configs, so this was #5500's
hallucination class surviving one level down (the property absent, the model
inventing it).

Collapse the three partially-overlapping dispatch tables into one canonical
render_property_value(kind, property) covering every kind, and have both
render_properties and render_typed_value delegate to it — render_typed_value
keeps only the `array` recursion and the bare-scalar (flattened-envelope)
fallback local. An array element of any kind now renders exactly as that kind
would at property level, making the drop impossible by construction (the same
posture as the single-emit-point collapse_ws fix). render_unknown is removed;
its concrete shapes live in the unified table, which logs only a genuinely
unreadable kind rather than every empty value.

Also: log once when a row has no `properties` object at all (the
inert-against-real-Composio case), not silently.

Tests: a rollup array of {formula, relation} now renders `3, rel-1` (both were
dropped), and the flattened `{"formula":3}` / `{"rollup":12}` envelope is
pinned. All 5 notion mock tests + sync matrix green; notion.rs 461 lines.
@YellowSnnowmann

Copy link
Copy Markdown
Contributor Author

@oxoxDev Fixed the Major structurally as you suggested, in 35bb7a4.

Major — unified the dispatch. Collapsed the three partially-overlapping tables (render_properties' match, render_unknown, render_typed_value) into one canonical render_property_value(kind, property) covering every kind. Both render_properties and render_typed_value now delegate to it; render_typed_value keeps only the array recursion and the bare-scalar (flattened-envelope) fallback local. So a rollup-array element of any kind renders exactly as that kind would at property level — rollup-of-formula and rollup-of-relation no longer drop. render_unknown is gone; its concrete shapes live in the one table, which now logs only a genuinely unreadable kind (via the _ arm's empty-scalar check) rather than firing for every empty value, which also sharpens the telemetry point you raised. Same "impossible by construction" posture as the single-emit-point collapse_ws.

Probe parity: a rollup array of {formula:number:3, relation:[rel-1]} now renders 3, rel-1 (both were dropped before).

Smaller items folded in:

  • tracing when properties is absent entirely — added; render_properties now logs row has no properties object; rendered nothing at the early return instead of returning silently.
  • Flattened {\"formula\":3} / {\"rollup\":12} fixture — added; the new test pins both (FlatF: 3, FlatR: 12) alongside the rollup-array-element case, so the round-2 regression can't silently return.
  • verification/button — left deferred per your note (they hit the _ arm and log; button-empty is correct, verification.state is a pre-existing enhancement).

Verified: fmt / default clippy -D warnings / doc gate clean; notion.rs clippy clean under --features sync; --all-features and --features sync matrices green; the 5 notion mock tests pass; notion.rs is 461 lines. Merge path clear — #144 merged at 13:26, #153 doesn't collide (touches only notion.rs + the mock).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants