@anchrd/intel-api 0.13.0 → 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.
@@ -1,4 +1,4 @@
1
- import type { AddBoardTaskInput, AgentKeyRotated, AgentList, AppendTableRowsInput, AppendTableRowsResult, ArchiveNodeInput, BoardTaskResult, ConfigureBoardInput, ConfigureBoardResult, CreateAgentInput, CreatedAgent, CreateNodeInput, DefineTableInput, DeleteBoardTaskInput, DeleteBoardTaskResult, DeleteTableRowsInput, DeleteTableRowsResult, Flow, FlowVersion, GetAgentInput, GetBoardInput, ListAgentsInput, ListNodesInput, MoveBoardTaskInput, Node, NodeAgent, NodeAttachment, NodeBoard, NodeCitation, NodeDocument, NodeGraph, NodeGraphInput, NodeLink, NodeTable, NodeVersion, RedefineTableInput, ResolveNodeLinksInput, ResolveNodeLinksResult, ResourceGrant, ResourceGrantList, ResourceVerb, RevokeGrantInput, RotateAgentKeyInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, ShareInput, ShareResult, UpdateBoardTaskInput, UpdateNodeInput, UpdateTableRowsInput, UpdateTableRowsResult } from "@anchrd/intel-contract";
1
+ import type { AddBoardTaskInput, AgentKeyRotated, AgentList, AppendTableRowsInput, AppendTableRowsResult, ArchiveNodeInput, BoardTaskResult, ConfigureBoardInput, ConfigureBoardResult, CreateAgentInput, CreatedAgent, CreateNodeInput, DefineTableInput, DeleteBoardTaskInput, DeleteBoardTaskResult, DeleteTableRowsInput, DeleteTableRowsResult, Flow, FlowVersion, GetAgentInput, GetBoardInput, ListAgentsInput, ListNodesInput, MoveBoardTaskInput, Node, NodeAgent, NodeAttachment, NodeBoard, NodeCitation, NodeDocument, NodeGraph, NodeGraphInput, NodeLink, NodeTable, NodeVersion, RedefineTableInput, RepairBoardTaskIdsInput, RepairBoardTaskIdsResult, ResolveNodeLinksInput, ResolveNodeLinksResult, ResourceGrant, ResourceGrantList, ResourceVerb, RevokeGrantInput, RotateAgentKeyInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, ShareInput, ShareResult, UpdateBoardTaskInput, UpdateNodeInput, UpdateTableRowsInput, UpdateTableRowsResult } from "@anchrd/intel-contract";
2
2
  import type { SemanticIndex } from "../adapters/semantic-index/semantic-index.types.js";
3
3
  export interface Actor {
4
4
  id: string;
@@ -27,7 +27,7 @@ export interface NewTableVersion {
27
27
  idempotencyKey: string;
28
28
  auditId: string;
29
29
  }
30
- export type SnapshotOperation = "node.table_update" | "node.table_delete" | "node.table_redefine" | "node.board_configure" | "node.board_task_add" | "node.board_task_update" | "node.board_task_move" | "node.board_task_delete";
30
+ export type SnapshotOperation = "node.table_update" | "node.table_delete" | "node.table_redefine" | "node.board_configure" | "node.board_task_add" | "node.board_task_update" | "node.board_task_move" | "node.board_task_delete" | "node.board_task_repair";
31
31
  /**
32
32
  * One version that replaces the readable state, written only against the state it replaces.
33
33
  *
@@ -145,7 +145,21 @@ export interface NodeRepository {
145
145
  occurredAt: string;
146
146
  }): Promise<boolean>;
147
147
  searchVisible(actor: Actor, input: SearchInput): Promise<NodeCitation[]>;
148
- hydrateVisibleCitations(actor: Actor, nodeIds: string[], scopeId?: string): Promise<NodeCitation[]>;
148
+ /**
149
+ * `scopeId` cuts the candidates to one folder subtree, which is how the semantic half of a scoped
150
+ * search is narrowed (#126): the vector index answers over everything and this join is where the
151
+ * subtree — a relation in D1, not a value on a vector — is applied. The ACL still applies too.
152
+ *
153
+ * ⚠️ One entry per node, and it names WHICH chunk answered (anchrd/intel#301). The passage a
154
+ * searcher is shown is then that card rather than whichever one happens to be first in the board;
155
+ * a node whose winning chunk has no `node_vectors` row — every vector written before #301 — falls
156
+ * back on the first full-text passage, which is exactly what this returned before.
157
+ */
158
+ hydrateVisibleCitations(actor: Actor, hits: Array<{
159
+ nodeId: string;
160
+ chunkKey: string;
161
+ }>, scopeId?: string): Promise<NodeCitation[]>;
162
+ invalidateVectors(): Promise<void>;
149
163
  listCurrentVersionIds(input: {
150
164
  after: string | null;
151
165
  limit: number;
@@ -354,6 +368,15 @@ export interface NodeService {
354
368
  updateBoardTask(actor: Actor, input: UpdateBoardTaskInput): Promise<BoardTaskResult>;
355
369
  moveBoardTask(actor: Actor, input: MoveBoardTaskInput): Promise<BoardTaskResult>;
356
370
  deleteBoardTask(actor: Actor, input: DeleteBoardTaskInput): Promise<DeleteBoardTaskResult>;
371
+ /**
372
+ * The sixth, and the only one that is not about somebody's project (anchrd/intel#341).
373
+ *
374
+ * ⚠️ It names no task id, because a board that needs it holds two entries under one — the pair a
375
+ * bundle written before anchrd/intel#321 could carry in. Everything else here is unreachable for
376
+ * that second entry: the five operations above address a task by id and would take, write or
377
+ * remove both at once.
378
+ */
379
+ repairBoardTaskIds(actor: Actor, input: RepairBoardTaskIdsInput): Promise<RepairBoardTaskIdsResult>;
357
380
  getAgent(actor: Actor, input: GetAgentInput): Promise<NodeAgent>;
358
381
  /**
359
382
  * An agent node, its first definition, and the Gate Application it runs as (#182, D29).
@@ -435,12 +458,47 @@ export interface NodeIndexTarget {
435
458
  * task's own title, which is the text the FTS table weights highest.
436
459
  */
437
460
  export interface IndexChunk {
461
+ /**
462
+ * What this passage is called inside its node (anchrd/intel#301).
463
+ *
464
+ * `""` for a node that is one passage — every kind but `board` — and the task's own id for a card.
465
+ * The full-text index does not store it; the vector index is named by it, which is how a semantic
466
+ * hit can say WHICH card answered instead of only which board.
467
+ */
468
+ key: string;
438
469
  title: string;
439
470
  text: string;
440
471
  }
472
+ /**
473
+ * What the vector index holds for one chunk of one node (anchrd/intel#301).
474
+ *
475
+ * `fingerprint` is over the exact text that was embedded, so the next pass can tell an untouched
476
+ * card from a changed one without asking Vectorize — whose writes are asynchronous and would answer
477
+ * about the save before last. `passage` is what a searcher is shown when this vector is the hit.
478
+ */
479
+ export interface NodeVectorRecord {
480
+ chunkKey: string;
481
+ fingerprint: string;
482
+ passage: string;
483
+ }
441
484
  export interface NodeIndexRepository {
442
485
  getTarget(versionId: string): Promise<NodeIndexTarget | null>;
486
+ /**
487
+ * The node behind this version when the reason `getTarget` refused it is that the node has been
488
+ * ARCHIVED — and null for every other reason (anchrd/intel#348).
489
+ *
490
+ * ⚠️ The distinction is the whole method. `getTarget` answers null for two very different
491
+ * situations: the node is archived, or this version has been superseded by a later save. The
492
+ * first one means "take this node out of the vector index"; the second is a queue message that
493
+ * arrived late — the ordinary case, because Cloudflare Queues deliver at least once — and acting
494
+ * on it would delete the vectors of a node that is perfectly alive. So this asks for the archived
495
+ * case by name and never infers it from an absence.
496
+ */
497
+ archivedNodeId(versionId: string): Promise<string | null>;
443
498
  replace(target: NodeIndexTarget, chunks: IndexChunk[]): Promise<void>;
499
+ listVectors(nodeId: string): Promise<NodeVectorRecord[]>;
500
+ replaceVectors(target: NodeIndexTarget, records: NodeVectorRecord[]): Promise<void>;
501
+ deleteVectors(nodeId: string): Promise<void>;
444
502
  markIndexed(versionId: string, occurredAt: string): Promise<void>;
445
503
  markError(versionId: string, message: string, occurredAt: string): Promise<void>;
446
504
  }
@@ -1,5 +1,13 @@
1
1
  -- #76. `context_policy` leaves the contract, the UI and every MCP answer. The COLUMN stays.
2
2
  --
3
+ -- ⚠️ HISTORY, and no longer an explanation of what is possible. The column was finally dropped by
4
+ -- `0018_no_context_policy_at_last.sql` (#86), through an ordinary migration file — the very thing
5
+ -- the conclusion at the bottom of this file says cannot be done. What changed is not D1 but the
6
+ -- recipe: `0005`, `0013` and `0016` worked out that the new table has to be created under the FINAL
7
+ -- name instead of renamed into place, and that `node_links` has to be carried out of the way
8
+ -- because it is the one child declared ON DELETE CASCADE. Read on for the three failures that
9
+ -- produced the lessons; read `0018` for the shape that works.
10
+ --
3
11
  -- ⚠️ That is not the intent, it is what D1 permits. SQLite cannot drop a column a CHECK names, and
4
12
  -- this one names itself. The way around it is a table rebuild, and `knowledge_nodes` carries six
5
13
  -- foreign keys, one of them from itself.
@@ -22,6 +30,13 @@
22
30
  -- A table rebuild with foreign keys is therefore not possible inside a migration file. It needs a
23
31
  -- session that drives the transaction itself.
24
32
  --
33
+ -- ⚠️ THAT CONCLUSION WAS WRONG, and the way it was wrong is worth more than the conclusion. The
34
+ -- three lessons above are all correct; what they did not contain was the fourth — do not RENAME at
35
+ -- all. Create the new table under the final name and insert the rows back under the name the
36
+ -- children have referenced the whole time, and there is nothing left for a deferred check to
37
+ -- complain about at COMMIT. `0005` found it, `0013` and `0016` repeated it, and `0018` used it to
38
+ -- drop this column at last (#86).
39
+ --
25
40
  -- What holds instead: the column sits in D1, nothing reads it, and `db.ts` writes a fixed value on
26
41
  -- insert because it is NOT NULL without a DEFAULT. The contract does not know it — for every
27
42
  -- consumer it is gone. What remains is one dead column, and that is the price of Intel running.
@@ -0,0 +1,38 @@
1
+ -- anchrd/intel#301: one vector per board card, and the record of which vectors a node has.
2
+ --
3
+ -- Vectorize is keyed by node id (`adapters/semantic-index`), so a board was ONE vector holding the
4
+ -- whole document while the full-text half had held one row per card since #285. "Where do I stand
5
+ -- with X" is the question a board exists to answer and it is an imprecise one: lexically it reaches
6
+ -- a card only where the searched word is written on it verbatim. A vector id therefore becomes
7
+ -- `<node id>#<task id>` for a board card and stays the bare node id for every other kind — no
8
+ -- existing vector changes its name, so no installation re-embeds its whole tree to get this.
9
+ --
10
+ -- This table is the D1 side of that index and nothing more: which vectors a node has, what text
11
+ -- each one was made from, and the passage a searcher is shown when that vector is the hit. It is
12
+ -- DERIVED like `node_fts` beside it, and `reindex` empties it so that a rebuild really rebuilds.
13
+ --
14
+ -- ⚠️ `fingerprint` is what keeps a board of three hundred cards from costing three hundred
15
+ -- embeddings per save. The indexing pass compares it against the text it is about to embed and
16
+ -- upserts only what changed; a row is written ONLY AFTER the upsert it describes succeeded. That is
17
+ -- why a fingerprint is stored rather than a timestamp: a pass that dies between the two leaves a
18
+ -- row missing, never a row claiming a vector that was never written, so the next pass repairs
19
+ -- itself instead of trusting a lie.
20
+ --
21
+ -- ⚠️ `chunk_key` is `''` for a node that has exactly one vector — every kind but `board`. NULL
22
+ -- would be the honest spelling and is the wrong one: NULLs are distinct from one another inside a
23
+ -- SQLite primary key, so two rows for the same node could both exist and neither would be found by
24
+ -- the other's write.
25
+ --
26
+ -- ⚠️ No foreign key on `node_id`, deliberately, and for the same reason `node_fts` has none: a
27
+ -- derived index must not be able to make a write to `nodes` fail, and every rebuild of `nodes`
28
+ -- (0005, 0013, 0016, 0018) has to carry each declared child through the detour those files describe.
29
+ -- A row that outlives its node is invisible anyway — hydration joins `nodes` and drops what is
30
+ -- archived or gone, exactly as it does for `node_fts`.
31
+ CREATE TABLE node_vectors (
32
+ node_id TEXT NOT NULL,
33
+ chunk_key TEXT NOT NULL,
34
+ version_id TEXT NOT NULL,
35
+ fingerprint TEXT NOT NULL,
36
+ passage TEXT NOT NULL,
37
+ PRIMARY KEY (node_id, chunk_key)
38
+ );
@@ -0,0 +1,90 @@
1
+ -- #86: `context_policy` leaves `nodes` for good.
2
+ --
3
+ -- #76 removed it from the contract, the UI and every MCP answer — for every consumer it has been
4
+ -- gone since. The COLUMN stayed because SQLite cannot drop one a CHECK names, and `db.ts` has been
5
+ -- writing the fixed value `'relevant'` into it ever since so that inserting a node works at all.
6
+ -- A dead column plus a line of code serving it: harmless, and ballast the next reader has to be
7
+ -- told about.
8
+ --
9
+ -- ⚠️ THE reason this ticket sat open for weeks is that three earlier attempts failed against
10
+ -- `--remote` while passing locally, and the file that recorded them (`0009_no_context_policy.sql`)
11
+ -- concluded that a table rebuild with foreign keys needs "a session that runs the transaction
12
+ -- itself, not a migration file". That conclusion is out of date, and the proof is in this
13
+ -- directory: `0005`, `0013` and `0016` each rebuilt this very table through an ordinary migration
14
+ -- file. `0016` did it against the live database on 2026-08-08 with 120 nodes, and every referencing
15
+ -- table came out with the same count it went in with.
16
+ --
17
+ -- ⚠️ One thing about that run is worth knowing before it is quoted as a precedent: the installation
18
+ -- held ZERO `node_links` rows, so the rescue below would have had nothing to rescue and the run
19
+ -- would have passed without exercising it at all. The one row it did carry was created for the
20
+ -- purpose, minutes before, by linking two throwaway documents. The proof was arranged, not found —
21
+ -- and the next person rebuilding this table has to arrange it again, because the installation still
22
+ -- has almost no links.
23
+ --
24
+ -- What the three of them worked out, and what this file copies rather than rediscovers:
25
+ --
26
+ -- 1. `PRAGMA foreign_keys = OFF` is IGNORED by D1 over the HTTP API. Locally miniflare obeys it,
27
+ -- so the integration test was green while the real database answered `FOREIGN KEY constraint
28
+ -- failed`. That is why the proof of a migration here is a run against `--remote`.
29
+ -- 2. The new table is created under the FINAL name rather than built beside the old one and
30
+ -- renamed over it. `ALTER TABLE ... RENAME` makes references FOLLOW the rename, so renaming
31
+ -- the old table out of the way quietly re-points every other table at a table about to be
32
+ -- dropped. Inserting the rows again under the name the children have referenced all along is
33
+ -- what settles them.
34
+ -- 3. `DROP TABLE` on a parent runs an implicit `DELETE FROM` first, and `node_links` is the one
35
+ -- child declared ON DELETE CASCADE — so that delete does not merely flag its rows, it REMOVES
36
+ -- them. They are carried out of the way and put back. That is a rescue, not a decision about
37
+ -- the data.
38
+ --
39
+ -- ⚠️ On an empty database neither detour is visible, because nothing points at anything. That is
40
+ -- how `0005`'s first version passed a green suite and then failed against the first database with
41
+ -- content in it, and why the proof for this file is a row count of every referencing table before
42
+ -- and after rather than a migration that merely ran.
43
+ PRAGMA defer_foreign_keys = TRUE;
44
+
45
+ -- Plain holding tables on purpose: no keys, no CHECKs, no foreign keys, and the column set taken
46
+ -- from whatever the live table has. Anything enforced here would only be enforced a second time on
47
+ -- the way back in, and a holding table that can reject a row is a holding table that can lose one.
48
+ CREATE TABLE nodes_carry AS SELECT * FROM nodes;
49
+ CREATE TABLE node_links_carry AS SELECT * FROM node_links;
50
+
51
+ DROP TABLE nodes;
52
+
53
+ -- The same table as `0016` left it, minus one column. Nothing else about it changes: same keys,
54
+ -- same CHECKs, same six kinds — a rebuild is the only way to drop the column, not an invitation to
55
+ -- change anything else while the table is open.
56
+ CREATE TABLE nodes (
57
+ id TEXT PRIMARY KEY NOT NULL,
58
+ parent_id TEXT REFERENCES nodes(id),
59
+ kind TEXT NOT NULL CHECK (kind IN ('folder', 'document', 'attachment', 'table', 'agent', 'board')),
60
+ title TEXT NOT NULL CHECK (length(title) BETWEEN 1 AND 240),
61
+ description TEXT CHECK (description IS NULL OR length(description) <= 2000),
62
+ owner_id TEXT NOT NULL,
63
+ current_version_id TEXT,
64
+ created_at TEXT NOT NULL,
65
+ updated_at TEXT NOT NULL,
66
+ archived_at TEXT
67
+ );
68
+
69
+ -- Columns named on both sides rather than `SELECT *`: the holding table still HAS `context_policy`,
70
+ -- and a positional insert would either fail or, worse, shift every value one place to the left.
71
+ INSERT INTO nodes (
72
+ id, parent_id, kind, title, description, owner_id,
73
+ current_version_id, created_at, updated_at, archived_at
74
+ )
75
+ SELECT
76
+ id, parent_id, kind, title, description, owner_id,
77
+ current_version_id, created_at, updated_at, archived_at
78
+ FROM nodes_carry;
79
+
80
+ -- `OR IGNORE` because whether the cascade above actually fired is SQLite's business, not this
81
+ -- migration's: if it did, this puts the rows back; if it did not, each one is already present under
82
+ -- the same primary key and this is a no-op. Either way `node_links` ends up holding exactly what it
83
+ -- held before, which is the only outcome this statement is permitted to have.
84
+ INSERT OR IGNORE INTO node_links SELECT * FROM node_links_carry;
85
+
86
+ DROP TABLE nodes_carry;
87
+ DROP TABLE node_links_carry;
88
+
89
+ CREATE INDEX nodes_parent_idx ON nodes(parent_id, archived_at, title);
90
+ CREATE INDEX nodes_owner_idx ON nodes(owner_id, archived_at);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anchrd/intel-api",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@anchrd/gate-sdk": "^0.7.0",
46
- "@anchrd/intel-contract": "^0.11.0",
46
+ "@anchrd/intel-contract": "^0.12.0",
47
47
  "@cfworker/json-schema": "^4.1.1",
48
48
  "@modelcontextprotocol/sdk": "^1.30.0",
49
49
  "fflate": "^0.8.3",