{"version":3,"file":"AppGraphBuilder.mjs","names":[],"sources":["../../src/AppGraphBuilder.ts"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\nimport type * as Context from 'effect/Context';\nimport * as Effect from 'effect/Effect';\nimport * as Function from 'effect/Function';\nimport * as Option from 'effect/Option';\nimport * as Atom from 'effect/reactivity/Atom';\nimport * as Registry from 'effect/reactivity/AtomRegistry';\n\nimport { Entity, type Type } from '@dxos/echo';\n// eslint-disable-next-line @dxos/rules/import-as-namespace -- `GraphBuilder` is this file's own class.\nimport * as Builder from '@dxos/graph/GraphBuilder';\nimport * as GraphNode from '@dxos/graph/GraphNode';\nimport { invariant } from '@dxos/invariant';\nimport { DXN } from '@dxos/keys';\nimport { log } from '@dxos/log';\nimport { isNonNullable } from '@dxos/util';\nimport * as Position from '@dxos/util/Position';\n\nimport { type FrameBudget, makeFrameBudget, scheduleTask, yieldOrContinue } from '#scheduler';\n\nimport * as Graph from './AppGraph.ts';\nimport * as Node from './AppGraphNode.ts';\nimport { nodeArgsUnchanged, normalizeRelation, withLabel } from './util.ts';\n\n//\n// Extension Types\n//\n\n/**\n * Graph builder extension for adding nodes to the graph based on a connection to an existing node.\n *\n * @param params.node The existing node the returned nodes will be connected to.\n */\nexport type ConnectorExtension = Builder.Connector<Node.Node, Node.NodeArg<any>>;\n\n/**\n * Constrained case of the connector extension for more easily adding actions to the graph.\n */\nexport type ActionsExtension = (\n  node: Atom.Atom<Option.Option<Node.Node>>,\n) => Atom.Atom<Omit<Node.NodeArg<Node.ActionData<any>>, 'type' | 'nodes' | 'edges'>[]>;\n\n/**\n * Constrained case of the connector extension for more easily adding action groups to the graph.\n */\nexport type ActionGroupsExtension = (\n  node: Atom.Atom<Option.Option<Node.Node>>,\n) => Atom.Atom<Omit<Node.NodeArg<typeof Node.actionGroupSymbol>, 'type' | 'data' | 'nodes' | 'edges'>[]>;\n\n/**\n * A generic builder extension specialized to the app vocabulary: app nodes, app relations, and a\n * {@link UrlBinding} as the extension metadata the app layer's node decorator reads.\n */\nexport type BuilderExtension = Builder.Extension<Node.Node, Node.NodeArg<any>, Node.RelationInput, UrlBinding>;\n\nexport type BuilderExtensions = Builder.Extensions<BuilderExtension>;\n\n/**\n * How an extension's nodes map to (and from) the URL pair chain. The `kind` is the resolution tier:\n *\n * - `'item'`      — Addressed by an id under the workspace (`doc/<id>`). May itself have children.\n * - `'singleton'` — A single fixed node per workspace, addressed by the key alone (`settings`); its own\n *                   segment is the key, below `path`.\n *\n * The binding's shape (`workspace`, `path`, `minDepth`) decides which node ids it addresses, and\n * a node's URL comes from the one binding its id fits. A node therefore has a URL whether or not it is\n * loaded, and bindings of different keys must not claim the same ids.\n *\n * The anchor and linked tiers are not declared per extension: they are fixed keys of the URL grammar,\n * configured once on the builder as {@link UrlGrammar}.\n */\nexport type UrlBinding = {\n  key: string;\n  kind: 'item' | 'singleton';\n  /** Node-id segments between the workspace and the node's own. */\n  path: readonly string[];\n  /**\n   * The fewest segments an item's id spans after `path`, joined by the grammar's tail separator; defaults\n   * to 1. Of two bindings sharing a path, the larger minimum claims the deeper ids. Ignored with `resolve`.\n   */\n  minDepth?: number;\n  /** Narrows the workspaces the binding applies to; defaults to every workspace. */\n  workspace?: (workspace: string) => boolean;\n  /**\n   * Forward resolution for a data-dependent shape (nested collections, say): the id is the node's last\n   * segment, at any depth below `path`, and this locates the rest.\n   */\n  resolve?: PathResolver;\n};\n\n/**\n * The URL grammar the builder resolves and stamps against, configured once at construction.\n *\n * The two keys are fixed tiers no extension declares (no connector produces their nodes): `anchorKey`\n * establishes the base that following pairs resolve against and is consumed as a rebase\n * (`w/<workspace>`); `linked` addresses a node attached to the preceding item through its relation\n * (`<key>/<variant>`, its segment `<prefix><variant>`). `tailSeparator` joins the node-id segments\n * after a key's static `path` into one URL id (`db/<slug>+<id>`), so a nested shape needs no resolver.\n */\nexport type UrlGrammar = {\n  anchorKey?: string;\n  linked: LinkedGrammar;\n  tailSeparator: string;\n};\n\n/** How a node attached to the preceding item is addressed: its URL key, its relation, its segment prefix. */\nexport type LinkedGrammar = { key: string; relation: Node.RelationInput; prefix: string };\n\n/** {@link UrlGrammar} as supplied at construction; anything omitted falls back to its default. */\nexport type UrlGrammarProps = Partial<Omit<UrlGrammar, 'linked'>> & { linked?: Partial<LinkedGrammar> };\n\n/** Default linked grammar; the prefix mirrors `@dxos/react-ui-attention`'s `linkedSegment`. */\nconst DEFAULT_LINKED: LinkedGrammar = { key: 'linked', relation: 'linked', prefix: '~' };\n\n/** Default tail separator; never appears in an entity id or a type slug. Internal, as above. */\nconst DEFAULT_TAIL_SEPARATOR = '+';\n\n/** Params passed to a {@link PathResolver} for a single `(key, id)` URL pair. */\nexport type PathResolveParams = {\n  /** The id segment from the `(key, id)` pair. */\n  id: string;\n  /** The workspace segment from the URL. */\n  workspace: string;\n  /** Qualified id of the workspace base node (`${GraphNode.RootId}/<workspace>`). */\n  workspaceBaseId: string;\n};\n\n/**\n * Forward resolution for a {@link UrlBinding} whose ids are data-dependent. Returns the candidate qualified\n * node id — `path-resolution.ts` then materializes its ancestors and verifies it — or `null` if the id\n * can't be located. Must be self-contained (the declaring plugin closes over any services it needs), so\n * `@dxos/app-graph` stays free of service dependencies.\n */\nexport type PathResolver = (params: PathResolveParams) => Effect.Effect<string | null>;\n\nconst startsWith = (segments: readonly string[], prefix: readonly string[]): boolean =>\n  prefix.length <= segments.length && prefix.every((segment, index) => segments[index] === segment);\n\n/**\n * The `(key, id?)` a binding gives the node `nodeId`, if its shape addresses that node: the reverse of\n * forward resolution, minus the workspace. A singleton has no id; a resolver-backed item keeps its last\n * segment; any other item joins the segments after `path` with the tail separator.\n */\nexport const urlRepresentation = (\n  nodeId: string,\n  url: UrlBinding,\n  tailSeparator: string = DEFAULT_TAIL_SEPARATOR,\n): Option.Option<{ key: string; id?: string }> => {\n  const [, workspace, ...segments] = nodeId.split(GraphNode.PathSeparator);\n  if (!workspace || !(url.workspace?.(workspace) ?? true) || !startsWith(segments, url.path)) {\n    return Option.none();\n  }\n  const tail = segments.slice(url.path.length);\n  if (url.kind === 'singleton') {\n    return tail.length === 1 && tail[0] === url.key ? Option.some({ key: url.key }) : Option.none();\n  }\n  if (tail.length === 0) {\n    return Option.none();\n  }\n  if (url.resolve) {\n    return Option.some({ key: url.key, id: tail[tail.length - 1] });\n  }\n  return tail.length >= (url.minDepth ?? 1)\n    ? Option.some({ key: url.key, id: tail.join(tailSeparator) })\n    : Option.none();\n};\n\n/**\n * The node a binding without a resolver addresses by `id` under `workspace`, if its shape admits that id:\n * the inverse of {@link urlRepresentation}. A singleton takes no id.\n */\nexport const urlCandidate = (\n  url: UrlBinding,\n  workspace: string,\n  id: string | undefined,\n  tailSeparator: string = DEFAULT_TAIL_SEPARATOR,\n): Option.Option<string> => {\n  if (url.resolve || !(url.workspace?.(workspace) ?? true)) {\n    return Option.none();\n  }\n  const base = [GraphNode.RootId, workspace, ...url.path];\n  if (url.kind === 'singleton') {\n    return id === undefined ? Option.some([...base, url.key].join(GraphNode.PathSeparator)) : Option.none();\n  }\n  const tail = id?.split(tailSeparator) ?? [];\n  return tail.length > 0 && tail.length >= (url.minDepth ?? 1)\n    ? Option.some([...base, ...tail].join(GraphNode.PathSeparator))\n    : Option.none();\n};\n\n/**\n * A node's own URL pair segment — `/<key>[/<id>]`, with no workspace/anchor prefix — or `undefined` when\n * the binding does not address it. A full URL is composed by prefixing `/w/<workspace>`.\n */\nexport const nodeUrlSegment = (\n  nodeId: string,\n  url: UrlBinding,\n  tailSeparator: string = DEFAULT_TAIL_SEPARATOR,\n): string | undefined =>\n  Option.match(urlRepresentation(nodeId, url, tailSeparator), {\n    onNone: () => undefined,\n    onSome: ({ key, id }) => (id === undefined ? `/${key}` : `/${key}/${id}`),\n  });\n\n//\n// Builder\n//\n\nexport type GraphBuilderTraverseOptions = Builder.TraverseOptions<Node.Node, Node.RelationInput>;\n\n/** Construction params: the backing graph's props plus the URL grammar's fixed keys. */\nexport type GraphBuilderProps = Pick<Graph.GraphProps, 'registry' | 'nodes' | 'edges'> & {\n  urlGrammar?: UrlGrammarProps;\n  /**\n   * Applied to each connector-produced node before it enters the graph. Defaults to stamping the\n   * URL segment implied by the producing extension's binding.\n   */\n  decorateNode?: (node: Node.NodeArg<any>, extension?: BuilderExtension) => Node.NodeArg<any>;\n};\n\n/**\n * The generic expansion engine specialized to the app vocabulary: app nodes and relations over an\n * {@link Graph.ExpandableGraph}, extensions carrying a {@link UrlBinding}, and connector-produced nodes\n * stamped with their URL segment as they enter the graph.\n */\nexport class GraphBuilder extends Builder.GraphBuilder<\n  Node.Node,\n  Node.NodeArg<any>,\n  Node.RelationInput,\n  UrlBinding,\n  Graph.ExpandableGraph\n> {\n  /** The URL grammar (see {@link UrlGrammar}); the keys are absent when URLs are not in play. */\n  readonly urlGrammar: UrlGrammar;\n\n  readonly #frameBudget = makeFrameBudget();\n\n  constructor({ registry, urlGrammar, decorateNode, ...graphProps }: GraphBuilderProps = {}) {\n    const grammar: UrlGrammar = {\n      tailSeparator: DEFAULT_TAIL_SEPARATOR,\n      ...urlGrammar,\n      linked: { ...DEFAULT_LINKED, ...urlGrammar?.linked },\n    };\n    super({\n      registry,\n      relationKey: (relation) => Graph.relationKey(relation ?? 'child'),\n      inline,\n      unchanged: nodeArgsUnchanged,\n      decorateNode,\n      store: (hooks, resolvedRegistry) => makeStore(graphProps, hooks, resolvedRegistry),\n    });\n    // An empty prefix would make every segment read as linked.\n    invariant(grammar.linked.prefix.length > 0, 'UrlGrammar.linked.prefix must not be empty');\n    invariant(grammar.tailSeparator.length > 0, 'UrlGrammar.tailSeparator must not be empty');\n    this.urlGrammar = grammar;\n  }\n\n  /** Hand flushes to the scheduler so a large expansion yields to the main thread. */\n  override _schedule(callback: () => void): Promise<void> {\n    return scheduleTask(callback, { strategy: 'smooth' });\n  }\n\n  override _yield(): Promise<void> {\n    return yieldOrContinue('idle');\n  }\n\n  override _frameBudget(): FrameBudget | undefined {\n    return this.#frameBudget;\n  }\n\n  override _onReleaseRelation(target: { id: string; relation: string }): void {\n    super._onReleaseRelation(target);\n    Graph.releaseRelation(this.graph, target.id, target.relation);\n  }\n\n  override _onExpand(id: string, relation: string): void {\n    super._onExpand(id, relation);\n\n    // TODO(wittjosiah): Remove. This is for backwards compatibility.\n    if (relation === CHILD_RELATION) {\n      Graph.expandSync(this.graph, id, Node.action);\n    }\n  }\n}\n\nconst CHILD_RELATION = Graph.relationKey('child');\n\n/** How an app node argument's inline descendants are traversed: its child nodes and its actions. */\nconst inline: Builder.Inline<Node.NodeArg<any>> = {\n  children: (node) => [...(node.nodes ?? []), ...(node.actions ?? [])],\n  map: (node, fn) => ({ ...node, nodes: node.nodes?.map(fn), actions: node.actions?.map(fn) }),\n};\n\n/** Adapt the app graph to the engine's store port, translating between relation keys and relations. */\nconst makeStore = (\n  props: Pick<Graph.GraphProps, 'nodes' | 'edges'>,\n  hooks: Builder.StoreHooks,\n  registry: Registry.AtomRegistry,\n): Builder.Store<Node.Node, Node.NodeArg<any>, Graph.ExpandableGraph> => {\n  const decode = ({ source, target, relation }: Builder.Edge): Graph.Edge => ({\n    source,\n    target,\n    relation: Graph.relationFromKey(relation),\n  });\n\n  const graph = Graph.make({\n    ...props,\n    registry,\n    onExpand: (id, relation) => hooks.onExpand(id, Graph.relationKey(relation)),\n    onRemoveNode: hooks.onRemoveNode,\n  });\n  // Connectors read node atoms between writes, so the builder keeps them pinned for its lifetime.\n  const release = Graph.retain(graph);\n\n  return {\n    graph,\n    node: (id) => graph.node(id),\n    addNodes: (nodes) => void Graph.addNodes(graph, [...nodes]),\n    removeNodes: (ids, edges) => void Graph.removeNodes(graph, [...ids], edges),\n    addEdges: (edges) => void Graph.addEdges(graph, edges.map(decode)),\n    removeEdges: (edges, removeOrphans) => void Graph.removeEdges(graph, edges.map(decode), removeOrphans),\n    sortEdges: (id, relation, order) => void Graph.sortEdges(graph, id, Graph.relationFromKey(relation), [...order]),\n    setNode: (id, node) => graph._setNode(id, node),\n    batch: (fn) => Graph.batch(graph, fn),\n    release: (ids) => void Graph.release(graph, ids),\n    outgoing: (id) =>\n      Graph.getInternal(graph)\n        ._model.outgoing(id)\n        .map(({ source, target, type }) => ({ source, target, relation: type })),\n    constructNode: (node) => graph._constructNode(node),\n    dispose: release,\n  };\n};\n\n/**\n * Creates a new GraphBuilder instance.\n */\nexport const make = (params?: GraphBuilderProps): GraphBuilder => new GraphBuilder(params);\n\n/**\n * Call from the handler of a user action, before its writes: graph updates those writes cause flush\n * before the next paint instead of waiting out the frame budget.\n */\nexport const flushBeforePaint = (builder: GraphBuilder): void => {\n  const budget = builder._frameBudget();\n  if (!budget) {\n    return;\n  }\n  budget.flushBeforePaint();\n  // A connector an exhausted budget left dirty is not reported dirty again, so flush it under the grant too.\n  builder._scheduleDirtyFlush(true);\n};\n\n/**\n * Creates a GraphBuilder from a serialized pickle string.\n */\nexport const from = (pickle?: string, registry?: Registry.AtomRegistry, urlGrammar?: UrlGrammarProps): GraphBuilder => {\n  if (!pickle) {\n    return make({ registry, urlGrammar });\n  }\n\n  const { nodes, edges } = JSON.parse(pickle);\n  return make({ nodes, edges, registry, urlGrammar });\n};\n\n// The expansion lifecycle is the generic engine's; the app layer only specializes the vocabulary.\n// Named (not namespace) re-export: this module already exports its own `GraphBuilder` class above.\nexport {\n  addExtension,\n  destroy,\n  explore,\n  flush,\n  release,\n  removeExtension,\n  setRetention,\n} from '@dxos/graph/GraphBuilder';\n\n/**\n * Flatten arbitrarily nested extension groups into a single list. Pinned to the app extension type,\n * which the generic signature cannot infer from a recursively nested argument.\n */\nexport const flattenExtensions = (extensions: BuilderExtensions, acc: BuilderExtension[] = []): BuilderExtension[] =>\n  Builder.flattenExtensions<BuilderExtension>(extensions, acc);\n\n//\n// Extension Creation\n//\n\n/**\n * A graph builder extension is used to add nodes to the graph.\n *\n * @param params.id The unique id of the extension.\n * @param params.relation The relation the graph is being expanded from the existing node.\n * @param params.position Affects the order the extensions are processed in.\n * @param params.url URL binding for the nodes this extension produces (key + resolution); see {@link UrlBinding}.\n * @param params.connector A function to add nodes to the graph based on a connection to an existing node.\n * @param params.actions A function to add actions to the graph based on a connection to an existing node.\n * @param params.actionGroups A function to add action groups to the graph based on a connection to an existing node.\n */\nexport type CreateExtensionRawOptions<Id extends string = string> = {\n  id: [DXN.Path<Id>] extends [never]\n    ? `Invalid id \"${Id}\": final segment must be camelCase — letters and digits, starting with a letter`\n    : Id;\n  relation?: Node.RelationInput;\n  position?: Position.Position;\n  url?: UrlBinding;\n  connector?: ConnectorExtension;\n  actions?: ActionsExtension;\n  actionGroups?: ActionGroupsExtension;\n};\n\n/**\n * Create a graph builder extension (low-level API that works directly with Atoms).\n */\nexport const createExtensionRaw = <const Id extends string = string>(\n  extension: CreateExtensionRawOptions<Id>,\n): BuilderExtension[] => {\n  const {\n    id,\n    position,\n    relation = 'child',\n    url,\n    connector: _connector,\n    actions: _actions,\n    actionGroups: _actionGroups,\n  } = extension;\n  if (!DXN.isValidPath(id)) {\n    log.warn(\n      'dropping graph extension with invalid id; the final segment must be camelCase — letters and digits, starting with a letter',\n      {\n        id,\n      },\n    );\n    return [];\n  }\n  const normalizedRelation = normalizeRelation(relation);\n  const getId = (key: string) => `${id}/${key}`;\n\n  const connector =\n    _connector &&\n    Atom.family((node: Atom.Atom<Option.Option<Node.Node>>) =>\n      _connector(node).pipe(withLabel(`graph-builder:_connector:${id}`)),\n    );\n\n  const actionGroups =\n    _actionGroups &&\n    Atom.family((node: Atom.Atom<Option.Option<Node.Node>>) =>\n      _actionGroups(node).pipe(withLabel(`graph-builder:_actionGroups:${id}`)),\n    );\n\n  const actions =\n    _actions &&\n    Atom.family((node: Atom.Atom<Option.Option<Node.Node>>) =>\n      _actions(node).pipe(withLabel(`graph-builder:_actions:${id}`)),\n    );\n\n  const extensions = [\n    connector\n      ? ({\n          id: getId('connector'),\n          position,\n          relation: normalizedRelation,\n          meta: url,\n          connector: Atom.family((node) =>\n            Atom.make((get) => {\n              try {\n                return get(connector(node));\n              } catch (error) {\n                log.warn('Error in connector', { id: getId('connector'), node, error });\n                return [];\n              }\n            }).pipe(withLabel(`graph-builder:connector:${id}`)),\n          ),\n        } satisfies BuilderExtension)\n      : undefined,\n    actionGroups\n      ? ({\n          id: getId('actionGroups'),\n          position,\n          relation: Node.action,\n          connector: Atom.family((node) =>\n            Atom.make((get) => {\n              try {\n                return get(actionGroups(node)).map((arg) => ({\n                  ...arg,\n                  data: Node.actionGroupSymbol,\n                  type: Node.ActionGroupType,\n                }));\n              } catch (error) {\n                log.warn('Error in actionGroups', { id: getId('actionGroups'), node, error });\n                return [];\n              }\n            }).pipe(withLabel(`graph-builder:connector:actionGroups:${id}`)),\n          ),\n        } satisfies BuilderExtension)\n      : undefined,\n    actions\n      ? ({\n          id: getId('actions'),\n          position,\n          relation: Node.action,\n          connector: Atom.family((node) =>\n            Atom.make((get) => {\n              try {\n                return get(actions(node)).map((arg) => ({ ...arg, type: Node.ActionType }));\n              } catch (error) {\n                log.warn('Error in actions', { id: getId('actions'), node, error });\n                return [];\n              }\n            }).pipe(withLabel(`graph-builder:connector:actions:${id}`)),\n          ),\n        } satisfies BuilderExtension)\n      : undefined,\n  ].filter(isNonNullable);\n\n  // A declaration-only extension: a `url` binding with no connector/actions (e.g. the workspace anchor,\n  // which registers a key for the parser/serializer but produces no nodes of its own). Emit it so the\n  // key table sees the binding; it has no connector so it never runs.\n  if (extensions.length === 0 && url) {\n    return [{ id, position, relation: normalizedRelation, meta: url } satisfies BuilderExtension];\n  }\n\n  return extensions;\n};\n\n/**\n * Options for creating a graph builder extension with simplified API.\n * All callbacks must return Effects for dependency injection.\n * Effects may defect — defects are caught, logged, and the extension returns empty results.\n * Use Effect.orDie on any failable effects inside callbacks.\n */\nexport type CreateExtensionOptions<TMatched = Node.Node, R = never, Id extends string = string> = {\n  id: [DXN.Path<Id>] extends [never]\n    ? `Invalid id \"${Id}\": final segment must be camelCase — letters and digits, starting with a letter`\n    : Id;\n  match: (node: Node.Node, get: Atom.AtomContext) => Option.Option<TMatched>;\n  actions?: (\n    matched: TMatched,\n    get: Atom.AtomContext,\n  ) => Effect.Effect<Omit<Node.NodeArg<Node.ActionData<any>, any>, 'type'>[], never, R>;\n  /** Contribute dropdown action groups (each with nested `actions`) to the matched node; the group's\n   * `type`/`data` are set automatically, so returning `Node.makeActionGroup(...)` output is fine. */\n  actionGroups?: (\n    matched: TMatched,\n    get: Atom.AtomContext,\n  ) => Effect.Effect<Omit<Node.NodeArg<typeof Node.actionGroupSymbol>, 'type' | 'data'>[], never, R>;\n  connector?: (matched: TMatched, get: Atom.AtomContext) => Effect.Effect<Node.NodeArg<any, any>[], never, R>;\n  relation?: Node.RelationInput;\n  position?: Position.Position;\n  /** URL binding for the nodes this extension produces (key + resolution); see {@link UrlBinding}. */\n  url?: UrlBinding;\n};\n\n/** Marks an extension body's first run; which bodies ran is not derivable from the extension list. */\nconst markBodyRun = (ran: Set<string>, extensionId: string, kind: string): void => {\n  if (ran.has(kind) || typeof performance === 'undefined') {\n    return;\n  }\n  ran.add(kind);\n  performance.mark(`graph-body:${kind}:${extensionId}`);\n};\n\n/**\n * Run an Effect synchronously with the provided context.\n * Defects are caught, logged, and the fallback value is returned.\n * @internal\n */\nconst runEffectSyncWithFallback = <T, R>(\n  effect: Effect.Effect<T, never, R>,\n  context: Context.Context<R>,\n  extensionId: string,\n  fallback: T,\n): T => {\n  return Effect.runSync(\n    effect.pipe(\n      Effect.provide(context),\n      Effect.catchDefect((defect) => {\n        log.warn('Extension failed', { extension: extensionId, defect });\n        return Effect.succeed(fallback);\n      }),\n    ),\n  );\n};\n\n/**\n * Create a graph builder extension with simplified API.\n * Returns an Effect to allow callbacks to access services via dependency injection.\n */\nexport const createExtension = <TMatched = Node.Node, R = never, const Id extends string = string>(\n  options: CreateExtensionOptions<TMatched, R, Id>,\n): Effect.Effect<BuilderExtension[], never, R> =>\n  Effect.map(Effect.context<R>(), (context) => {\n    const { id, match, actions, actionGroups, connector, relation, position, url } = options;\n    const bodiesRun = new Set<string>();\n\n    const connectorExtension = connector\n      ? createConnectorWithRuntime(id, match, connector, context, bodiesRun)\n      : undefined;\n\n    const actionsExtension = actions\n      ? (node: Atom.Atom<Option.Option<Node.Node>>) =>\n          Atom.make((get) =>\n            Function.pipe(\n              get(node),\n              Option.flatMap((matchedNode) => match(matchedNode, get)),\n              Option.map((matched) => {\n                markBodyRun(bodiesRun, id, 'actions');\n                return runEffectSyncWithFallback(actions(matched, get), context, id, []).map((action) => ({\n                  ...action,\n                  // Attach captured context for action execution.\n                  _actionContext: context,\n                }));\n              }),\n              Option.getOrElse(() => []),\n            ),\n          )\n      : undefined;\n\n    const actionGroupsExtension = actionGroups\n      ? (node: Atom.Atom<Option.Option<Node.Node>>) =>\n          Atom.make((get) =>\n            Function.pipe(\n              get(node),\n              Option.flatMap((matchedNode) => match(matchedNode, get)),\n              Option.map((matched) => {\n                markBodyRun(bodiesRun, id, 'actionGroups');\n                return runEffectSyncWithFallback(actionGroups(matched, get), context, id, []).map((group) => ({\n                  ...group,\n                  // Attach captured context to the group's child actions so they execute with the\n                  // extension's services (e.g. Capability.Service) even without an explicit runner.\n                  actions: group.actions?.map((action) => ({ ...action, _actionContext: context })),\n                }));\n              }),\n              Option.getOrElse(() => []),\n            ),\n          )\n      : undefined;\n\n    return createExtensionRaw({\n      id,\n      relation,\n      position,\n      url,\n      connector: connectorExtension,\n      actions: actionsExtension,\n      actionGroups: actionGroupsExtension,\n    });\n  });\n\n/**\n * Create a connector extension from a matcher and factory function.\n * The factory's data type is inferred from the matcher's return type.\n */\nexport const createConnector = <TData>(\n  matcher: (node: Node.Node, get: Atom.AtomContext) => Option.Option<TData>,\n  factory: (data: TData, get: Atom.AtomContext) => Node.NodeArg<any>[],\n): ConnectorExtension => Builder.createConnector(matcher, factory);\n\n/**\n * Create a connector extension from a matcher and factory function with Effect support.\n * The factory must return an Effect. Errors are caught and logged.\n * @internal\n */\nconst createConnectorWithRuntime = <TData, R>(\n  extensionId: string,\n  matcher: (node: Node.Node, get: Atom.AtomContext) => Option.Option<TData>,\n  factory: (data: TData, get: Atom.AtomContext) => Effect.Effect<Node.NodeArg<any>[], never, R>,\n  context: Context.Context<R>,\n  bodiesRun: Set<string>,\n): ConnectorExtension =>\n  Builder.createConnector(matcher, (data, get) => {\n    markBodyRun(bodiesRun, extensionId, 'connector');\n    return runEffectSyncWithFallback(factory(data, get), context, extensionId, []);\n  });\n\n/**\n * Options for creating a type-based extension.\n * All callbacks must return Effects for dependency injection.\n * Effects may fail - errors are caught, logged, and the extension returns empty results.\n */\nexport type CreateTypeExtensionOptions<\n  T extends Type.AnyEntity = Type.AnyEntity,\n  R = never,\n  Id extends string = string,\n> = {\n  id: [DXN.Path<Id>] extends [never]\n    ? `Invalid id \"${Id}\": final segment must be camelCase — letters and digits, starting with a letter`\n    : Id;\n  type: T;\n  actions?: (\n    object: Type.InstanceType<T>,\n    get: Atom.AtomContext,\n  ) => Effect.Effect<Omit<Node.NodeArg<Node.ActionData<any>>, 'type'>[], never, R>;\n  actionGroups?: (\n    object: Type.InstanceType<T>,\n    get: Atom.AtomContext,\n  ) => Effect.Effect<Omit<Node.NodeArg<typeof Node.actionGroupSymbol>, 'type' | 'data'>[], never, R>;\n  connector?: (object: Type.InstanceType<T>, get: Atom.AtomContext) => Effect.Effect<Node.NodeArg<any>[], never, R>;\n  relation?: Node.RelationInput;\n  position?: Position.Position;\n};\n\n/**\n * Create an extension that matches nodes by schema type.\n * The entity type is inferred from the schema type and works for both object and relation schemas.\n * Returns an Effect to allow callbacks to access services via dependency injection.\n */\nexport const createTypeExtension = <T extends Type.AnyEntity, R = never, const Id extends string = string>(\n  options: CreateTypeExtensionOptions<T, R, Id>,\n): Effect.Effect<BuilderExtension[], never, R> => {\n  const { id, type, actions, actionGroups, connector, relation, position } = options;\n  // `string` for the id: this forwards an already-validated value, so re-checking it here would\n  // reject the `Id` type parameter's error branch rather than the caller's literal.\n  return createExtension<Type.InstanceType<T>, R, string>({\n    id,\n    match: (node) => (Entity.instanceOf(type, node.data) ? Option.some(node.data) : Option.none()),\n    actions,\n    actionGroups,\n    connector,\n    relation,\n    position,\n  });\n};\n"],"mappings":"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"}