@starlove/orbit 1.4.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +10 -8
- package/README.md +146 -164
- package/dist/full/csp.txt +1 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-subset.woff2 +0 -0
- package/dist/full/fonts/JetBrains_Mono/README.txt +5 -0
- package/dist/full/fonts/Mona_Sans/MonaSans-subset.woff2 +0 -0
- package/{public/fonts/Open_Sans → dist/full/fonts/Mona_Sans}/OFL.txt +93 -93
- package/dist/full/fonts/Mona_Sans/README.txt +10 -0
- package/{public/fonts/JetBrains_Mono → dist/full/fonts/Newsreader}/OFL.txt +93 -93
- package/dist/full/fonts/Newsreader/README.txt +8 -0
- package/dist/full/fonts/Newsreader/newsreader.woff2 +0 -0
- package/dist/full/index.html +36 -653
- package/dist/full/js/app.BmuponPJ.js +32 -0
- package/dist/full/js/app.BmuponPJ.js.map +1 -0
- package/dist/full/js/chunk.1fJGQGbW.js +2 -0
- package/dist/full/js/chunk.1fJGQGbW.js.map +1 -0
- package/dist/full/js/chunk.5D7JO2zp.js +2 -0
- package/dist/full/js/chunk.5D7JO2zp.js.map +1 -0
- package/dist/full/js/chunk.BrNn_D79.js +2 -0
- package/dist/full/js/chunk.BrNn_D79.js.map +1 -0
- package/dist/full/js/chunk.CokJXIqM.js +2 -0
- package/dist/full/js/chunk.CokJXIqM.js.map +1 -0
- package/dist/full/js/chunk.D_Haqy6R.js +2 -0
- package/dist/full/js/chunk.D_Haqy6R.js.map +1 -0
- package/dist/full/js/chunk.RO0jsH6I.js +15 -0
- package/dist/full/js/chunk.RO0jsH6I.js.map +1 -0
- package/dist/full/js/chunk.dqWOEtBM.js +2 -0
- package/dist/full/js/chunk.dqWOEtBM.js.map +1 -0
- package/dist/full/js/warm.f260bbbc.js +15 -0
- package/dist/full/orbits-favicon.png +0 -0
- package/dist/full/orbits-logo-bg-wide.png +0 -0
- package/dist/full/orbits-logo-bg.png +0 -0
- package/dist/full/orbits-logo.png +0 -0
- package/dist/full/styles.2b154ffe.css +1 -0
- package/dist/full/vendor/starlove-ui/theme-init.js +66 -0
- package/dist/full/version.json +2 -2
- package/docs/AGENT_GUIDE.md +114 -0
- package/docs/UPDATING.md +249 -64
- package/lib/adapters/indexeddb/blobs.d.ts +3 -0
- package/lib/adapters/indexeddb/blobs.js +54 -0
- package/lib/adapters/indexeddb/database.d.ts +60 -0
- package/lib/adapters/indexeddb/database.js +126 -0
- package/lib/adapters/indexeddb/index.d.ts +5 -0
- package/lib/adapters/indexeddb/index.js +3 -0
- package/lib/adapters/indexeddb/locks.d.ts +14 -0
- package/lib/adapters/indexeddb/locks.js +40 -0
- package/lib/adapters/indexeddb/store.d.ts +40 -0
- package/lib/adapters/indexeddb/store.js +424 -0
- package/lib/adapters/indexeddb/tables.d.ts +43 -0
- package/lib/adapters/indexeddb/tables.js +127 -0
- package/lib/adapters/memory/blobs.d.ts +3 -0
- package/lib/adapters/memory/blobs.js +44 -0
- package/lib/adapters/memory/board.d.ts +41 -0
- package/lib/adapters/memory/board.js +521 -0
- package/lib/adapters/memory/index.d.ts +2 -0
- package/lib/adapters/memory/index.js +2 -0
- package/lib/adapters/memory/search.d.ts +6 -0
- package/lib/adapters/memory/search.js +30 -0
- package/lib/adapters/memory/store.d.ts +34 -0
- package/lib/adapters/memory/store.js +266 -0
- package/lib/adapters/sqlite/backups.d.ts +53 -0
- package/lib/adapters/sqlite/backups.js +232 -0
- package/lib/adapters/sqlite/blobs.d.ts +14 -0
- package/lib/adapters/sqlite/blobs.js +109 -0
- package/lib/adapters/sqlite/board.d.ts +77 -0
- package/lib/adapters/sqlite/board.js +328 -0
- package/lib/adapters/sqlite/database.d.ts +67 -0
- package/lib/adapters/sqlite/database.js +285 -0
- package/lib/adapters/sqlite/index.d.ts +3 -0
- package/lib/adapters/sqlite/index.js +2 -0
- package/lib/adapters/sqlite/lock.d.ts +11 -0
- package/lib/adapters/sqlite/lock.js +34 -0
- package/lib/adapters/sqlite/records.d.ts +54 -0
- package/lib/adapters/sqlite/records.js +546 -0
- package/lib/adapters/sqlite/schema.d.ts +54 -0
- package/lib/adapters/sqlite/schema.js +276 -0
- package/lib/adapters/sqlite/store.d.ts +39 -0
- package/lib/adapters/sqlite/store.js +385 -0
- package/lib/browser/in-page.d.ts +31 -0
- package/lib/browser/in-page.js +46 -0
- package/lib/browser/old-preview.d.ts +13 -0
- package/lib/browser/old-preview.js +80 -0
- package/lib/browser/preview-board.d.ts +8 -0
- package/lib/browser/preview-board.js +69 -0
- package/lib/cli/main.d.ts +7 -0
- package/lib/cli/main.js +343 -0
- package/lib/cli/orbit.d.ts +8 -0
- package/lib/cli/orbit.js +25 -0
- package/lib/client/http.d.ts +38 -0
- package/lib/client/http.js +334 -0
- package/lib/client/local.d.ts +26 -0
- package/lib/client/local.js +190 -0
- package/lib/client/object-url.d.ts +8 -0
- package/lib/client/object-url.js +21 -0
- package/lib/client/types.d.ts +268 -0
- package/lib/client/types.js +8 -0
- package/lib/commands/agent-tools.d.ts +31 -0
- package/lib/commands/agent-tools.js +100 -0
- package/lib/commands/index.d.ts +56 -0
- package/lib/commands/index.js +298 -0
- package/lib/commands/parse.d.ts +73 -0
- package/lib/commands/parse.js +231 -0
- package/lib/commands/present.d.ts +134 -0
- package/lib/commands/present.js +198 -0
- package/lib/commands/session.d.ts +85 -0
- package/lib/commands/session.js +138 -0
- package/lib/commands/verbs.d.ts +35 -0
- package/lib/commands/verbs.js +842 -0
- package/lib/core/actors.d.ts +18 -0
- package/lib/core/actors.js +45 -0
- package/lib/core/agent.d.ts +128 -0
- package/lib/core/agent.js +323 -0
- package/lib/core/boards.d.ts +113 -0
- package/lib/core/boards.js +288 -0
- package/lib/core/bootstrap.d.ts +47 -0
- package/lib/core/bootstrap.js +100 -0
- package/lib/core/bytes.d.ts +10 -0
- package/lib/core/bytes.js +52 -0
- package/lib/core/card-paths.d.ts +25 -0
- package/lib/core/card-paths.js +44 -0
- package/lib/core/context.d.ts +60 -0
- package/lib/core/context.js +42 -0
- package/lib/core/cross-board.d.ts +81 -0
- package/lib/core/cross-board.js +228 -0
- package/lib/core/describe.d.ts +21 -0
- package/lib/core/describe.js +81 -0
- package/lib/core/errors.d.ts +159 -0
- package/lib/core/errors.js +160 -0
- package/lib/core/files.d.ts +79 -0
- package/lib/core/files.js +262 -0
- package/lib/core/history.d.ts +20 -0
- package/lib/core/history.js +53 -0
- package/lib/core/image-types.d.ts +16 -0
- package/lib/core/image-types.js +36 -0
- package/lib/core/model.d.ts +263 -0
- package/lib/core/model.js +34 -0
- package/lib/core/nebula-link.d.ts +14 -0
- package/lib/core/nebula-link.js +39 -0
- package/lib/core/relations.d.ts +58 -0
- package/lib/core/relations.js +112 -0
- package/lib/core/search.d.ts +38 -0
- package/lib/core/search.js +92 -0
- package/lib/core/sessions.d.ts +44 -0
- package/lib/core/sessions.js +158 -0
- package/lib/core/snapshot-schema.d.ts +295 -0
- package/lib/core/snapshot-schema.js +82 -0
- package/lib/core/snapshots.d.ts +75 -0
- package/lib/core/snapshots.js +409 -0
- package/lib/core/states.d.ts +25 -0
- package/lib/core/states.js +126 -0
- package/lib/core/text.d.ts +40 -0
- package/lib/core/text.js +113 -0
- package/lib/core/tickets.d.ts +69 -0
- package/lib/core/tickets.js +311 -0
- package/lib/core/trello.d.ts +17 -0
- package/lib/core/trello.js +269 -0
- package/lib/core/verdicts.d.ts +28 -0
- package/lib/core/verdicts.js +92 -0
- package/lib/core/views.d.ts +95 -0
- package/lib/core/views.js +186 -0
- package/lib/http/app.d.ts +46 -0
- package/lib/http/app.js +491 -0
- package/lib/mcp-server.d.ts +10 -0
- package/lib/mcp-server.js +120 -0
- package/lib/node/assets.d.ts +7 -0
- package/lib/node/assets.js +68 -0
- package/lib/node/data-folder.d.ts +21 -0
- package/lib/node/data-folder.js +34 -0
- package/lib/node/env.d.ts +38 -0
- package/lib/node/env.js +49 -0
- package/lib/node/host-check.d.ts +24 -0
- package/lib/node/host-check.js +53 -0
- package/lib/node/host-services.d.ts +9 -0
- package/lib/node/host-services.js +150 -0
- package/lib/node/local.d.ts +22 -0
- package/lib/node/local.js +61 -0
- package/lib/node/migrate.d.ts +53 -0
- package/lib/node/migrate.js +635 -0
- package/lib/node/package.d.ts +7 -0
- package/lib/node/package.js +14 -0
- package/lib/node/repo-board.d.ts +49 -0
- package/lib/node/repo-board.js +245 -0
- package/lib/node/serve.d.ts +8 -0
- package/lib/node/serve.js +106 -0
- package/lib/orbit.d.ts +47 -0
- package/lib/orbit.js +44 -0
- package/lib/quiet-sqlite-warning.d.ts +1 -0
- package/lib/quiet-sqlite-warning.js +18 -0
- package/lib/store/blob-path.d.ts +6 -0
- package/lib/store/blob-path.js +12 -0
- package/lib/store/conformance.d.ts +52 -0
- package/lib/store/conformance.js +1449 -0
- package/lib/store/search-words.d.ts +10 -0
- package/lib/store/search-words.js +19 -0
- package/lib/store/types.d.ts +452 -0
- package/lib/store/types.js +34 -0
- package/llms.txt +63 -0
- package/package.json +109 -25
- package/src/cli/orbit.js +11 -658
- package/src/mcp-server.js +16 -876
- package/SKILL-ORBIT.md +0 -151
- package/build.mjs +0 -210
- package/dist/full/js/app.4e6fbe20.js +0 -952
- package/dist/full/js/app.4e6fbe20.js.map +0 -7
- package/dist/full/styles.4e60d824.css +0 -1
- package/docs/AGENT_PROTOCOL.md +0 -424
- package/docs/MCP_SETUP.md +0 -54
- package/docs/ORBIT_DISPATCH.md +0 -280
- package/docs/SS-Orbit-Dark.png +0 -0
- package/docs/SS-Orbit-Light.png +0 -0
- package/docs/mcp-client.example.json +0 -13
- package/docs/setup/orbit-init-wizard.md +0 -174
- package/public/apple-touch-icon.png +0 -0
- package/public/cursors/live_arrow.cur +0 -0
- package/public/cursors/live_link.cur +0 -0
- package/public/cursors/live_move.cur +0 -0
- package/public/cursors/live_select.cur +0 -0
- package/public/cursors/live_unavail.cur +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/OFL.txt +0 -93
- package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/OFL.txt +0 -93
- package/public/fonts/Inter/README.txt +0 -118
- package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/README.txt +0 -79
- package/public/fonts/OpenRing-Regular.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/README.txt +0 -100
- package/public/fonts/Orbitron/OFL.txt +0 -93
- package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/public/fonts/Orbitron/README.txt +0 -68
- package/public/fonts/Rajdhani/OFL.txt +0 -93
- package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/public/fonts/runescape-uf.ttf +0 -0
- package/public/index.html +0 -769
- package/public/js/actor-labels.js +0 -65
- package/public/js/api.js +0 -39
- package/public/js/app.js +0 -45
- package/public/js/artifact-drawer.js +0 -79
- package/public/js/board-export.js +0 -161
- package/public/js/board-menu.js +0 -374
- package/public/js/card-actions.js +0 -545
- package/public/js/config.js +0 -20
- package/public/js/create-card.js +0 -190
- package/public/js/dom.js +0 -19
- package/public/js/drawer.js +0 -273
- package/public/js/font-preference.js +0 -230
- package/public/js/format.js +0 -433
- package/public/js/help-menu.js +0 -31
- package/public/js/icons.js +0 -16
- package/public/js/kanban.js +0 -694
- package/public/js/local-backend.js +0 -1015
- package/public/js/main.js +0 -161
- package/public/js/modal.js +0 -33
- package/public/js/motion-preference.js +0 -64
- package/public/js/panel.js +0 -58
- package/public/js/router.js +0 -134
- package/public/js/search.js +0 -56
- package/public/js/settings-tabs.js +0 -7
- package/public/js/settings.js +0 -1634
- package/public/js/sse.js +0 -159
- package/public/js/starscape.js +0 -332
- package/public/js/state.js +0 -129
- package/public/js/text-editing.js +0 -128
- package/public/js/theme-preference.js +0 -95
- package/public/js/ticket-detail.js +0 -1516
- package/public/js/ticket-typeahead.js +0 -49
- package/public/js/toast.js +0 -89
- package/public/js/topbar-notes.js +0 -155
- package/public/js/transport.js +0 -22
- package/public/js/typeahead.js +0 -206
- package/public/js/unread.js +0 -121
- package/public/js/update-check.js +0 -58
- package/public/js/url-routes.js +0 -94
- package/public/orbits-favicon.png +0 -0
- package/public/orbits-logo-bg-wide.png +0 -0
- package/public/orbits-logo-bg.png +0 -0
- package/public/orbits-logo.png +0 -0
- package/public/styles.css +0 -7709
- package/src/core/agent.js +0 -387
- package/src/core/agents-md.js +0 -109
- package/src/core/artifacts.js +0 -149
- package/src/core/attachments.js +0 -238
- package/src/core/auth.js +0 -85
- package/src/core/backups.js +0 -142
- package/src/core/board.js +0 -148
- package/src/core/boards.js +0 -409
- package/src/core/bootstrap.js +0 -170
- package/src/core/css-bundle.js +0 -30
- package/src/core/db.js +0 -315
- package/src/core/delete-board.js +0 -163
- package/src/core/dispatch.js +0 -1007
- package/src/core/events.js +0 -314
- package/src/core/http.js +0 -115
- package/src/core/migrate.js +0 -198
- package/src/core/paths.js +0 -55
- package/src/core/provision-repo-board.js +0 -119
- package/src/core/queries.js +0 -246
- package/src/core/registry.js +0 -211
- package/src/core/relations.js +0 -163
- package/src/core/review-verdicts.js +0 -138
- package/src/core/router.js +0 -556
- package/src/core/search.js +0 -183
- package/src/core/seed.js +0 -234
- package/src/core/snapshots.js +0 -692
- package/src/core/states.js +0 -167
- package/src/core/system-picker.js +0 -109
- package/src/core/tickets.js +0 -400
- package/src/core/util.js +0 -106
- package/src/core/version-check.js +0 -154
- package/src/mcp/orbit-client.js +0 -287
- package/src/server.js +0 -115
package/lib/core/text.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The small rules every use case shares for what people and agents send:
|
|
3
|
+
* names on one line, slugs, priorities and types read the way the API has
|
|
4
|
+
* always read them, each refusal with its error code.
|
|
5
|
+
*/
|
|
6
|
+
import { OrbitError } from "./errors.js";
|
|
7
|
+
/**
|
|
8
|
+
* What a field holds as text: a string as it is, a number or boolean written
|
|
9
|
+
* out, anything else (an object, null, nothing) as "". Input is never
|
|
10
|
+
* stringified as "[object Object]".
|
|
11
|
+
*/
|
|
12
|
+
export function asText(value) {
|
|
13
|
+
if (typeof value === "string")
|
|
14
|
+
return value;
|
|
15
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint")
|
|
16
|
+
return String(value);
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
/** A board slug: lowercase letters, digits and single dashes, at most 80 characters. */
|
|
20
|
+
export function slugify(value) {
|
|
21
|
+
return String(value)
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.trim()
|
|
24
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
25
|
+
.replace(/^-+|-+$/g, "")
|
|
26
|
+
.slice(0, 80);
|
|
27
|
+
}
|
|
28
|
+
/** True for a slug slugify leaves as it is. */
|
|
29
|
+
export function isSlug(value) {
|
|
30
|
+
return typeof value === "string" && value !== "" && slugify(value) === value;
|
|
31
|
+
}
|
|
32
|
+
/** A name as one clean line: line breaks and control characters become spaces. */
|
|
33
|
+
export function cleanName(value) {
|
|
34
|
+
return asText(value ?? "")
|
|
35
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g, " ")
|
|
36
|
+
.replace(/\s+/g, " ")
|
|
37
|
+
.trim();
|
|
38
|
+
}
|
|
39
|
+
/** A string with something in it, trimmed; otherwise `missing_<field>`. */
|
|
40
|
+
export function requiredString(value, field) {
|
|
41
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
42
|
+
throw new OrbitError(`missing_${field}`);
|
|
43
|
+
return value.trim();
|
|
44
|
+
}
|
|
45
|
+
/** A lane's name: required, and one clean line. */
|
|
46
|
+
export function laneNameOf(value) {
|
|
47
|
+
const name = cleanName(requiredString(value, "name"));
|
|
48
|
+
if (!name)
|
|
49
|
+
throw new OrbitError("missing_name");
|
|
50
|
+
return name;
|
|
51
|
+
}
|
|
52
|
+
/** 0 to 4, rounded; anything unreadable is med (2). */
|
|
53
|
+
export function normalizePriority(value) {
|
|
54
|
+
const number = Number(value ?? 2);
|
|
55
|
+
if (!Number.isFinite(number))
|
|
56
|
+
return 2;
|
|
57
|
+
return Math.max(0, Math.min(4, Math.round(number)));
|
|
58
|
+
}
|
|
59
|
+
const TICKET_TYPES = ["epic", "feature", "task", "bug"];
|
|
60
|
+
export function normalizeTicketType(value) {
|
|
61
|
+
const type = asText(value || "task").toLowerCase().trim();
|
|
62
|
+
if (!TICKET_TYPES.includes(type))
|
|
63
|
+
throw new OrbitError("invalid_ticket_type");
|
|
64
|
+
return type;
|
|
65
|
+
}
|
|
66
|
+
export function normalizeEntryType(value) {
|
|
67
|
+
const type = asText(value || "").toLowerCase().trim();
|
|
68
|
+
if (type !== "decision" && type !== "lesson")
|
|
69
|
+
throw new OrbitError("invalid_project_entry_type");
|
|
70
|
+
return type;
|
|
71
|
+
}
|
|
72
|
+
const ROLES = ["ai_ready", "in_progress", "review", "done"];
|
|
73
|
+
/** A lane's role in the agent flow, or null for none. */
|
|
74
|
+
export function normalizeStateRole(value) {
|
|
75
|
+
if (value === null || value === undefined || value === "")
|
|
76
|
+
return null;
|
|
77
|
+
const role = asText(value).toLowerCase().trim();
|
|
78
|
+
if (!ROLES.includes(role))
|
|
79
|
+
throw new OrbitError("invalid_state_role");
|
|
80
|
+
return role;
|
|
81
|
+
}
|
|
82
|
+
/** `existing`, then a blank line, then `note`; just `existing` when the note is empty. */
|
|
83
|
+
export function appendNote(existing, note) {
|
|
84
|
+
const text = asText(note ?? "").trim();
|
|
85
|
+
if (!text)
|
|
86
|
+
return existing;
|
|
87
|
+
return [existing.trim(), text].filter(Boolean).join("\n\n");
|
|
88
|
+
}
|
|
89
|
+
/** At most `max` characters; `max` 0 means no cap. */
|
|
90
|
+
export function cap(value, max) {
|
|
91
|
+
const text = asText(value ?? "");
|
|
92
|
+
return max && text.length > max ? text.slice(0, max) : text;
|
|
93
|
+
}
|
|
94
|
+
/** A whole number above zero, else `fallback`. */
|
|
95
|
+
export function positiveInt(value, fallback) {
|
|
96
|
+
const n = Number(value);
|
|
97
|
+
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
98
|
+
}
|
|
99
|
+
/** A whole number from zero, else `fallback`. */
|
|
100
|
+
export function nonNegativeInt(value, fallback) {
|
|
101
|
+
const n = Number(value);
|
|
102
|
+
return Number.isInteger(n) && n >= 0 ? n : fallback;
|
|
103
|
+
}
|
|
104
|
+
/** An optional string field, trimmed; "" when absent. */
|
|
105
|
+
export function optionalString(value) {
|
|
106
|
+
if (value === undefined || value === null)
|
|
107
|
+
return "";
|
|
108
|
+
return asText(value).trim();
|
|
109
|
+
}
|
|
110
|
+
/** A field that is a string when given, else "" (the API's `body.x || ""`). */
|
|
111
|
+
export function textOr(value, fallback = "") {
|
|
112
|
+
return typeof value === "string" && value !== "" ? value : value === undefined || value === null || value === "" ? fallback : asText(value);
|
|
113
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cards and their comments: making, changing, archiving, restoring and
|
|
3
|
+
* deleting a card, and the discussion on it. Every change records an event,
|
|
4
|
+
* keeps the card's search document current and moves its epic's
|
|
5
|
+
* `updated_at`, so the epic sorts near its sub-cards' work.
|
|
6
|
+
*/
|
|
7
|
+
import type { BoardCtx } from "./context.ts";
|
|
8
|
+
import type { Comment, Id, OrbitEvent, Ticket, TicketType } from "./model.ts";
|
|
9
|
+
import type { TicketView } from "./views.ts";
|
|
10
|
+
import type { BoardReader } from "../store/types.ts";
|
|
11
|
+
/** The card's epic, checked: on this board, not itself or its own descendant, and the card isn't an epic. */
|
|
12
|
+
export declare function checkParent(r: BoardReader, value: unknown, type: TicketType, ticketId?: string): Promise<Id | null>;
|
|
13
|
+
export interface NewTicketInput {
|
|
14
|
+
board_id?: unknown;
|
|
15
|
+
title?: unknown;
|
|
16
|
+
description?: unknown;
|
|
17
|
+
type?: unknown;
|
|
18
|
+
parent_ticket_id?: unknown;
|
|
19
|
+
ai_plan?: unknown;
|
|
20
|
+
plan_artifact_path?: unknown;
|
|
21
|
+
implementation_summary?: unknown;
|
|
22
|
+
implementation_artifact_path?: unknown;
|
|
23
|
+
implementation_updates?: unknown;
|
|
24
|
+
nebula_url?: unknown;
|
|
25
|
+
state_id?: unknown;
|
|
26
|
+
priority?: unknown;
|
|
27
|
+
labels?: unknown;
|
|
28
|
+
}
|
|
29
|
+
/** Makes a card, in its lane or the board's default lane. */
|
|
30
|
+
export declare function createTicket(c: BoardCtx, input: NewTicketInput): Promise<TicketView>;
|
|
31
|
+
declare const EDITABLE: readonly ["title", "description", "type", "parent_ticket_id", "ai_plan", "plan_artifact_path", "implementation_summary", "implementation_artifact_path", "implementation_updates", "nebula_url", "state_id", "priority"];
|
|
32
|
+
export type TicketUpdate = Partial<Record<(typeof EDITABLE)[number] | "labels", unknown>>;
|
|
33
|
+
/** Reads a card on this board, or `ticket_not_found`. */
|
|
34
|
+
export declare function ticketOn(r: BoardReader, ticketId: Id): Promise<Ticket>;
|
|
35
|
+
/**
|
|
36
|
+
* Changes a card's fields; `labels` (names) replaces its labels. Moving a card
|
|
37
|
+
* into a done lane deletes the relations that made it a blocker: the relation
|
|
38
|
+
* is the record of what still blocks.
|
|
39
|
+
*/
|
|
40
|
+
export declare function updateTicket(c: BoardCtx, ticketId: Id, input: TicketUpdate): Promise<TicketView>;
|
|
41
|
+
/** Puts a card in the archive: off the board, out of search and of agents' work, its data kept. */
|
|
42
|
+
export declare function archiveTicket(c: BoardCtx, ticketId: Id): Promise<TicketView>;
|
|
43
|
+
export declare function restoreTicket(c: BoardCtx, ticketId: Id): Promise<TicketView>;
|
|
44
|
+
/** Deletes an archived card for good, with its comments, links and images; its events stay. */
|
|
45
|
+
export declare function deleteTicket(c: BoardCtx, ticketId: Id): Promise<{
|
|
46
|
+
ok: true;
|
|
47
|
+
deleted_id: Id;
|
|
48
|
+
}>;
|
|
49
|
+
/** Adds a comment to a card: a person's, or an agent's note. */
|
|
50
|
+
export declare function createComment(c: BoardCtx, ticketId: Id, input: {
|
|
51
|
+
body?: unknown;
|
|
52
|
+
author?: unknown;
|
|
53
|
+
kind?: unknown;
|
|
54
|
+
}): Promise<Comment>;
|
|
55
|
+
/** Hides a comment from agents, or shows it again: it leaves (or rejoins) everything an agent can read or search. */
|
|
56
|
+
export declare function setCommentHidden(c: BoardCtx, ticketId: Id, commentId: Id, input: {
|
|
57
|
+
omitted_for_ai?: unknown;
|
|
58
|
+
omittedForAi?: unknown;
|
|
59
|
+
}): Promise<Comment>;
|
|
60
|
+
/** One event of a card's history, its data parsed as `body`. */
|
|
61
|
+
export type HistoryEntry = OrbitEvent & {
|
|
62
|
+
body: unknown;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* A card's moves between lanes, oldest first: moves, takes that moved it (into
|
|
66
|
+
* In Progress), asks and finishes, each event's data parsed as `body`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function ticketHistory(c: BoardCtx, ticketId: Id): Promise<HistoryEntry[]>;
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cards and their comments: making, changing, archiving, restoring and
|
|
3
|
+
* deleting a card, and the discussion on it. Every change records an event,
|
|
4
|
+
* keeps the card's search document current and moves its epic's
|
|
5
|
+
* `updated_at`, so the epic sorts near its sub-cards' work.
|
|
6
|
+
*/
|
|
7
|
+
import { cardLinkFromInput } from "./card-paths.js";
|
|
8
|
+
import { OrbitError } from "./errors.js";
|
|
9
|
+
import { record, reindex, searchDocument, touchTicket } from "./history.js";
|
|
10
|
+
import { normalizeNebulaUrl } from "./nebula-link.js";
|
|
11
|
+
import { normalizePriority, normalizeTicketType, requiredString, textOr, asText } from "./text.js";
|
|
12
|
+
import { Lanes, ticketView } from "./views.js";
|
|
13
|
+
const LABEL_COLOR = "#64748b";
|
|
14
|
+
/** The label with this name, made when the board has none. */
|
|
15
|
+
async function ensureLabel(w, c, labels, name) {
|
|
16
|
+
const existing = labels.find((label) => label.name === name);
|
|
17
|
+
if (existing)
|
|
18
|
+
return existing;
|
|
19
|
+
const label = { id: c.ids.next(), board_id: c.board.id, name, color: LABEL_COLOR, created_at: c.clock.now() };
|
|
20
|
+
await w.labels.insert(label);
|
|
21
|
+
labels.push(label);
|
|
22
|
+
return label;
|
|
23
|
+
}
|
|
24
|
+
/** The card's epic, checked: on this board, not itself or its own descendant, and the card isn't an epic. */
|
|
25
|
+
export async function checkParent(r, value, type, ticketId = "") {
|
|
26
|
+
if (value === undefined || value === null || value === "")
|
|
27
|
+
return null;
|
|
28
|
+
if (type === "epic")
|
|
29
|
+
throw new OrbitError("epic_cannot_have_parent");
|
|
30
|
+
if (value === ticketId)
|
|
31
|
+
throw new OrbitError("ticket_cannot_parent_itself");
|
|
32
|
+
const parent = await r.tickets.get(asText(value));
|
|
33
|
+
if (!parent)
|
|
34
|
+
throw new OrbitError("invalid_parent_ticket");
|
|
35
|
+
// The whole chain up, not one hop: A under B under C under A is a cycle too.
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
let ancestor = parent;
|
|
38
|
+
let guard = 0;
|
|
39
|
+
while (ancestor) {
|
|
40
|
+
if (ticketId && ancestor.id === ticketId)
|
|
41
|
+
throw new OrbitError("ticket_parent_cycle");
|
|
42
|
+
const next = ancestor.parent_ticket_id;
|
|
43
|
+
if (!next || seen.has(next) || guard++ > 1000)
|
|
44
|
+
break;
|
|
45
|
+
seen.add(next);
|
|
46
|
+
ancestor = await r.tickets.get(next);
|
|
47
|
+
}
|
|
48
|
+
return parent.id;
|
|
49
|
+
}
|
|
50
|
+
/** Makes a card, in its lane or the board's default lane. */
|
|
51
|
+
export async function createTicket(c, input) {
|
|
52
|
+
if (input.board_id && input.board_id !== c.board.id)
|
|
53
|
+
throw new OrbitError("board_id_mismatch");
|
|
54
|
+
const title = requiredString(input.title, "title");
|
|
55
|
+
const view = await c.data.write(async (w) => {
|
|
56
|
+
const lanes = await Lanes.of(w);
|
|
57
|
+
const stateId = input.state_id ? asText(input.state_id) : (lanes.list.find((s) => s.is_default) ?? lanes.list[0])?.id;
|
|
58
|
+
if (!stateId)
|
|
59
|
+
throw new OrbitError("board_has_no_states");
|
|
60
|
+
if (!lanes.get(stateId))
|
|
61
|
+
throw new OrbitError("invalid_state");
|
|
62
|
+
const type = normalizeTicketType(input.type || (input.parent_ticket_id ? "feature" : "task"));
|
|
63
|
+
const parentId = await checkParent(w, input.parent_ticket_id, type);
|
|
64
|
+
const nebulaUrl = normalizeNebulaUrl(input.nebula_url);
|
|
65
|
+
const at = c.clock.now();
|
|
66
|
+
const ticket = await w.tickets.create({
|
|
67
|
+
id: c.ids.next(),
|
|
68
|
+
board_id: c.board.id,
|
|
69
|
+
title,
|
|
70
|
+
description: textOr(input.description),
|
|
71
|
+
type,
|
|
72
|
+
parent_ticket_id: parentId,
|
|
73
|
+
ai_plan: textOr(input.ai_plan),
|
|
74
|
+
plan_artifact_path: cardLinkFromInput(c, c.board.id, input.plan_artifact_path),
|
|
75
|
+
implementation_summary: textOr(input.implementation_summary),
|
|
76
|
+
implementation_artifact_path: cardLinkFromInput(c, c.board.id, input.implementation_artifact_path),
|
|
77
|
+
implementation_updates: textOr(input.implementation_updates),
|
|
78
|
+
nebula_url: nebulaUrl,
|
|
79
|
+
state_id: stateId,
|
|
80
|
+
priority: normalizePriority(input.priority),
|
|
81
|
+
created_by: c.actor.name,
|
|
82
|
+
created_at: at,
|
|
83
|
+
updated_at: at,
|
|
84
|
+
archived_at: null
|
|
85
|
+
});
|
|
86
|
+
const names = Array.isArray(input.labels) ? input.labels : [];
|
|
87
|
+
if (names.length) {
|
|
88
|
+
const labels = await w.labels.list();
|
|
89
|
+
for (const name of names) {
|
|
90
|
+
const label = await ensureLabel(w, c, labels, requiredString(name, "label"));
|
|
91
|
+
await w.ticketLabels.add(ticket.id, label.id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
await record(w, c, "ticket_created", ticket.id, { title, type, parent_ticket_id: parentId });
|
|
95
|
+
await w.search.index(ticket.id, searchDocument(ticket, []));
|
|
96
|
+
await touchTicket(w, parentId, at);
|
|
97
|
+
return ticketView(ticket, lanes, c.board);
|
|
98
|
+
});
|
|
99
|
+
c.changed(c.board.id);
|
|
100
|
+
return view;
|
|
101
|
+
}
|
|
102
|
+
const EDITABLE = [
|
|
103
|
+
"title",
|
|
104
|
+
"description",
|
|
105
|
+
"type",
|
|
106
|
+
"parent_ticket_id",
|
|
107
|
+
"ai_plan",
|
|
108
|
+
"plan_artifact_path",
|
|
109
|
+
"implementation_summary",
|
|
110
|
+
"implementation_artifact_path",
|
|
111
|
+
"implementation_updates",
|
|
112
|
+
"nebula_url",
|
|
113
|
+
"state_id",
|
|
114
|
+
"priority"
|
|
115
|
+
];
|
|
116
|
+
/** Reads a card on this board, or `ticket_not_found`. */
|
|
117
|
+
export async function ticketOn(r, ticketId) {
|
|
118
|
+
const ticket = await r.tickets.get(ticketId);
|
|
119
|
+
if (!ticket)
|
|
120
|
+
throw new OrbitError("ticket_not_found");
|
|
121
|
+
return ticket;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Changes a card's fields; `labels` (names) replaces its labels. Moving a card
|
|
125
|
+
* into a done lane deletes the relations that made it a blocker: the relation
|
|
126
|
+
* is the record of what still blocks.
|
|
127
|
+
*/
|
|
128
|
+
export async function updateTicket(c, ticketId, input) {
|
|
129
|
+
const view = await c.data.write(async (w) => {
|
|
130
|
+
const before = await ticketOn(w, ticketId);
|
|
131
|
+
const lanes = await Lanes.of(w);
|
|
132
|
+
const type = input.type !== undefined ? normalizeTicketType(input.type) : before.type;
|
|
133
|
+
// Each field as it will be stored; the same values drive the event, so history shows what was saved.
|
|
134
|
+
const applied = {};
|
|
135
|
+
for (const field of EDITABLE) {
|
|
136
|
+
const value = input[field];
|
|
137
|
+
if (value === undefined)
|
|
138
|
+
continue;
|
|
139
|
+
if (field === "type")
|
|
140
|
+
applied.type = type;
|
|
141
|
+
else if (field === "title")
|
|
142
|
+
applied.title = requiredString(value, "title");
|
|
143
|
+
else if (field === "priority")
|
|
144
|
+
applied.priority = normalizePriority(value);
|
|
145
|
+
else if (field === "nebula_url")
|
|
146
|
+
applied.nebula_url = normalizeNebulaUrl(value);
|
|
147
|
+
else if (field === "parent_ticket_id")
|
|
148
|
+
applied.parent_ticket_id = await checkParent(w, value, type, before.id);
|
|
149
|
+
else if (field === "plan_artifact_path" || field === "implementation_artifact_path")
|
|
150
|
+
applied[field] = cardLinkFromInput(c, c.board.id, value);
|
|
151
|
+
else
|
|
152
|
+
applied[field] = value;
|
|
153
|
+
}
|
|
154
|
+
// An epic has no parent, said or not.
|
|
155
|
+
if (type === "epic" && applied.parent_ticket_id === undefined)
|
|
156
|
+
applied.parent_ticket_id = null;
|
|
157
|
+
const labelNames = Array.isArray(input.labels) ? input.labels : null;
|
|
158
|
+
const fields = Object.keys(applied);
|
|
159
|
+
if (!fields.length && !labelNames)
|
|
160
|
+
return ticketView(before, lanes, c.board);
|
|
161
|
+
if (applied.state_id && !lanes.get(asText(applied.state_id)))
|
|
162
|
+
throw new OrbitError("invalid_state");
|
|
163
|
+
const at = c.clock.now();
|
|
164
|
+
let after = before;
|
|
165
|
+
if (fields.length)
|
|
166
|
+
after = await w.tickets.update(ticketId, { ...applied, updated_at: at });
|
|
167
|
+
if (labelNames) {
|
|
168
|
+
const labels = await w.labels.list();
|
|
169
|
+
const ids = [];
|
|
170
|
+
for (const raw of labelNames) {
|
|
171
|
+
const name = String(raw ?? "").trim().replace(/\s+/g, " ");
|
|
172
|
+
if (!name)
|
|
173
|
+
continue;
|
|
174
|
+
const label = await ensureLabel(w, c, labels, name);
|
|
175
|
+
if (!ids.includes(label.id))
|
|
176
|
+
ids.push(label.id);
|
|
177
|
+
}
|
|
178
|
+
await w.ticketLabels.set(ticketId, ids);
|
|
179
|
+
if (!fields.length)
|
|
180
|
+
after = await w.tickets.update(ticketId, { updated_at: at });
|
|
181
|
+
}
|
|
182
|
+
if (before.state_id !== after.state_id) {
|
|
183
|
+
if (lanes.roleOf(after) === "done" && lanes.roleOf(before) !== "done") {
|
|
184
|
+
for (const relation of await w.relations.list(ticketId)) {
|
|
185
|
+
const blocker = (relation.source_ticket_id === ticketId && relation.type === "blocks") || (relation.target_ticket_id === ticketId && relation.type === "blocked_by");
|
|
186
|
+
if (blocker)
|
|
187
|
+
await w.relations.remove(relation.id);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
await record(w, c, "state_changed", ticketId, { from: lanes.nameOf(before), to: lanes.nameOf(after), actor_type: c.actor.type, actor_id: c.actor.id });
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const body = { ...applied };
|
|
194
|
+
if (labelNames)
|
|
195
|
+
body.labels = labelNames;
|
|
196
|
+
if (Object.keys(body).length)
|
|
197
|
+
await record(w, c, "ticket_updated", ticketId, body);
|
|
198
|
+
}
|
|
199
|
+
await reindex(w, ticketId);
|
|
200
|
+
// Both epics changed on a move between them: one lost a sub-card, the other gained one.
|
|
201
|
+
await touchTicket(w, before.parent_ticket_id, at);
|
|
202
|
+
if (after.parent_ticket_id && after.parent_ticket_id !== before.parent_ticket_id)
|
|
203
|
+
await touchTicket(w, after.parent_ticket_id, at);
|
|
204
|
+
return ticketView(after, lanes, c.board);
|
|
205
|
+
});
|
|
206
|
+
c.changed(c.board.id);
|
|
207
|
+
return view;
|
|
208
|
+
}
|
|
209
|
+
async function setArchived(c, ticketId, archive) {
|
|
210
|
+
const { view, changed } = await c.data.write(async (w) => {
|
|
211
|
+
const ticket = await ticketOn(w, ticketId);
|
|
212
|
+
const lanes = await Lanes.of(w);
|
|
213
|
+
if (Boolean(ticket.archived_at) === archive)
|
|
214
|
+
return { view: ticketView(ticket, lanes, c.board), changed: false };
|
|
215
|
+
const at = c.clock.now();
|
|
216
|
+
const after = await w.tickets.update(ticketId, { archived_at: archive ? at : null, updated_at: at });
|
|
217
|
+
await record(w, c, archive ? "ticket_archived" : "ticket_restored", ticketId, { title: ticket.title });
|
|
218
|
+
await touchTicket(w, ticket.parent_ticket_id, at);
|
|
219
|
+
return { view: ticketView(after, lanes, c.board), changed: true };
|
|
220
|
+
});
|
|
221
|
+
if (changed)
|
|
222
|
+
c.changed(c.board.id);
|
|
223
|
+
return view;
|
|
224
|
+
}
|
|
225
|
+
/** Puts a card in the archive: off the board, out of search and of agents' work, its data kept. */
|
|
226
|
+
export function archiveTicket(c, ticketId) {
|
|
227
|
+
return setArchived(c, ticketId, true);
|
|
228
|
+
}
|
|
229
|
+
export function restoreTicket(c, ticketId) {
|
|
230
|
+
return setArchived(c, ticketId, false);
|
|
231
|
+
}
|
|
232
|
+
/** Deletes an archived card for good, with its comments, links and images; its events stay. */
|
|
233
|
+
export async function deleteTicket(c, ticketId) {
|
|
234
|
+
const paths = await c.data.write(async (w) => {
|
|
235
|
+
const ticket = await ticketOn(w, ticketId);
|
|
236
|
+
if (!ticket.archived_at)
|
|
237
|
+
throw new OrbitError("ticket_not_archived");
|
|
238
|
+
const images = await w.attachments.list(ticketId);
|
|
239
|
+
await record(w, c, "ticket_deleted", ticketId, { title: ticket.title, type: ticket.type, board_id: c.board.id });
|
|
240
|
+
await w.tickets.remove(ticketId);
|
|
241
|
+
await touchTicket(w, ticket.parent_ticket_id, c.clock.now());
|
|
242
|
+
return images.map((image) => `attachments/${image.stored_path}`);
|
|
243
|
+
});
|
|
244
|
+
for (const path of paths)
|
|
245
|
+
await c.blobs.remove(c.board.id, path);
|
|
246
|
+
c.changed(c.board.id);
|
|
247
|
+
return { ok: true, deleted_id: ticketId };
|
|
248
|
+
}
|
|
249
|
+
/** Adds a comment to a card: a person's, or an agent's note. */
|
|
250
|
+
export async function createComment(c, ticketId, input) {
|
|
251
|
+
const comment = await c.data.write(async (w) => {
|
|
252
|
+
const ticket = await ticketOn(w, ticketId);
|
|
253
|
+
const body = requiredString(input.body, "body");
|
|
254
|
+
const at = c.clock.now();
|
|
255
|
+
const made = {
|
|
256
|
+
id: c.ids.next(),
|
|
257
|
+
ticket_id: ticketId,
|
|
258
|
+
author: typeof input.author === "string" && input.author ? input.author : c.actor.name,
|
|
259
|
+
kind: typeof input.kind === "string" && input.kind ? input.kind : c.actor.type === "agent" ? "agent_note" : "human_comment",
|
|
260
|
+
body,
|
|
261
|
+
omitted_for_ai: 0,
|
|
262
|
+
created_at: at
|
|
263
|
+
};
|
|
264
|
+
await w.comments.insert(made);
|
|
265
|
+
await w.tickets.update(ticketId, { updated_at: at });
|
|
266
|
+
await record(w, c, "comment_created", ticketId, { kind: made.kind });
|
|
267
|
+
await reindex(w, ticketId);
|
|
268
|
+
await touchTicket(w, ticket.parent_ticket_id, at);
|
|
269
|
+
return made;
|
|
270
|
+
});
|
|
271
|
+
c.changed(c.board.id);
|
|
272
|
+
return comment;
|
|
273
|
+
}
|
|
274
|
+
/** Hides a comment from agents, or shows it again: it leaves (or rejoins) everything an agent can read or search. */
|
|
275
|
+
export async function setCommentHidden(c, ticketId, commentId, input) {
|
|
276
|
+
const comment = await c.data.write(async (w) => {
|
|
277
|
+
const ticket = await ticketOn(w, ticketId);
|
|
278
|
+
const existing = await w.comments.get(commentId);
|
|
279
|
+
if (existing?.ticket_id !== ticketId)
|
|
280
|
+
throw new OrbitError("comment_not_found");
|
|
281
|
+
const hidden = input.omitted_for_ai ?? input.omittedForAi;
|
|
282
|
+
if (typeof hidden !== "boolean")
|
|
283
|
+
throw new OrbitError("omitted_for_ai_required");
|
|
284
|
+
const at = c.clock.now();
|
|
285
|
+
const after = await w.comments.update(commentId, { omitted_for_ai: hidden ? 1 : 0 });
|
|
286
|
+
await w.tickets.update(ticketId, { updated_at: at });
|
|
287
|
+
await record(w, c, "comment_ai_omission_changed", ticketId, { comment_id: commentId, omitted_for_ai: hidden });
|
|
288
|
+
await reindex(w, ticketId);
|
|
289
|
+
await touchTicket(w, ticket.parent_ticket_id, at);
|
|
290
|
+
return after;
|
|
291
|
+
});
|
|
292
|
+
c.changed(c.board.id);
|
|
293
|
+
return comment;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* A card's moves between lanes, oldest first: moves, takes that moved it (into
|
|
297
|
+
* In Progress), asks and finishes, each event's data parsed as `body`.
|
|
298
|
+
*/
|
|
299
|
+
export async function ticketHistory(c, ticketId) {
|
|
300
|
+
return c.data.read(async (r) => {
|
|
301
|
+
await ticketOn(r, ticketId);
|
|
302
|
+
const events = await r.events.list({ ticketId, types: ["state_changed", "agent_claimed", "checkpoint_requested", "agent_completed"] });
|
|
303
|
+
const entries = events.map((event) => ({ ...event, body: JSON.parse(event.body_json) }));
|
|
304
|
+
// A take that left the card where it was (a takeover in place) moved nothing.
|
|
305
|
+
return entries.filter((entry) => entry.type !== "agent_claimed" || movedLanes(entry.body));
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function movedLanes(body) {
|
|
309
|
+
const { from, to } = (body ?? {});
|
|
310
|
+
return typeof to === "string" && to !== "" && from !== to;
|
|
311
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trello's board export, read as an Orbit export: lists become lanes (roles
|
|
3
|
+
* guessed from their names), cards become cards with their checklists,
|
|
4
|
+
* links and due dates in the description, labels and comments come along.
|
|
5
|
+
* Pure: it only reshapes the data it is given.
|
|
6
|
+
*/
|
|
7
|
+
import type { Clock, Ids } from "./context.ts";
|
|
8
|
+
type Raw = Record<string, unknown>;
|
|
9
|
+
export declare function looksLikeTrello(value: unknown): value is Raw;
|
|
10
|
+
/** A Trello export as an Orbit export, for the board `target` names (or one named from Trello's). */
|
|
11
|
+
export declare function trelloToSnapshot(trello: Raw, target: {
|
|
12
|
+
id?: string;
|
|
13
|
+
slug?: string;
|
|
14
|
+
repo_url?: string;
|
|
15
|
+
default_branch?: string;
|
|
16
|
+
}, clock: Clock, ids: Ids): Raw;
|
|
17
|
+
export {};
|