Skip to content

feat(task-board): the import reply says what happened to each item - #7555

Open
viktormarinho wants to merge 1 commit into
mainfrom
t3code/task-import-item-outcomes
Open

viktormarinho wants to merge 1 commit into
mainfrom
t3code/task-import-item-outcomes

Conversation

@viktormarinho

@viktormarinho viktormarinho commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

What is this contribution about?

The reports engine pushes Deco Score findings to POST /api/:org/internal/task-board/import. The reply only had counts, so the engine could not tell which card each finding became. It needs that for two things. The owner widget will link each finding to its task, and the daily recompute will skip findings that already have a card.

The reply now has items, with one entry per request item in request order:

items: Array<{
  index: number;    // position in the request's items[]
  outcome: "created" | "updated" | "semantic_match" | "dismissed" | "quota_blocked";
  id: string;       // task_board_items.id of the card the item created, refreshed, matched, or was blocked by
  key_seq: number;  // that card's key_seq
}>

Each branch of the import loop reports this:

Branch outcome id / key_seq
No match, new card created the new card
externalKey matches an open card updated that card
Normalized title matches an open card updated that card
Same normalized title as an earlier item in the same request updated the card the earlier item created or matched
Semantic match to a reports card, which gets refreshed semantic_match that card
Semantic match to a human card, which is left as is semantic_match that card
Dismissed key, or a keyless item whose title matches a dismissed card dismissed the dismissed card
Created, but the task quota refused the Super Agent delegation quota_blocked the new card, now unassigned

Choices worth reviewing:

  • One semantic_match for both owners. The engine only needs the card id for both of its uses. Whether the card's text was refreshed depends on who owns it, and the engine does nothing different either way. A flag can be added later without breaking callers.
  • outcome is the most specific label, and the counts keep their meaning. Every semantic_match is still counted in updated and every quota_blocked in created, as before. The counts are now derived from items, so the two cannot disagree.
  • id and key_seq are always present. Every current outcome names a card.
  • A replayed run_id has no items. It still replies { created: 0, updated: 0, delegated: 0, deduped: true }. That request wrote nothing and the route does not re-read what the original run did, so the per-item result is unknown.
  • An empty items array is still a 400. The schema requires at least one item, so no reply carries items: [].
  • No new queries. To name the dismissed card, the dismissed key and title sets became maps to { id, key_seq }. They are built from the two queries the transaction already runs, which now also select key_seq. If several dismissed cards share a key or normalized title, the reply names one of them. Any of them blocks the item.

Every existing field keeps its meaning, so the change is additive. The current engine casts the JSON and reads only the counts, so it keeps working. A follow-up PR in the reports repository will store these ids to link findings to tasks.

No shared type or schema describes this reply, and apps/docs does not document the import contract. The module docstring in task-board-import.ts is where the contract lives, and it now describes items.

How did you verify your code works?

  • bun test apps/api/src/api/routes/task-board-import.integration.test.ts apps/api/src/api/routes/task-board-import.test.ts against a local Postgres: 33 pass.
  • New integration tests:
    • A title repeated in one request gives [created X, updated X], and one card holds the later description.
    • A keyless item whose title was dismissed gets dismissed with the dismissed card's id, and the next item keeps index 1.
    • Semantic match: a Bun server in the test speaks the OpenAI chat completions API and stands in for the org's fast model. The real tier resolution, provider adapter, HTTP call and verdict gate run. In one request, one item matches a human card (description unchanged), one matches a reports card (description refreshed), and one is created. Both matched cards get a duplicate_reported activity.
    • Quota blocked: setGlobalSettings turns on quota enforcement with zero free executions. The delegated item reports quota_blocked with its card, and the card ends up unassigned.
    • An empty batch still returns 400, with no items.
  • Existing tests now assert items as well. They cover created, delegated (reported as created), keyed refresh, keyed dismissal and its restore, the exact-title refresh, and a done card's regression (the reply names the new card, not the done one). They also cover a dismissal in another org (that org's reply names its own new card) and the run_id replay (no items).
  • bun run --cwd=apps/api check (tsc) is clean.
  • bun run verify: fmt, lint and knip pass. The unit stage failed on the first run while the machine was at a load average of about 40 on 10 cores. The errors were Cannot find module for modules that exist, plus 5 to 20 second timeouts. None of those files import the import route. Rerunning the 46 affected files passed (510 tests). A second full bun run test gave 9630 pass and 1 fail, NDJSONLogExporter > should flush via timer interval, a 150 ms timer test that passes on its own.
  • Not run: e2e (no spec covers this route) and the reports engine against this branch.

Screenshots/Demonstration

No UI change.

How to Test

  1. Run bun run dev with VAULT_SERVICE_TOKEN set.
  2. Post two items whose titles differ only in case:
    curl -s -X POST "http://localhost:3000/api/<org-id>/internal/task-board/import" \
      -H "Authorization: Bearer $VAULT_SERVICE_TOKEN" \
      -H "content-type: application/json" \
      -d '{"items":[{"title":"Add an H1 to the home page"},{"title":"add an h1 to the home page"}],"source":{"url":"https://shop.example/"}}'
  3. Expect created: 1, updated: 1, and items with { index: 0, outcome: "created" } and { index: 1, outcome: "updated" } carrying the same id and key_seq.
  4. Delete that card on the board and post the same body again. Expect dismissed: 2, and both entries as dismissed with the deleted card's id.
  5. Or run the integration file with DATABASE_URL pointing at a disposable Postgres.

Migration Notes

None.

Review Checklist

  • PR title is clear and descriptive
  • Changes are tested and working
  • Documentation is updated (if needed)
  • No breaking changes

Summary by cubic

Makes the task-board import reply say what happened to each pushed item, so the reports engine can link findings to the cards they became.

  • The reply now includes items: one { index, outcome, id, key_seq } entry per request item in request order, naming the card the item created, refreshed, matched, or was blocked by.
  • outcome is the most specific label — created, updated, semantic_match, dismissed, or quota_blocked — and the counts keep their old meaning, so semantic_match items still count as updated and quota_blocked as created.
  • A replayed run_id still omits items, and empty batches still return 400.
  • Integration tests cover same-title folding, dismissed keyless items, semantic matching against a fake OpenAI-compatible server, and quota-blocked delegations.

Written for commit 041bfb8. Summary will update on new commits.

Review in cubic

The reports engine needs to know which card each pushed finding became,
to link findings to tasks and to skip findings that already have a card.
The import reply only carried counts.

Add `items` to the reply: one `{ index, outcome, id, key_seq }` entry per
request item, in request order. `outcome` is `created`, `updated`,
`semantic_match`, `dismissed` or `quota_blocked`, and `id`/`key_seq` name
the card the item created, refreshed, matched or was blocked by. The
dismissed lookups become maps to the dismissed card, built from the rows
the transaction already reads. The counts are now derived from `items`
and keep their meaning. A replayed run_id still omits `items`.

This branch has not been deployed

No deployments
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