@anchrd/intel-api 0.12.5 → 0.14.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/dist/adapters/cloudflare/cloudflare.js +1 -0
- package/dist/adapters/cloudflare-api/cloudflare-api.js +153 -44
- package/dist/adapters/db/db-indexing.js +79 -0
- package/dist/adapters/db/db.js +66 -27
- package/dist/adapters/openid/openid.js +70 -10
- package/dist/adapters/semantic-index/semantic-index.js +97 -17
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +20 -1
- package/dist/bundle/bundle.js +46 -1
- package/dist/http/http.js +4 -0
- package/dist/indexing/indexing.js +177 -17
- package/dist/indexing/indexing.types.d.ts +1 -0
- package/dist/mcp/mcp.js +48 -34
- package/dist/nodes/board/board.d.ts +46 -0
- package/dist/nodes/board/board.js +475 -8
- package/dist/nodes/board/board.types.d.ts +7 -0
- package/dist/nodes/document-links/document-links.js +12 -1
- package/dist/nodes/nodes.js +152 -16
- package/dist/nodes/nodes.types.d.ts +61 -3
- package/dist/tools/tools.js +7 -1
- package/migrations/0009_no_context_policy.sql +15 -0
- package/migrations/0017_a_vector_per_card.sql +38 -0
- package/migrations/0018_no_context_policy_at_last.sql +90 -0
- package/package.json +2 -2
package/dist/mcp/mcp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AddBoardTaskInput, AppendTableRowsInput, ArchiveFlowInput, ArchiveNodeInput, CancelFlowRunInput, CompleteFlowRunStepInput, ConfigureBoardInput, CreateAgentInput, CreateFlowInput, CreateNodeInput, DefineTableInput, DeleteBoardTaskInput, DeleteTableRowsInput, ExecuteToolInput, GetAgentInput, GetBoardInput, GetFlowInput, GetFlowRunInput, GetFlowVersionInput, GetNodeInput, GetNodeVersionInput, GetTableInput, IntelId, ListAgentsInput, ListFlowRunsInput, ListFlowsInput, ListGrantsInput, ListNodesInput, MoveBoardTaskInput, NodeGraphInput, PauseAgentInput, PreviewFlowPublishInput, PublishFlowInput, RedefineTableInput, RelationGraphInput, ResolveNodeLinksInput, RevokeGrantInput, RotateAgentKeyInput, RunAgentNowInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveFlowVersionInput, SaveNodeVersionInput, SearchInput, ShareInput, StartFlowRunInput, TestToolInput, UnpublishFlowInput, UpdateBoardTaskInput, UpdateFlowInput, UpdateNodeInput, UpdateTableRowsInput, } from "@anchrd/intel-contract";
|
|
1
|
+
import { AddBoardTaskInput, AppendTableRowsInput, ArchiveFlowInput, ArchiveNodeInput, CancelFlowRunInput, CompleteFlowRunStepInput, ConfigureBoardInput, CreateAgentInput, CreateFlowInput, CreateNodeInput, DefineTableInput, DeleteBoardTaskInput, DeleteTableRowsInput, ExecuteToolInput, GetAgentInput, GetBoardInput, GetFlowInput, GetFlowRunInput, GetFlowVersionInput, GetNodeInput, GetNodeVersionInput, GetTableInput, IntelId, ListAgentsInput, ListFlowRunsInput, ListFlowsInput, ListGrantsInput, ListNodesInput, MoveBoardTaskInput, NodeGraphInput, PauseAgentInput, PreviewFlowPublishInput, PublishFlowInput, RedefineTableInput, RelationGraphInput, RepairBoardTaskIdsInput, ResolveNodeLinksInput, RevokeGrantInput, RotateAgentKeyInput, RunAgentNowInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveFlowVersionInput, SaveNodeVersionInput, SearchInput, ShareInput, StartFlowRunInput, TestToolInput, UnpublishFlowInput, UpdateBoardTaskInput, UpdateFlowInput, UpdateNodeInput, UpdateTableRowsInput, } from "@anchrd/intel-contract";
|
|
2
2
|
import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
4
4
|
import { z } from "zod";
|
|
@@ -8,18 +8,18 @@ function text(value) {
|
|
|
8
8
|
return { content: [{ type: "text", text: JSON.stringify(value) }], isError: false };
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* ⚠️ One name per tool, and the second one is gone for good (#149).
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* Every tool here carried a `knowledge_*` alias beside its name from #125 until today. The alias
|
|
14
|
+
* was never politeness: a tool name lives in somebody else's configuration — a portal endpoint, an
|
|
15
|
+
* agent definition, a saved prompt — so renaming it here alone breaks them at the moment we deploy
|
|
16
|
+
* rather than at a moment they chose. ADR-0005 §8 therefore kept both, **for a transition**.
|
|
17
|
+
*
|
|
18
|
+
* A transition without an end date is not a transition, it is a second permanent name — and two
|
|
19
|
+
* names for one tool is exactly the friction #125 removed. It ends here. What follows for anybody
|
|
20
|
+
* writing a tool: there is one name, and a rename is a breaking change that needs its own ticket
|
|
21
|
+
* and its own notice, not a quiet second entry.
|
|
18
22
|
*/
|
|
19
|
-
function registerWithAlias(server, names, config, handler) {
|
|
20
|
-
server.registerTool(names.name, config, handler);
|
|
21
|
-
server.registerTool(names.deprecated, { ...config, description: `Deprecated: use ${names.name}. ${config.description}` }, handler);
|
|
22
|
-
}
|
|
23
23
|
// MCP returns resource payloads inline as base64, so an attachment cannot be streamed to the client
|
|
24
24
|
// and must fit in Worker memory twice over. Refuse oversized attachments instead of losing the
|
|
25
25
|
// isolate; HTTP still serves them as a stream.
|
|
@@ -112,7 +112,7 @@ export async function handleMcp(request, deps) {
|
|
|
112
112
|
name: deps.authorization.identity.name ?? null,
|
|
113
113
|
}));
|
|
114
114
|
if (permits(deps.authorization, "knowledge", "read")) {
|
|
115
|
-
|
|
115
|
+
server.registerTool("node_list", {
|
|
116
116
|
title: "List nodes",
|
|
117
117
|
description: "List authorized folders, documents, attachments, and tables under one parent.",
|
|
118
118
|
inputSchema: ListNodesInput,
|
|
@@ -124,7 +124,7 @@ export async function handleMcp(request, deps) {
|
|
|
124
124
|
openWorldHint: false,
|
|
125
125
|
},
|
|
126
126
|
}, async (input) => text(await deps.nodes.list(actor, input)));
|
|
127
|
-
|
|
127
|
+
server.registerTool("node_get", {
|
|
128
128
|
title: "Get node",
|
|
129
129
|
description: "Get one authorized node and its current immutable content version.",
|
|
130
130
|
inputSchema: GetNodeInput,
|
|
@@ -136,7 +136,7 @@ export async function handleMcp(request, deps) {
|
|
|
136
136
|
openWorldHint: false,
|
|
137
137
|
},
|
|
138
138
|
}, async (input) => text(await deps.nodes.get(actor, input.nodeId)));
|
|
139
|
-
|
|
139
|
+
server.registerTool("node_versions_list", {
|
|
140
140
|
title: "List node versions",
|
|
141
141
|
description: "List immutable versions of one authorized node.",
|
|
142
142
|
inputSchema: GetNodeInput,
|
|
@@ -163,7 +163,7 @@ export async function handleMcp(request, deps) {
|
|
|
163
163
|
openWorldHint: false,
|
|
164
164
|
},
|
|
165
165
|
}, async (input) => text(await deps.nodes.getVersion(actor, input.nodeId, input.versionId)));
|
|
166
|
-
|
|
166
|
+
server.registerTool("node_attachment_get", {
|
|
167
167
|
title: "Get node attachment",
|
|
168
168
|
description: "Get authorized immutable attachment metadata and its explicit MCP resource URI.",
|
|
169
169
|
inputSchema: GetNodeInput,
|
|
@@ -199,7 +199,7 @@ export async function handleMcp(request, deps) {
|
|
|
199
199
|
],
|
|
200
200
|
};
|
|
201
201
|
});
|
|
202
|
-
|
|
202
|
+
server.registerTool("search", {
|
|
203
203
|
title: "Search nodes",
|
|
204
204
|
description: "Search only authorized indexed nodes and return version-pinned citations. Pass scopeId " +
|
|
205
205
|
"with a folder node id to search that folder and everything beneath it instead of " +
|
|
@@ -240,7 +240,7 @@ export async function handleMcp(request, deps) {
|
|
|
240
240
|
openWorldHint: false,
|
|
241
241
|
},
|
|
242
242
|
}, async (input) => text(await deps.nodes.getAgent(actor, input)));
|
|
243
|
-
|
|
243
|
+
server.registerTool("node_table_get", {
|
|
244
244
|
title: "Get node table",
|
|
245
245
|
description: "Read one authorized table as column names and rows.",
|
|
246
246
|
inputSchema: GetTableInput,
|
|
@@ -267,7 +267,7 @@ export async function handleMcp(request, deps) {
|
|
|
267
267
|
openWorldHint: false,
|
|
268
268
|
},
|
|
269
269
|
}, async (input) => text(await deps.nodes.getBoard(actor, input)));
|
|
270
|
-
|
|
270
|
+
server.registerTool("node_links_list", {
|
|
271
271
|
title: "List node links",
|
|
272
272
|
description: "List authorized outgoing links and backlinks for one node.",
|
|
273
273
|
inputSchema: GetNodeInput,
|
|
@@ -286,7 +286,7 @@ export async function handleMcp(request, deps) {
|
|
|
286
286
|
// ⚠️ A target this caller may not reach, or one that is gone, is simply absent from the answer.
|
|
287
287
|
// The two are indistinguishable on purpose: telling them apart would confirm that a document
|
|
288
288
|
// exists somewhere they cannot look.
|
|
289
|
-
|
|
289
|
+
server.registerTool("node_links_resolve", {
|
|
290
290
|
title: "Resolve node links",
|
|
291
291
|
description: "Resolve document link targets to the titles this caller is authorized to see. Targets that are unreachable or deleted are absent from the result.",
|
|
292
292
|
inputSchema: ResolveNodeLinksInput,
|
|
@@ -298,7 +298,7 @@ export async function handleMcp(request, deps) {
|
|
|
298
298
|
openWorldHint: false,
|
|
299
299
|
},
|
|
300
300
|
}, async (input) => text(await deps.nodes.resolveLinks(actor, input)));
|
|
301
|
-
|
|
301
|
+
server.registerTool("node_graph", {
|
|
302
302
|
title: "Get node graph",
|
|
303
303
|
description: "Get the authorized nodes and explicit relationships for discovery.",
|
|
304
304
|
inputSchema: NodeGraphInput,
|
|
@@ -330,7 +330,7 @@ export async function handleMcp(request, deps) {
|
|
|
330
330
|
}, async (input) => text(await deps.bundle.manifest(bundleActor, input.nodeId)));
|
|
331
331
|
}
|
|
332
332
|
if (deps.authorization.can("intel", "admin")) {
|
|
333
|
-
|
|
333
|
+
server.registerTool("node_reindex", {
|
|
334
334
|
title: "Reindex nodes",
|
|
335
335
|
description: "Queue every current canonical node version for derived index rebuild.",
|
|
336
336
|
inputSchema: EmptyInput,
|
|
@@ -344,7 +344,7 @@ export async function handleMcp(request, deps) {
|
|
|
344
344
|
}, async () => text(await deps.nodes.reindex(actor)));
|
|
345
345
|
}
|
|
346
346
|
if (permits(deps.authorization, "knowledge", "create")) {
|
|
347
|
-
|
|
347
|
+
server.registerTool("node_create", {
|
|
348
348
|
title: "Create node",
|
|
349
349
|
description: "Create a governed folder, document, attachment, or table node.",
|
|
350
350
|
inputSchema: CreateNodeInput,
|
|
@@ -415,7 +415,7 @@ export async function handleMcp(request, deps) {
|
|
|
415
415
|
openWorldHint: true,
|
|
416
416
|
},
|
|
417
417
|
}, async (input) => text(await deps.nodes.rotateAgentKey(actor, input, { token: deps.bearer })));
|
|
418
|
-
|
|
418
|
+
server.registerTool("node_save", {
|
|
419
419
|
title: "Save node version",
|
|
420
420
|
description: "Append an immutable content version using an optimistic base version.",
|
|
421
421
|
inputSchema: SaveNodeVersionInput,
|
|
@@ -427,7 +427,7 @@ export async function handleMcp(request, deps) {
|
|
|
427
427
|
openWorldHint: false,
|
|
428
428
|
},
|
|
429
429
|
}, async (input) => text(await deps.nodes.save(actor, input)));
|
|
430
|
-
|
|
430
|
+
server.registerTool("node_attachment_save", {
|
|
431
431
|
title: "Save node attachment",
|
|
432
432
|
description: "Append immutable base64 file bytes to an attachment node.",
|
|
433
433
|
inputSchema: SaveAttachmentInput,
|
|
@@ -459,7 +459,7 @@ export async function handleMcp(request, deps) {
|
|
|
459
459
|
zip: decodeZipBase64(input.zipBase64),
|
|
460
460
|
idempotencyKey: input.idempotencyKey,
|
|
461
461
|
})));
|
|
462
|
-
|
|
462
|
+
server.registerTool("node_table_define", {
|
|
463
463
|
title: "Define node table columns",
|
|
464
464
|
description: "Write the column names of an empty table. The header is the contract every append is checked against and cannot be rewritten.",
|
|
465
465
|
inputSchema: DefineTableInput,
|
|
@@ -473,7 +473,7 @@ export async function handleMcp(request, deps) {
|
|
|
473
473
|
}, async (input) => text(await deps.nodes.defineTable(actor, input)));
|
|
474
474
|
// The tool #40 exists for: an agent collecting findings on a schedule appends them without
|
|
475
475
|
// reading or resending what is already there, and two agents appending at once lose nothing.
|
|
476
|
-
|
|
476
|
+
server.registerTool("node_table_append", {
|
|
477
477
|
title: "Append node table rows",
|
|
478
478
|
description: "Append rows to a table without reading or resending its existing content. Each row must have exactly as many cells as the table has columns; a row that does not is rejected and nothing is written.",
|
|
479
479
|
inputSchema: AppendTableRowsInput,
|
|
@@ -540,7 +540,7 @@ export async function handleMcp(request, deps) {
|
|
|
540
540
|
*/
|
|
541
541
|
server.registerTool("board_configure", {
|
|
542
542
|
title: "Configure board statuses",
|
|
543
|
-
description: 'Write the board\'s whole status list, in the order it should be drawn. Adding, renaming and reordering columns are all this call. The "archived" status is part of every board and cannot be removed, and a status that tasks still sit in cannot be removed either.',
|
|
543
|
+
description: 'Write the board\'s whole status list, in the order it should be drawn. Adding, renaming and reordering columns are all this call. The "archived" status is part of every board and cannot be removed, and a status that tasks still sit in cannot be removed either. Set "terminal" on a column to say that standing in it means the work is finished — that is what decides whether a task waiting on another is still blocked. Several columns may be terminal; "archived" always is and cannot be set otherwise. Omitting it leaves an ordinary column not terminal.',
|
|
544
544
|
inputSchema: ConfigureBoardInput,
|
|
545
545
|
annotations: {
|
|
546
546
|
title: "Configure board statuses",
|
|
@@ -553,7 +553,7 @@ export async function handleMcp(request, deps) {
|
|
|
553
553
|
}, async (input) => text(await deps.nodes.configureBoard(actor, input)));
|
|
554
554
|
server.registerTool("board_task_add", {
|
|
555
555
|
title: "Add board task",
|
|
556
|
-
description: "Add one task to a board. The server assigns its id and its place in the order; name afterTaskId or beforeTaskId to put it between two cards, or neither to put it last in its column. parentId makes it a subtask of another task on the same board, dependsOn names tasks of the same board it waits for, and references names Intel nodes. Needs no baseVersionId: tasks are addressed by id, so parallel writers do not collide.",
|
|
556
|
+
description: "Add one task to a board. The server assigns its id and its place in the order; name afterTaskId or beforeTaskId to put it between two cards, or neither to put it last in its column. parentId makes it a subtask of another task on the same board, dependsOn names tasks of the same board it waits for, and references names Intel nodes. dependsOn, labels and references are sets: naming the same task, word or node twice is refused rather than quietly folded together. Needs no baseVersionId: tasks are addressed by id, so parallel writers do not collide.",
|
|
557
557
|
inputSchema: AddBoardTaskInput,
|
|
558
558
|
annotations: {
|
|
559
559
|
title: "Add board task",
|
|
@@ -565,7 +565,7 @@ export async function handleMcp(request, deps) {
|
|
|
565
565
|
}, async (input) => text(await deps.nodes.addBoardTask(actor, input)));
|
|
566
566
|
server.registerTool("board_task_update", {
|
|
567
567
|
title: "Update board task",
|
|
568
|
-
description: "Change what a task says: title, assignee, labels, dates, dependencies, description or references. Where a task SITS — its status, its parent, its order — is board_task_move instead. Every named field replaces its current value whole; fields that are not named stay as they are.",
|
|
568
|
+
description: "Change what a task says: title, assignee, labels, dates, dependencies, description or references. Where a task SITS — its status, its parent, its order — is board_task_move instead. Every named field replaces its current value whole; fields that are not named stay as they are. dependsOn, labels and references are sets: appending to a list this task already holds is refused rather than quietly folded together, so read the current value before you write it back.",
|
|
569
569
|
inputSchema: UpdateBoardTaskInput,
|
|
570
570
|
annotations: {
|
|
571
571
|
title: "Update board task",
|
|
@@ -601,7 +601,21 @@ export async function handleMcp(request, deps) {
|
|
|
601
601
|
openWorldHint: false,
|
|
602
602
|
},
|
|
603
603
|
}, async (input) => text(await deps.nodes.deleteBoardTask(actor, input)));
|
|
604
|
-
|
|
604
|
+
server.registerTool("board_repair_task_ids", {
|
|
605
|
+
title: "Repair repeated board task ids",
|
|
606
|
+
description: "Give a fresh id to every task entry on a board that repeats an id another entry already used. Only a board imported from a hand-written or foreign bundle can hold such a pair, and it cannot be repaired with the other calls: they address a task by id, so they would read, write or remove both entries at once. The first entry under an id keeps it — that is the one every parentId and dependsOn on the board already means — and each later one becomes its own task with everything else it carries, so nothing is lost and no edge is invented. The answer names each pair. A board that names every task once is refused rather than rewritten. Repeated STATUS ids are repaired with board_configure instead, by writing the status list with distinct ids.",
|
|
607
|
+
inputSchema: RepairBoardTaskIdsInput,
|
|
608
|
+
annotations: {
|
|
609
|
+
title: "Repair repeated board task ids",
|
|
610
|
+
readOnlyHint: false,
|
|
611
|
+
// Nothing is removed and nothing is overwritten: one entry gains an id of its own and
|
|
612
|
+
// every other field on every task stays exactly as it stood.
|
|
613
|
+
destructiveHint: false,
|
|
614
|
+
idempotentHint: true,
|
|
615
|
+
openWorldHint: false,
|
|
616
|
+
},
|
|
617
|
+
}, async (input) => text(await deps.nodes.repairBoardTaskIds(actor, input)));
|
|
618
|
+
server.registerTool("node_update", {
|
|
605
619
|
title: "Update node",
|
|
606
620
|
description: "Rename, move, or describe a node.",
|
|
607
621
|
inputSchema: UpdateNodeInput,
|
|
@@ -613,7 +627,7 @@ export async function handleMcp(request, deps) {
|
|
|
613
627
|
openWorldHint: false,
|
|
614
628
|
},
|
|
615
629
|
}, async (input) => text(await deps.nodes.update(actor, input)));
|
|
616
|
-
|
|
630
|
+
server.registerTool("node_archive", {
|
|
617
631
|
title: "Archive node",
|
|
618
632
|
description: "Archive or restore one node using optimistic concurrency. Archiving an agent also switches off its Gate application; restoring switches it back on.",
|
|
619
633
|
inputSchema: ArchiveNodeInput,
|
|
@@ -632,7 +646,7 @@ export async function handleMcp(request, deps) {
|
|
|
632
646
|
async (input) => text(await deps.nodes.archive(actor, input, { token: deps.bearer })));
|
|
633
647
|
}
|
|
634
648
|
if (permits(deps.authorization, "knowledge", "share")) {
|
|
635
|
-
|
|
649
|
+
server.registerTool("node_shares_list", {
|
|
636
650
|
title: "List node shares",
|
|
637
651
|
description: "List direct grants for nodes the caller is allowed to manage.",
|
|
638
652
|
inputSchema: ListGrantsInput,
|
|
@@ -644,7 +658,7 @@ export async function handleMcp(request, deps) {
|
|
|
644
658
|
openWorldHint: false,
|
|
645
659
|
},
|
|
646
660
|
}, async (input) => text(await deps.nodes.listGrants(actor, input.resourceId)));
|
|
647
|
-
|
|
661
|
+
server.registerTool("node_share", {
|
|
648
662
|
title: "Share node",
|
|
649
663
|
description: "Grant inherited node access to a Gate user, verified email, or the organization.",
|
|
650
664
|
inputSchema: ShareInput,
|
|
@@ -656,7 +670,7 @@ export async function handleMcp(request, deps) {
|
|
|
656
670
|
openWorldHint: false,
|
|
657
671
|
},
|
|
658
672
|
}, async (input) => text(await deps.nodes.share(actor, input)));
|
|
659
|
-
|
|
673
|
+
server.registerTool("node_revoke_share", {
|
|
660
674
|
title: "Revoke node share",
|
|
661
675
|
description: "Revoke one direct node grant by ID.",
|
|
662
676
|
inputSchema: RevokeGrantInput,
|
|
@@ -1,4 +1,50 @@
|
|
|
1
|
+
import { type BoardDocument } from "@anchrd/intel-contract";
|
|
1
2
|
import type { BoardDeps, BoardOperations } from "./board.types.js";
|
|
3
|
+
/**
|
|
4
|
+
* A stored board brought up to the current schema, before it is validated.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ It runs before `BoardDocument.parse`, on `unknown`, because after that parse a missing field or
|
|
7
|
+
* a repeated id is already a refusal — and `parseStoredBoard` is deliberately loud
|
|
8
|
+
* (`board_unreadable`), so an un-upgraded board would not read as "old", it would read as corrupt.
|
|
9
|
+
*
|
|
10
|
+
* The upgrade is persisted by the next write of any kind, because every write serialises the whole
|
|
11
|
+
* document. Nothing has to be migrated ahead of time and nothing has to be re-migrated.
|
|
12
|
+
*/
|
|
13
|
+
export declare function upgradeStoredBoard(parsed: unknown): unknown;
|
|
14
|
+
/**
|
|
15
|
+
* The first id a board document uses for two different things (anchrd/intel#321).
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ Deliberately NOT a rule on `BoardDocument`, and that is the whole decision. A schema rule would
|
|
18
|
+
* reach every parse of a stored body — `parseStoredBoard`, `indexing.ts`, the link reader — and a
|
|
19
|
+
* board imported before this existed would stop answering with `board_unreadable`: the whole board
|
|
20
|
+
* gone over something that costs one view. It is the trap #311 walked into, and #318 walked into
|
|
21
|
+
* from the other side.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ And NOT folded in `upgradeStoredBoard` either, which is where the sibling rules of #318 went. A
|
|
24
|
+
* repeated `dependsOn`, label or reference carries no information, so dropping the second mention
|
|
25
|
+
* loses nothing. Two tasks under one id are two whole tasks — titles, dates, descriptions — and a
|
|
26
|
+
* fold on the READ is written back by the next save of any kind, because every write serialises the
|
|
27
|
+
* whole document. That is one task gone for good, for every board, without anybody having asked.
|
|
28
|
+
* The UI folds the same pair for a DRAWING (`orderedTasks`), which costs nothing: the document keeps
|
|
29
|
+
* both, and repairing the pair brings the second card back.
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ What this does NOT rescue, and it is worth knowing: `replaced` matches a task by id, so the
|
|
32
|
+
* first `board_task_update` against such a pair writes the SAME task into both entries. The stored
|
|
33
|
+
* board is then two identical tasks rather than two different ones. That is a consequence of
|
|
34
|
+
* addressing a board by task id at all (#285) and predates this rule; the reason the rule is at the
|
|
35
|
+
* import is to stop the pair from existing, not to make it survivable. A board that already holds
|
|
36
|
+
* one is repaired by `repairTaskIds` below — asked for, never done on the quiet — and the refusal
|
|
37
|
+
* here names it, because "your file is wrong" without a way out is where anchrd/intel#341 started.
|
|
38
|
+
*
|
|
39
|
+
* So it is asked exactly where there is a caller to answer: the bundle import, the one door a board
|
|
40
|
+
* document written elsewhere comes in through. Everything else mints task ids itself (`deps.id()`)
|
|
41
|
+
* and takes its status list through `ConfigureBoardInput`, which has demanded distinct ids since
|
|
42
|
+
* #285.
|
|
43
|
+
*/
|
|
44
|
+
export declare function repeatedBoardId(document: BoardDocument): {
|
|
45
|
+
list: "statuses" | "tasks";
|
|
46
|
+
id: string;
|
|
47
|
+
} | null;
|
|
2
48
|
/**
|
|
3
49
|
* Every board operation, applied to the document rather than to the file (#285).
|
|
4
50
|
*
|