Skip to content

fix(forge): treat Linear's duplicate state as closed (RIG-3590) - #1069

Open
rigel-mintaka wants to merge 2 commits into
mainfrom
compass-forge/rig-3590-duplicate-closed
Open

fix(forge): treat Linear's duplicate state as closed (RIG-3590)#1069
rigel-mintaka wants to merge 2 commits into
mainfrom
compass-forge/rig-3590-duplicate-closed

Conversation

@rigel-mintaka

@rigel-mintaka rigel-mintaka commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Fixes RIG-3590.

Problem

Linear's WorkflowState.type SDL enum has seven values: triage, backlog,
unstarted, started, completed, canceled, duplicate.
linearClosedStateTypes in go/internal/forge/linear.go listed two of them,
so an issue closed as a duplicate resolved to the forge's open truth.

That slice feeds two consumers, which is what makes it more than a cosmetic
mis-label:

  • mapLinearState — the read mapping, so a fetched duplicate reported open.
  • teamIssueFilter — the GraphQL IssueFilter, as nin for open and in for
    closed, so the server's own open-issue query returned duplicates.

Net effect: an issue a human closed as a duplicate was re-served as live work.
The existing doc comment already enumerated duplicate as a valid SDL type, so
the enum was known — only the closed set was short.

Fix

Add duplicate to linearClosedStateTypes. Both consumers read the one slice,
so the mapping and the query stay consistent by construction.

Verification

Red first, on both surfaces, before the fix:

mapLinearState("duplicate") = "open", want "closed"
open-issue filter must exclude duplicate; nin = [completed canceled]
  • TestMapLinearStateCoversEverySDLType — table over all seven SDL types, not
    just the branched ones, so a new Linear type arriving fails here.
  • TestTeamIssueFilterExcludesDuplicateFromOpen — asserts the query filter and
    the read mapping agree. They share the slice, so a divergence between them is
    the failure the mapping test alone cannot catch.
  • Both mutation-proved: reverting duplicate from the slice fails both.
  • go/internal/forge/testdata/linear/list_issues.json updated — its pinned
    request body carries the nin list, so the behaviour change is visible in the
    golden. Hand-edited because the -update capture path is the livegithub
    suite's (it needs a real Linear call).
  • go build, go vet, go test ./internal/forge/... ./server/... all pass.
  • moon ci (MOON_BASE=origin/main): 33 actions, 0 failed.

No ledger rows touched.

One-time notification on deploy

Surfaced by review. forge_artifact_cursors.snapshot persists the observed
state, so a Linear issue already sitting in duplicate has "open" stored
against it. On the first reconcile sweep after deploy, detectArtifact sees
fetched.State != prev.State and emits one corrective STATE notification per
affected subscribed issue. The artifact did not change — the mapping did.

Self-healing, no migration or backfill: STATE delivery is at-least-once by
design and the delivered value is the correct one. Noted so nobody debugs the
burst on deploy day.

No stored row is made inconsistent: store.Issue.ForgeState is written only
through the board-ingest lane, and buildBoardIngestLane is GitHub-only by
construction, so no Linear issue has ever reached that column.

No GitHub analogue

Checked. GitHub's issue state is already two-valued and passes through
verbatim; its state_reason (which does have a duplicate value) is a reason
orthogonal to state — a GitHub issue closed as a duplicate already reports
closed. state_reason appears nowhere in the Go tree. The asymmetry is
correct: Linear is the only provider projecting a seven-value category enum
onto the forge's two-value domain.

Review

0 high, 2 medium, 5 low. Both mediums were comments of mine asserting
unverified claims, fixed in the second commit: the doc comment said Linear
files duplicate under Done (it does not — duplicate is its own system-managed
terminal category, peer to completed), and the table test's comment promised
enum-drift protection a hardcoded table cannot provide. Also took the free low:
a fallback row pinning that an unrecognised type maps to open, mutation-proved
unique against a mutant that keeps all seven real verdicts and flips only the
unknown case.

Two lows surfaced for your ruling rather than fixed here:

  • The frozen compass-forge-state-transition record quotes the old two-element
    slice as a literal code block, now stale. T3's behaviour is unaffected (its
    default rule targets completed explicitly, never "everything else"), so the
    quote misgrounds a reader without misrouting the implementation. Whether a
    frozen record may be amended for code drift is your convention call.
  • mapLinearState still fails open on an unrecognised type — the same shape
    that produced this bug. Making enum drift fail loud (or default closed) is a
    behaviour change needing a ruling, so it is not in this PR.

@linear-code

linear-code Bot commented Sep 11, 2026

Copy link
Copy Markdown

RIG-3590

@trunk-io

trunk-io Bot commented Sep 11, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-forge-rig-3590-dupli.compass-eng-docs.pages.dev

Deployed from compass-forge/rig-3590-duplicate-closed at 6ba75b2.

…ory model (RIG-3590)

Review found two comments asserting things I had not verified.

The doc comment on linearClosedStateTypes claimed Linear files duplicate
under Done. It does not: duplicate is its own system-managed terminal
category, peer to completed, applied when an issue is marked a duplicate.
The conclusion held but the reason was wrong, and it sat next to the frozen
compass-forge-state-transition record whose default close rule targets
completed only -- a reader believing the false premise had a reason to fold
duplicate into that set, which that record rejects.

The table test's comment promised enum-drift protection a hardcoded table
cannot give. RIG-3590 was a KNOWN type left unhandled, so the promise
installed false confidence exactly where the real failure mode lives.

Also adds the missing fallback row: an unrecognised type maps to open.
Mutation-proved unique -- a mutant that keeps all seven real verdicts and
flips only the unknown case is caught by that row alone.

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@rigel-mintaka
rigel-mintaka marked this pull request as ready for review September 11, 2026 03:16
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