fbp-spec

Storage specification and manipulation API for flow-based programming graphs. Use when working with FBP graph storage, manipulating graph data structures, or using the @fbp/spec package.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "fbp-spec" with this command: npx skills add constructive-io/constructive-skills/constructive-io-constructive-skills-fbp-spec

Storage specification and manipulation API for flow-based programming graphs.

Installation

pnpm add @fbp/spec

Overview

@fbp/spec provides a two-layer type system for flow-based programming graphs:

LayerPurpose
StorageMinimal canonical format for persistence
RendererExtended types with derived data for UI
APIPure functions for graph manipulation

The storage layer is designed for content-addressable storage (merkle trees) where each graph state can be uniquely hashed.

Design Philosophy

Boundary Nodes as Single Source of Truth

Traditional graph formats store interface definitions in two places (arrays and boundary nodes), causing sync bugs. This spec eliminates the problem by using boundary nodes as the ONLY source of truth.

The inputs/outputs/props arrays are NOT stored in the storage format — they are derived at runtime from boundary nodes and cached in the renderer layer.

Path-Based Identity

Nodes are identified by their path from the root:

/                     # Root scope
/add1                 # Root-level node
/subnet1/add1         # Node inside subnet1
/subnet1/nested/add1  # Deeply nested node

Per-Scope Edges

Edges are stored within the scope they belong to. Root-level edges are in graph.edges, subnet edges are in node.edges.

API Reference

All API functions are pure and immutable — they return new graphs without modifying the original.

Path Utilities

import { parsePath, joinPath, getParentPath, getNodeName, isRootPath } from '@fbp/spec';

parsePath('/foo/bar')     // ['foo', 'bar']
joinPath(['foo', 'bar'])  // '/foo/bar'
getParentPath('/foo/bar') // '/foo'
getNodeName('/foo/bar')   // 'bar'
isRootPath('/')           // true

Node Operations

import { insertNode, removeNode, renameNode, moveNode } from '@fbp/spec';

// Insert a node at root scope
const newGraph = insertNode(graph, '/', { 
  name: 'add1', 
  type: 'math/add' 
});

// Insert into a subnet
const newGraph = insertNode(graph, '/subnet1', { 
  name: 'multiply1', 
  type: 'math/multiply' 
});

// Remove a node and connected edges
const newGraph = removeNode(graph, '/add1');

// Rename a node (updates edge references)
const newGraph = renameNode(graph, '/add1', 'adder');

// Move a node to a different scope
const newGraph = moveNode(graph, '/add1', '/subnet1');

Property Operations

import { setProps, getProps, removeProp } from '@fbp/spec';

const newGraph = setProps(graph, '/add1', [
  { name: 'a', value: 5 },
  { name: 'b', value: 10 }
]);

const props = getProps(graph, '/add1');
// [{ name: 'a', value: 5 }, { name: 'b', value: 10 }]

const newGraph = removeProp(graph, '/add1', 'a');

Edge Operations

import { addEdge, removeEdge } from '@fbp/spec';

const newGraph = addEdge(graph, '/', {
  src: { node: 'input1', port: 'value' },
  dst: { node: 'add1', port: 'a' }
});

const newGraph = removeEdge(graph, '/',
  { node: 'input1', port: 'value' },
  { node: 'add1', port: 'a' }
);

Query Helpers

import { getNode, getNodes, getEdges, findNodes, findBoundaryNodes, hasNode, countNodes } from '@fbp/spec';

const node = getNode(graph, '/subnet1/add1');
const rootNodes = getNodes(graph, '/');
const rootEdges = getEdges(graph, '/');

const addNodes = findNodes(graph, (node) => node.type === 'math/add');
// [{ node: {...}, path: '/add1' }, { node: {...}, path: '/subnet1/add2' }]

const boundary = findBoundaryNodes(graph, '/subnet1');
// { inputs: [...], outputs: [...], props: [...] }

if (hasNode(graph, '/subnet1/add1')) { /* exists */ }

const total = countNodes(graph);

Metadata Operations

import { setMeta, setPosition } from '@fbp/spec';

const newGraph = setMeta(graph, '/add1', { description: 'Adds two numbers' });
const newGraph = setPosition(graph, '/add1', 100, 200);

Example: Simple Math Graph

{
  "nodes": [
    { 
      "name": "input_a", 
      "type": "graphInput", 
      "meta": { "x": 0, "y": 0 },
      "props": [
        { "name": "portName", "value": "a" }, 
        { "name": "dataType", "value": "number" }
      ]
    },
    { 
      "name": "input_b", 
      "type": "graphInput", 
      "meta": { "x": 0, "y": 100 },
      "props": [
        { "name": "portName", "value": "b" }, 
        { "name": "dataType", "value": "number" }
      ]
    },
    { 
      "name": "add1", 
      "type": "math/add", 
      "meta": { "x": 200, "y": 50 }
    },
    { 
      "name": "output_sum", 
      "type": "graphOutput", 
      "meta": { "x": 400, "y": 50 },
      "props": [
        { "name": "portName", "value": "sum" }, 
        { "name": "dataType", "value": "number" }
      ]
    }
  ],
  "edges": [
    { "src": { "node": "input_a", "port": "value" }, "dst": { "node": "add1", "port": "a" } },
    { "src": { "node": "input_b", "port": "value" }, "dst": { "node": "add1", "port": "b" } },
    { "src": { "node": "add1", "port": "sum" }, "dst": { "node": "output_sum", "port": "value" } }
  ]
}

Normative Rules

  1. Boundary Nodes ARE the Interface — No separate inputs/outputs/props arrays in storage
  2. Edges are Per-Scope — Each subnet stores its own edges
  3. Path-Based Identity — Renaming/moving changes identity
  4. Minimal Storage — Only store what's needed to reconstruct the graph

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

constructive-graphql-codegen

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

github-workflows-ollama

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

github-workflows-pgpm

No summary provided by upstream source.

Repository SourceNeeds Review