mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
main
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d95c20aab2 |
feat: complete issue status category upgrade (MUL-7365 PR3) (#8466)
* feat: converge issue status categories on upgrade (MUL-7365) Co-authored-by: multica-agent <[email protected]> * refactor(status): remove redundant stored-category normalization (MUL-7365) Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
9d18186e6e |
fix(status): MUL-7365 expand category compatibility before backfill (#8407)
* fix(status): stage category expansion and preserve installed client groups (MUL-7365) Co-authored-by: multica-agent <[email protected]> * fix(status): validate mixed storage with GC and recovery compatibility Co-authored-by: multica-agent <[email protected]> * docs(status): align phase one rollout with merged main dependency Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
7dafc0cd62 |
MUL-7240: Unify issue lifecycle into four stored categories (#8261)
* feat: simplify issue status categories Co-authored-by: multica-agent <[email protected]> * feat: adopt four stored issue lifecycle categories Co-authored-by: multica-agent <[email protected]> * fix: preserve visible work when upgrading saved category preferences Co-authored-by: multica-agent <[email protected]> * docs: align lifecycle comments with stored and wire contracts Co-authored-by: multica-agent <[email protected]> * test: align mobile grouping expectations with four categories Co-authored-by: multica-agent <[email protected]> * fix(issues): group user views by concrete status keys Co-authored-by: multica-agent <[email protected]> * fix(settings): simplify status editor copy and category selector Co-authored-by: multica-agent <[email protected]> * feat(statuses): allow ordering built-ins and unify row actions Co-authored-by: multica-agent <[email protected]> * feat(statuses): configure custom status icons independently of category Co-authored-by: multica-agent <[email protected]> * fix(statuses): dismiss built-in status notice Co-authored-by: multica-agent <[email protected]> * fix(issue-status): require moving issues before archive and polish controls Co-authored-by: multica-agent <[email protected]> * fix(ci): renumber issue status migrations after main updates Co-authored-by: multica-agent <[email protected]> * fix(statuses): isolate archive inspection and align catalog behavior Co-authored-by: multica-agent <[email protected]> * fix(statuses): respect injectable catalog for table grouping Co-authored-by: multica-agent <[email protected]> * refactor(statuses): simplify built-in skill and runtime guidance Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3dcfdaea41 |
feat(issues): simplify board defaults and sorting (#8256)
* feat(issues): simplify board defaults and sorting Co-authored-by: multica-agent <[email protected]> * fix(issues): address board review feedback Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
e3a40b0b8d |
feat(MUL-6243): custom issue status UI — picker, board, filters, settings (#7103)
* refactor(views): widen IssueStatus to open key, resolve category at render sites Board columns, swimlane columns and hidden-column panels are CATEGORIES, not status keys, so adding a custom status never adds a column. Retyped those to IssueStatusCategory and made every presentation lookup (STATUS_CONFIG, i18n labels, status icons) resolve the key to its category first. Status filters stay in concrete keys and map back to columns via the catalog. MUL-6243 Co-authored-by: multica-agent <[email protected]> * feat(views): catalog-driven status picker and per-status labels The picker now offers the workspace catalog's active statuses grouped by category, instead of the 7 hardcoded built-ins, and falls back to the built-ins until the catalog lands so a cold render is unchanged. Built-in names always resolve through i18n, never through the catalog's server-seeded English name — otherwise every non-English workspace would see 'In Progress' where it used to see '进行中'. Custom names come from the catalog. That rule lives in one place: useStatusLabel. Applied it to every surface that shows an issue's OWN status (detail activity feed, table group headers and CSV export, hover card, inbox), which previously showed the category label and so could not tell two statuses in the same category apart. MUL-6243 Co-authored-by: multica-agent <[email protected]> * feat(views): show custom statuses on cards and in filters Board columns and list sections are categories, so two issues in the same column can sit on different statuses with nothing to tell them apart. CustomStatusChip is that signal, and it renders nothing for a built-in — a workspace with no custom statuses looks unchanged. The status filter now lists the workspace catalog instead of the 7 hardcoded built-ins. Without this an issue moved onto a custom status could not be narrowed to at all. Picker and filter share one useStatusOptions hook so the two lists cannot drift apart. MUL-6243 Co-authored-by: multica-agent <[email protected]> * feat(settings): issue status catalog management with drag reorder Owner/admin CRUD for custom statuses, organised by CATEGORY rather than as one flat list. Category is not decoration here — it is the behavior a status inherits, and picking it is the one moment an admin can, since it is immutable afterwards. Built-ins are shown but locked: each is its category's canonical definition. Drag reorder writes intra-category positions starting at 1, because the built-in is seeded at 0 and PATCH on it is a 403, so it always heads its category. The optimistic cache write re-SORTS rather than only writing positions, or the drag would visually undo itself until the refetch. custom_issue_statuses is now published to the frontend, so the tab can hide creation when the deployment has the rollout gate off instead of offering a button that 403s. MUL-6243 Co-authored-by: multica-agent <[email protected]> * fix(issues): close the category contract end to end Elon's review found the board only half-converted: the UI targeted categories while the server still paged by concrete status key, so a card on a custom status was dropped twice — it never came back from the server, and the row filter would have discarded it if it had. 1. New `status_category` group kind (and compound secondary axis). Grouping rewrites the column with a static CASE built from the catalog; paging expands the category to concrete keys and stays on `status = ANY(...)`, so (workspace_id, status) is still usable. A workspace with no custom statuses gets exactly `i.status` back. Row filter, facet totals and swimlane cells all fold by category now. 2. Column visibility split from the status filter. Hiding a column used to write the other six built-in keys into statusFilters, so hiding Backlog silently dropped every CUSTOM status in other categories. Display state is now its own field. The saved-view baseline also stopped deleting custom status filters on reopen. 3. The board/list right-click menu reads the shared catalog options, so every entry point that can set a status offers the same set. 4. Remaining presentation consumers resolve a category instead of guessing: gantt bars were keyed by IssueStatus and went colorless on any custom key; hover card, sidebar, search, tab strip, inbox row, create toast and the activity feed all resolve explicitly now. 5. Reorder is one atomic statement behind a new endpoint, and the client submits ACTIVE rows only. The per-row PATCH loop half-committed when an archived row rejected mid-sequence. Also: one catalog observer per card instead of three, and the stale tasks/todo.md draft is gone. MUL-6243 Co-authored-by: multica-agent <[email protected]> * fix(issues): atomic reorder, category behavior sweep, rollout-safe grouping Second review round. 1. Reorder is now one transaction holding the catalog's SHARED lock. The old shape validated ids, THEN ran the UPDATE — an archive committing in that window left the UPDATE silently skipping the archived row while the earlier rows were rewritten, and still returned 200. The authoritative active set is read under the lock and the payload must match it EXACTLY: a partial order would assign positions from the array index and collide with the rows it left out. Affected rows are checked against the payload before commit. 2. Swept the Web behavior consumers that still compared raw keys, so a custom category actually inherits behavior: gantt hide-completed, batch backlog parking, done/cancelled strikethrough, the mark-done button, sub-issue progress, search cancelled-demotion and mention demotion/dimming. issueBehavesAs is the one predicate they share, and it fails toward showing more and asking more. Two glyph sites (gantt row, swimlane lane) were still painting custom in_review as Todo. 3. Rollout safety. status_category is a contract this PR introduces, and the default-off creation flag does not guard the request path, so a new Web build would have 400'd against any un-upgraded pod — for every workspace. The client now sends it only once the catalog confirms the workspace HAS a custom status, which can only be true if the fleet already serves this version. Everyone else keeps the exact request they made before. That also uncovered a real defect: plain status grouping 500'd on a workspace holding custom statuses, because the descriptor rejected any non-built-in value and failed the whole response. 4. One catalog read per resolve instead of three, and the common path reads none at all. Pinned with observable scan-count tests rather than an assertion that the SQL string matches. 5. A custom-key filter no longer routes on a guess. categoryOf answers todo for anything unloaded, so a saved qa filter paged the todo column while the query still restricted status=qa — empty on every cold load, permanent if the catalog fetch failed. Unresolved custom keys now contribute no column until the catalog is authoritative; facet counts drop unknown keys rather than inflating todo. MUL-6243 Co-authored-by: multica-agent <[email protected]> * fix(issues): swimlane category axis, catalog tri-state, tests with teeth Third review round. 1. Swimlane was only half-converted. The compound spec sent secondary=status_category unconditionally — the same rolling-deploy hole the board/list branches had — and the client row matcher then compared the raw key, so a qa row the server correctly folded into the in_review cell was thrown away again. Both now follow the same hasCustomStatuses switch, and the matcher knows which axis it is on. 2. The cold-load fix did not actually make the surface wait. Returning an empty column set is indistinguishable from "narrow to nothing", so the surface fetched nothing and rendered an empty board with no spinner; on catalog failure that was permanent, with no retry. Resolution is now pending / resolved / error: pending and error suspend fetching AND hold loading, error surfaces isStatusCatalogError. Fetching is suspended rather than left to fall back to all columns, which would briefly show the UNFILTERED board to someone opening a saved qa view. The group contract also joins the branch cursor identity, so a status cursor cannot ride into a status_category request. 3. Two tests were false-green and are replaced: - The reorder race archived BEFORE calling reorder, so the active-set check caught it even with the lock deleted. It now reproduces the real interleaving (reorder parked on the lock, archive commits, then reorder proceeds) and fails without the shared lock — with a 200, which is the actual hole. - The catalog read count read pg_stat_user_tables, whose counters flush asynchronously, and asserted delta <= 1 — vacuous at zero. It now counts through an injected querier and asserts exactly 1 read and 0 category-expansion reads. Also: plain status grouping 500'd on a workspace holding custom statuses (the descriptor rejected non-built-in values and failed the whole response) — that is the path the Table view and every client fallback use. And the chat mention items dropped status_category, so a custom done status in Current/Recent rendered as an active Todo. MUL-6243 Co-authored-by: multica-agent <[email protected]> * fix(issues): give the status catalog failure a real user path Fourth review round. One group, both halves of it. A failed status catalog suspends row fetching, because a custom status filter cannot be routed to a column without it. That left isLoading and isEmpty both false, so IssueSurface fell straight through to the content branch and rendered an empty view with no explanation and no way out — the controller's isStatusCatalogError had no consumer. The catalog now carries its own `retry` (React Query's refetch) rather than just a failure flag, the controller re-exposes it, and IssueSurface renders an explicit error state ahead of loading/empty/content. Component test covers the full loop: catalog rejects, error appears with no row requests issued, clicking retry succeeds, fetching resumes. The blocking condition is also narrowed. A BACKGROUND refetch can fail while the last successful catalog is still cached, and error-before-loaded meant that perfectly serviceable snapshot got a retry screen in front of it. Only a failure with no data behind it blocks now; otherwise the last successful catalog keeps resolving. That rule lives in status-behavior.test.ts, where it can be asserted directly. The component test asserts the wiring — it was originally written to claim the rule too, but reverting the narrowing did not fail it, so the claim moved to the layer that actually holds it. MUL-6243 Co-authored-by: multica-agent <[email protected]> * fix(issues): accept custom status keys in table filters.statuses Filtering a board, list or table by a custom status 400'd the whole request: `filters.statuses` carries the exact keys the user picked, and the table query still validated that list against the 7 built-ins. Every surface errored out instead of narrowing — the one thing the filter work in this PR exists to do. The predicate is a parameterized `status = ANY(...)`, so an unknown key simply matches nothing; the check is now non-empty and length-bounded. Found while writing the post-deploy verification steps, which is where the gap became obvious: "filter by QA" was step one and it 400s. MUL-6243 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
14c2e4e831 |
MUL-6243 feat(issue-status): board fetches by category; archive retires without migration (#7084)
* feat(issue-status): board fetches by category; archive retires without migration (MUL-6243) Product decisions from the issue thread: 1. Board fetches by CATEGORY, not per status. PAGINATED_STATUSES becomes PAGINATED_CATEGORIES and the cache buckets by category, so the board's fan-out stays fixed at 7 parallel requests however many custom statuses a workspace defines — instead of one more request per status added. Adds status_category / status_categories filters to the issue list endpoints, backed by the existing issue_effective_status() SQL function. 3. Archiving retires a status from FUTURE use only. It no longer refuses while issues still carry it: those issues keep their status and keep resolving to their category's behavior (Effective ignores archived_at), while Resolve still rejects the status so nothing new can be assigned to it. The exclusive catalog lock stays — it is what makes "no NEW assignment after archive" exact rather than approximate. Also lands the frontend catalog foundation: packages/core/issue-statuses with useIssueStatuses(wsId) (React Query, keyed by wsId per the state rules) and a pure buildIssueStatusCatalog for non-React callers, plus IssueStatusCategory as a distinct type from the status key and status_category on issue payloads. NOT included, and deliberately: widening IssueStatus to string. That change forces every Record<IssueStatus, …> presentation lookup in packages/views to resolve a key to its category first, and it is the first task of the follow-up PR. Everything here is the groundwork that makes it mechanical. Co-authored-by: multica-agent <[email protected]> * fix(issue-status): restore the status index, make category authoritative, keep archived resolvable Review round 1 on #7084 (MUL-6243): 1. Category filtering wrapped issue.status in issue_effective_status(), which makes the (workspace_id, status) index unusable — and the board switched to it unconditionally, so every workspace paid for it immediately. Categories are now expanded to concrete status keys (issuestatus.ExpandCategories) and filtered with an indexed status = ANY(...). Measured on a 100k-row table with the production index: seq scan cost 27013 -> index-only scan cost 6.07. A workspace with no custom statuses expands each category to exactly its own key, reproducing the pre-feature query. Archived keys are included, since issues left on them still belong in their category's column. 2. status_category was only filled for built-ins, so a custom-status issue arriving over the websocket was silently dropped by addIssueToBuckets and a cross-category update was skipped by the cache coordinator, drifting off-window totals permanently. List, grouped, get, create, update and batch responses now carry an authoritative category via a per-request Resolver (archived included), and the client invalidates instead of no-oping when a category is unresolvable. 3. The catalog was fetched without archived entries, so after archiving a status the issues still on it lost their name and colour and had their category guessed as todo — contradicting the decision that archiving leaves existing issues untouched. Resolution now includes archived entries; activeStatuses / inCategory expose only assignable ones to pickers. Co-authored-by: multica-agent <[email protected]> * fix(issue-status): category on every payload exit, one resolver per request, no same-category duplication Review round 2 on #7084 (MUL-6243): 1. Category fill was incomplete and N+1. The create WEBSOCKET payload was unfilled — only the creator's HTTP response was, which is too late for the other tabs the event exists to serve. GetIssue, the open_only list, search, the duplicate-conflict response, the table rows, the GitHub PR update and the onboarding shim were also unfilled. Grouped and batch called the single-response helper per row, so each custom-status row hit the catalog separately. Every exit now goes through newStatusCategoryFiller, a request-scoped closure over ONE Resolver: built-in statuses still cost no query, and a page of custom-status rows costs one catalog read rather than one per row. 2. A status KEY change within one category duplicated the card and inflated the total. patchIssueInBuckets compared patch.status (a key) against loc.status (a bucket CATEGORY), so in_review -> a custom human_review took the cross-bucket path, then deleted from and re-inserted into the same bucket from a pre-delete snapshot. It now resolves the destination category first and compares category to category; same-category changes replace in place and leave the total alone. The destination category is resolved from the patch alone — the merged issue inherits the previous status_category, which would have kept a genuinely moved card in its old column. patchNeedsInvalidation lets the loaded branch of the cache coordinator invalidate instead of silently no-oping on a status it cannot resolve. Also drops the unrelated share_link.sql.go churn from an sqlc version difference, per the review nit. The four cache cases were verified to fail against the previous logic (card duplicated, total +1) before the fix was restored. Co-authored-by: multica-agent <[email protected]> * fix(issue-status): normalize status_category on every cache write; one filler per create Review round 3 on #7084 (MUL-6243): Must-fix — a cached entity kept the STALE status_category after a status change. The server now sends a category on every issue, so a cached entity is {status: "todo", status_category: "todo"}; an optimistic patch carries only {status: "done"}, and a bare {...issue, ...patch} moved the card to the done bucket while the entity still claimed todo. A single update self-heals from the server response, but the batch API returns only {updated: n} and does not refetch bucketed lists, so the inconsistency was permanent — and the next off-window count then decremented the wrong bucket, recreating total drift. normalizeStatusPatch now rewrites status_category to match status before any cache write, applied at applyIssueChange's entry (covering the flat, table and detail spreads), in patchIssueInBuckets, and in the children mutation. An unresolvable custom key drops the inherited category rather than keeping it; patchNeedsInvalidation still routes it to a refetch. Test fixtures now carry status_category for built-ins, matching what the server actually sends — without that the regression was invisible. Non-blocking items also addressed: - CreateIssue builds one filler and shares it between the broadcast payload and the HTTP response, so a custom-status create reads the catalog once. - Background events via IssueToMap now resolve an authoritative category (IssueToMapWithCategory) for the task, issue and autopilot publishers. The channel router keeps plain IssueToMap and says why: it always creates with the built-in "todo", where key IS category. The "one resolver serves a whole page" assertion was REMOVED rather than kept: pg_stat counters lag, so a delta around one request passes even with a per-row filler. Verified that directly before dropping it. The comment now states what is and is not pinned instead of implying coverage that does not exist. Co-authored-by: multica-agent <[email protected]> * fix(issue-status): address review nits - Normalize the single-update children optimistic patch too, matching the batch path. It self-healed on onSettled, but it broke the entity/bucket invariant for the duration of the request. - Rename TestListEndpointsCarryStatusCategory: the old name claimed it covered "without per row queries", which the test explicitly does not assert. - Drop the unrelated gofmt churn in empty_claim_cache.go. - Add a background-event assertion: IssueToMap leaves a custom status uncategorized, IssueToMapWithCategory resolves it — the reason the publishers use the latter. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
446080bd57 |
MUL-6243 feat(issue-status): per-workspace custom issue statuses over 7 canonical categories (#7065)
* feat(issue-status): per-workspace custom issue statuses over 7 canonical categories (MUL-6243) Co-authored-by: multica-agent <[email protected]> * fix(issue-status): address review — normalize all status consumers, close archive race, gate rollout, split PK index Review round 1 (MUL-6243 / PR #7065): 1. Behavior normalization was incomplete. Adds issue_effective_status() as a SQL mirror of issuestatus.Effective so the six SQL consumers the Go resolver cannot reach (duplicate guard, open-issue list, sub-issue progress, project stats, completed-inbox archive, open_only) resolve categories too. Wires the resolver into the task-failure sweeper and the stage-barrier helpers. 2. Archive TOCTOU. Archive now runs its in-use census and the archive itself in one transaction under an exclusive catalog lock; writes that target a custom status take the shared side. Built-in writes stay lock-free — a built-in can never be archived. 3. Rollout safety. Creating a custom status is gated behind the default-off CustomIssueStatuses flag, so a value old pods cannot interpret cannot exist until the fleet is updated. Readers ship ungated and are safe. 4. Primary key index. Follows the repo convention (327-329): table without a PK, unique index built CONCURRENTLY in its own migration, attached via PRIMARY KEY USING INDEX, and registered for invalid-index cleanup. 5. Status normalization vs stored value. Write paths now persist the canonical key from Resolve, so 'HUMAN_REVIEW' no longer validates and then fails the column format constraint as a 500. Also fixes the two CLI tests failing in CI: they asserted the pre-MUL-6243 contract that an unknown-but-well-formed status is rejected locally. Co-authored-by: multica-agent <[email protected]> * fix(issue-status): close the archive race in both orderings; resolve categories in batch barrier and CLI progress Review round 2 (MUL-6243 / PR #7065): 1. The archive race was only half closed. Handlers resolved the status BEFORE opening a transaction, so an archive could commit in that window and the write would still land on an archived key. assertIssueStatusStillActive now takes the shared catalog lock and RE-RESOLVES inside the caller's transaction, which is what actually makes the status provably active at write time. Applied to create, update, batch, and both description-merge branches, which previously bypassed the guard entirely. A losing writer gets 409 rather than a silent partial update. 2. The batch stage-barrier prefilter still compared literals, so a batch that moved the last child onto a custom done status left childDoneCompleted empty and skipped the parent notification. It now resolves categories first. 3. multica issue children --output json counted only literal done/cancelled. ListChildIssues now emits status_category and the CLI reads it, falling back to status for older backends. TestArchiveCommitsInsideTheWriteRaceWindow interleaves the two operations so the writer validates against an active status and the archive commits before the write. Verified it fails without the re-resolve (issue stranded on the archived status) and passes with it. Co-authored-by: multica-agent <[email protected]> * refactor(issue-status): address review nits — honest race-test naming, one catalog read per list Review round 3 nits (MUL-6243 / PR #7065): 1. TestArchiveRaceBothOrderings claimed more than it proved. Its 'archive wins' cases archived first and only then sent the request, which the pre-flight check alone rejects. Split into TestWritesRejectAnArchivedStatus (the sequential case, named for what it is) and TestArchiveRefusesAfterACommitted Write, and extended TestArchiveCommitsInsideTheWriteRaceWindow — the only test that truly interleaves archive between pre-flight and write — from create alone to all five write endpoints. Re-verified by deleting the in-transaction re-resolve: 4 of 5 fail with issues stranded on the archived status (create is guarded in the service layer and checked separately). 2. ListChildIssues / ListChildrenByParents issued one category lookup per custom-status child. issuestatus.Resolver now amortizes that to a single catalog read per request, lazily on the first custom key, so built-in statuses still cost zero queries however long the list is. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |