@anchrd/intel-api 0.5.1 → 0.6.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.
@@ -89,7 +89,15 @@ export function createKnowledgeRepository(deps) {
89
89
  * may see and never before them, and `COUNT(*) OVER ()` counts those same rows alone (#30).
90
90
  */
91
91
  const visibleChildren = (bounded) => `${visibleCte}
92
- SELECT ${nodeColumns}${bounded ? ", COUNT(*) OVER () AS total" : ""}
92
+ SELECT ${nodeColumns}${bounded ? ", COUNT(*) OVER () AS total" : ""},
93
+ -- Whether this row has children THIS actor may see (#59), asked of the same allowed set one
94
+ -- level down. A second rule written here would drift from the one above it, and the drift
95
+ -- would show as a chevron that opens onto nothing.
96
+ EXISTS (
97
+ SELECT 1 FROM knowledge_nodes child
98
+ JOIN allowed AS allowed_child ON allowed_child.id = child.id
99
+ WHERE child.parent_id = n.id AND child.archived_at IS NULL
100
+ ) AS has_children
93
101
  FROM knowledge_nodes n
94
102
  JOIN allowed ON allowed.id = n.id
95
103
  WHERE n.parent_id IS ? AND (? = 1 OR n.archived_at IS NULL)
@@ -100,7 +108,11 @@ export function createKnowledgeRepository(deps) {
100
108
  .prepare(visibleChildren(false))
101
109
  .bind(...readBindings(actor), input.parentId, input.includeArchived ? 1 : 0)
102
110
  .all();
103
- return (result.results ?? []).map(mapNode);
111
+ const rows = result.results ?? [];
112
+ return {
113
+ items: rows.map(mapNode),
114
+ withChildren: rows.filter((row) => row.has_children === 1).map((row) => row.id),
115
+ };
104
116
  },
105
117
  async listVisibleBounded(actor, input) {
106
118
  const result = await deps.db
@@ -280,6 +280,46 @@ export function createFlows(deps) {
280
280
  // library folder carries `execute` for everyone and nothing else (ADR-0004 §2/§3), so insisting on
281
281
  // `read` here would turn every library flow into a 404 for exactly the people it exists for — and
282
282
  // the call rule in section 3 would have nothing left to permit.
283
+ /**
284
+ * Which of these flows call another one that THIS actor may also see (#59).
285
+ *
286
+ * ⚠️ Per reader, and that is the point: a flow whose only call is hidden from this person has
287
+ * nothing to unfold, so a chevron there would promise what opening cannot deliver. `listCalls`
288
+ * filters the same way — asking the same question here, rather than reading the graph and hoping,
289
+ * is the only way the two cannot drift apart.
290
+ *
291
+ * ⚠️ Two reads for the whole level, never one per flow (#30): the versions come back together and
292
+ * the callees go out together.
293
+ */
294
+ async function callableCallers(actor, flows) {
295
+ const versionOf = new Map();
296
+ for (const flow of flows) {
297
+ const versionId = flow.currentVersionId ?? flow.publishedVersionId;
298
+ if (versionId)
299
+ versionOf.set(flow.id, versionId);
300
+ }
301
+ if (versionOf.size === 0)
302
+ return [];
303
+ const versions = new Map((await deps.repository.getVersions([...new Set(versionOf.values())])).map((version) => [
304
+ version.id,
305
+ version,
306
+ ]));
307
+ const wanted = new Map();
308
+ for (const [flowId, versionId] of versionOf) {
309
+ const version = versions.get(versionId);
310
+ if (version)
311
+ wanted.set(flowId, calleeIds(version.graph));
312
+ }
313
+ const everyCallee = [...new Set([...wanted.values()].flat())];
314
+ if (everyCallee.length === 0)
315
+ return [];
316
+ const reachable = new Set((await deps.repository.listCallable(actor, everyCallee))
317
+ .filter((callee) => !callee.archivedAt)
318
+ .map((callee) => callee.id));
319
+ return [...wanted.entries()]
320
+ .filter(([, callees]) => callees.some((callee) => reachable.has(callee)))
321
+ .map(([flowId]) => flowId);
322
+ }
283
323
  async function requireRunnableFlow(actor, flowId) {
284
324
  const flow = await deps.repository.getCallable(actor, flowId);
285
325
  if (!flow || flow.archivedAt)
@@ -720,7 +760,8 @@ export function createFlows(deps) {
720
760
  }
721
761
  return {
722
762
  async list(actor, input = {}) {
723
- return { items: await deps.repository.listVisible(actor, input) };
763
+ const items = await deps.repository.listVisible(actor, input);
764
+ return { items, withCalls: await callableCallers(actor, items) };
724
765
  },
725
766
  async get(actor, flowId) {
726
767
  const flow = await requireFlow(actor, flowId);
@@ -885,7 +926,7 @@ export function createFlows(deps) {
885
926
  const flow = await requireFlow(actor, flowId);
886
927
  const versionId = flow.currentVersionId ?? flow.publishedVersionId;
887
928
  if (!versionId)
888
- return { items: [] };
929
+ return { items: [], withCalls: [] };
889
930
  const version = await requireVersion(versionId, flow.id);
890
931
  const wanted = calleeIds(version.graph);
891
932
  // One read for every callee rather than one per callee (#30). This is what the sidebar asks
@@ -901,7 +942,8 @@ export function createFlows(deps) {
901
942
  if (callee && !callee.archivedAt)
902
943
  items.push(callee);
903
944
  }
904
- return { items };
945
+ // Ein aufgerufener Flow ruft selbst welche: dieselbe Frage, eine Ebene tiefer (#59).
946
+ return { items, withCalls: await callableCallers(actor, items) };
905
947
  },
906
948
  // "What this flow needs", straight out of the graph: no arithmetic, nothing that can go stale,
907
949
  // and no claim about whether anyone may reach it. A standing "this flow has conflicts" badge
@@ -157,10 +157,12 @@ export type FolderAccess = "ok" | "missing" | "not-a-folder" | "forbidden";
157
157
  export interface FlowService {
158
158
  list(actor: FlowActor, input?: ListFlowsInput): Promise<{
159
159
  items: Flow[];
160
+ withCalls: string[];
160
161
  }>;
161
162
  get(actor: FlowActor, flowId: string): Promise<FlowDocument>;
162
163
  listCalls(actor: FlowActor, flowId: string): Promise<{
163
164
  items: Flow[];
165
+ withCalls: string[];
164
166
  }>;
165
167
  validate(actor: FlowActor, flowId: string): Promise<FlowValidation>;
166
168
  relationGraph(actor: FlowActor, input: RelationGraphInput): Promise<RelationGraph>;
@@ -286,7 +286,7 @@ export function createKnowledge(deps) {
286
286
  }
287
287
  return {
288
288
  async list(actor, input) {
289
- return { items: await deps.repository.listVisible(actor, input) };
289
+ return await deps.repository.listVisible(actor, input);
290
290
  },
291
291
  // The same level under a bound, for the one caller that draws a bounded picture of it. It goes
292
292
  // through the same predicate as `list`, so what is drawn is a prefix of what is listed and never
@@ -25,7 +25,10 @@ export interface NewKnowledgeTableVersion {
25
25
  auditId: string;
26
26
  }
27
27
  export interface KnowledgeRepository {
28
- listVisible(actor: Actor, input: ListKnowledgeNodesInput): Promise<KnowledgeNode[]>;
28
+ listVisible(actor: Actor, input: ListKnowledgeNodesInput): Promise<{
29
+ items: KnowledgeNode[];
30
+ withChildren: string[];
31
+ }>;
29
32
  listVisibleBounded(actor: Actor, input: {
30
33
  parentId: string | null;
31
34
  limit: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anchrd/intel-api",
3
- "version": "0.5.1",
3
+ "version": "0.6.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.3.0",
46
+ "@anchrd/intel-contract": "^0.4.0",
47
47
  "@modelcontextprotocol/sdk": "^1.30.0",
48
48
  "ajv": "^8.20.0",
49
49
  "hono": "^4.12.32",