jlevy / jlevy/tbd

Linear sync rejects a valid duplicate close: "duplicate_of is only valid with resolution: duplicate" when resolution is duplicate

Open
#267 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
79
Forks
9
Avg merge
1d 14h
Merged PRs (30d)
42

Description

## Summary

`tbd sync` fails one item on every run with a message that contradicts the record it is rejecting:

```
✗ -aaaa: duplicate_of: duplicate_of is only valid with resolution: duplicate
```

The issue it names **does** have `resolution: duplicate`. The local record is valid, `tbd show` and `tbd list` accept it, and only the Linear integration path rejects it.

Version 0.8.1, Node v24.19.0, Linear integration.

## The record

```yaml
status: closed
resolution: duplicate
duplicate_of: is-01xxxxxxxxxxxxxxxxxxxxxxxx
close_reason: "Superseded by and its tracked audit bead."
```

Both fields are set, and they are consistent with each other.

## Why it looks like a projection bug rather than bad data

The rule in the published bundle (`dist/schemas-*.mjs`) is:

```js
if (issue.duplicate_of != null && issue.resolution !== "duplicate") ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["duplicate_of"],
message: "duplicate_of is only valid with resolution: duplicate"
});
```

It fires only when `duplicate_of` is set **and** `resolution` is anything other than `"duplicate"`. Since the stored record has `resolution: duplicate`, and the same schema accepts the record everywhere else, the object being validated on the sync path cannot be the stored one. The most likely explanation is that the integration builds a partial or projected issue for the push that carries `duplicate_of` but drops `resolution`, so a cross-field invariant is evaluated against a record that is missing one of the two fields it relates.

If that is right, the fix is either to include `resolution` in whatever object the sync path validates, or to skip cross-field checks when validating a partial update, since a partial cannot satisfy an invariant spanning a field it does not carry.

## Reproduction

1. Close an issue as a duplicate so it stores both `resolution: duplicate` and `duplicate_of`.
2. Configure the Linear integration.
3. Run `tbd sync`.

Expected: the item syncs, or fails with a reason that matches its data.
Actual: it fails with the message above on every run. `tbd show` on the same issue shows both fields set.

## Impact

Low severity, persistent. The rest of the sync completes normally, so nothing is lost. The cost is that `tbd sync` and `tbd integration sync` both exit non-zero every time and print a failure, which trains the reader to ignore the sync's error output. In a repository syncing several thousand issues, a permanent known-failing line is exactly where a real failure would hide.

There is also no obvious way to clear it from the user side. Removing `duplicate_of` would silence it but would discard the link to the superseding issue, which is the information the field exists to carry.

## Relation to #244

This looks like a follow-up on the feature #244 asked for. The state model now expresses duplicate with a pointer, which is what that issue argued for and what both providers model. The gap is in carrying that pair across the Linear sync boundary intact.

## Also seen in the same run, if useful

Two other diagnostics appear in `tbd integration sync` output on this repository. I have not investigated either and they may be configuration rather than defects, so I have not filed them; happy to if they are of interest.

- Six issues report `nested 3 levels, past max_nesting 2`, so part of the hierarchy is not represented on the Linear side.
- Five items report `Linear assignee is not present in user_map; assignee synchronization skipped.`

Identifiers, titles and paths above are redacted or genericized, as this came from a private repository.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with the Linear integration paths invoked by `tbd sync` and `tbd integration sync`, then compare their projected issue object with the stored record. Inspect the validation rule in `dist/schemas-*.mjs` and trace where `resolution` may be dropped while `duplicate_of` is retained. Done means a valid duplicate record syncs successfully, with regression coverage for the reproduced case.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend-api-design
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.