Skip to content

Commit c72a943

Browse files
committed
docs: codify divergence policy + add in-code breadcrumbs
Add a Divergence Policy section to docs/UPSTREAM_SYNC.md with: - Criteria for when to diverge (data loss / malformed wire / hard UX failure; cosmetic issues stay on parity). - Required workflow (file upstream issue, diverge commit prefix, add non-parity row, in-code breadcrumb, regression test, changelog entry). - Review signal (sync PR title convention, budget of max 2 divergences per sync before escalating). Retroactively add `# Divergence from upstream — see docs/UPSTREAM_SYNC.md` breadcrumbs at the four divergence sites introduced in this sync: - src/chat_sdk/thread.py `_adapter_name = adapter.name` in from_json - src/chat_sdk/channel.py same in ChannelImpl.from_json - src/chat_sdk/adapters/google_chat/format_converter.py to_ast() custom-link regex - src/chat_sdk/adapters/google_chat/format_converter.py extract_plain_text() custom-link regex - src/chat_sdk/adapters/google_chat/format_converter.py reserved-char fallback branch in _node_to_gchat The placeholder-clear divergence in thread._fallback_stream already had a full multi-line explanation; leaving that alone. https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
1 parent 139bb63 commit c72a943

4 files changed

Lines changed: 54 additions & 0 deletions

File tree

docs/UPSTREAM_SYNC.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,55 @@ tests. If upstream tests lock in inconsistent behavior, choose one of:
7272
- **Preserve parity** and document the inconsistency in the non-parity section below
7373
- **Intentionally diverge** and document the divergence in the non-parity section
7474

75+
## Divergence Policy
76+
77+
Every divergence from upstream has a cost: merge conflicts on future syncs,
78+
cross-SDK state drift, and a gradual slide from "port" toward "fork". Follow
79+
the rules below before adding one.
80+
81+
### When to diverge
82+
83+
1. **Default: preserve parity.** Matching upstream behavior — even buggy —
84+
reduces merge conflicts and keeps cross-SDK state predictable. If the
85+
behavior is cosmetic or stylistic, preserve parity and move on.
86+
2. **Diverge only when upstream** causes one of:
87+
- **Data loss or corruption** (e.g. dropping fields on round-trip).
88+
- **Malformed wire output** the platform itself mis-renders.
89+
- **Hard UX failure with no workaround** (e.g. stuck loading state
90+
that users can't clear).
91+
3. **Before diverging**, open an issue upstream
92+
([vercel/chat](https://github.com/vercel/chat/issues)) linking the bug.
93+
If upstream accepts and fixes it, delete the divergence on the next sync.
94+
4. **Budget**: a sync PR that accumulates **more than 2 divergences** is a
95+
signal — escalate to a design discussion ("is this still a port?")
96+
before landing quietly.
97+
98+
### How to land a divergence
99+
100+
1. **Commit prefix**: use `diverge(scope): ...`, not `fix:``fix:` implies
101+
parity with upstream's intent.
102+
2. **Add a row to the [Known Non-Parity](#known-non-parity-with-typescript-sdk)
103+
table** with: Python behavior, TS behavior, rationale, and upstream
104+
issue link (if filed).
105+
3. **Drop a one-line breadcrumb at the divergence site**:
106+
```python
107+
# Divergence from upstream — see docs/UPSTREAM_SYNC.md
108+
```
109+
So a future porter doesn't delete the code thinking it's drift.
110+
4. **Add a regression test** that fails if someone "fixes" the divergence
111+
back to upstream's behavior. The test's docstring should cite the reason.
112+
5. **CHANGELOG entry** under a "Python-specific (divergence from upstream)"
113+
subsection.
114+
115+
### Review signal
116+
117+
- **Sync PR titles**: `sync: upstream v<ver>` (not a branch name). Reviewers
118+
scanning the PR list need to see "this is a sync" at a glance.
119+
- **Divergence commits are separate** from the sync commit. Don't bundle a
120+
divergence into `sync: upstream v...`; split it into its own
121+
`diverge(scope): ...` commit with the non-parity table update in the same
122+
commit.
123+
75124
## How to Diff Upstream Changes
76125

77126
```bash

src/chat_sdk/adapters/google_chat/format_converter.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def to_ast(self, gchat_text: str) -> Root:
4242
"""
4343
markdown = gchat_text
4444

45+
# Divergence from upstream — see docs/UPSTREAM_SYNC.md.
4546
# Google Chat custom link syntax <url|text> -> [text](url). Must run
4647
# before bold/strikethrough so the `|` inside a link label isn't
4748
# matched by those patterns. Accepts any RFC 3986 scheme, not just
@@ -66,6 +67,7 @@ def extract_plain_text(self, text: str) -> str:
6667
result = re.sub(r"```[\s\S]*?```", lambda m: m.group(0).strip("`").strip(), text)
6768
# Inline code
6869
result = re.sub(r"`([^`]+)`", r"\1", result)
70+
# Divergence from upstream — see docs/UPSTREAM_SYNC.md.
6971
# Google Chat custom link syntax: <url|text> -> text (any RFC 3986 scheme)
7072
result = re.sub(r"<[a-zA-Z][a-zA-Z0-9+.\-]*:[^|\s>]+\|([^>]+)>", r"\1", result)
7173
# Bold markers (*text*)
@@ -115,6 +117,7 @@ def _node_to_gchat(self, node: Content) -> str:
115117
url = node.get("url", "")
116118
if link_text == url:
117119
return url
120+
# Divergence from upstream — see docs/UPSTREAM_SYNC.md.
118121
# Labels containing the `|` or `>` delimiters, or a newline, can't
119122
# be emitted safely in <url|text> form — Google Chat (and our own
120123
# round-trip regex) stops at the first `>` / `|`. Fall back to

src/chat_sdk/channel.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,7 @@ def from_json(
428428
)
429429
if adapter is not None:
430430
channel._adapter = adapter
431+
# Divergence from upstream — see docs/UPSTREAM_SYNC.md.
431432
# Keep _adapter_name in sync with the explicit adapter so
432433
# to_json() doesn't serialize a stale name.
433434
channel._adapter_name = adapter.name

src/chat_sdk/thread.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,7 @@ def from_json(
779779
)
780780
if adapter is not None:
781781
thread._adapter = adapter
782+
# Divergence from upstream — see docs/UPSTREAM_SYNC.md.
782783
# Keep _adapter_name in sync with the explicit adapter so
783784
# to_json() doesn't serialize a stale name after rebind.
784785
thread._adapter_name = adapter.name

0 commit comments

Comments
 (0)